Lecture notes for CSC 252, Thur. Mar. 27 ff, 2014 Announcements A6 will be assigned next Tuesday trivia will be due a week from today main assignment will be due the night of Monday April 14 Read chapter 8. Skim chapter 7. I'm not sure how much of the latter I'll be able to cover in class. ======================================== Exceptions In computer architecture, an exception is a condition that causes an abrupt transfer of control to the operating system. Three categories: interrupt -- caused by I/O device (external to the processor) trap -- requested by the program (syscall) fault -- accidentally caused by the program (divide by zero, access to invalid address, illegal instruction, etc.) (NB: The book distinguishes between faults, which may return, and aborts, which cannot. Power failure is definitely an abort. Some user errors may be. Most authors don't make this fine a distinction. Also: exact use of the terms "interrupt", "exception", "trap", and "fault" varies from author to author and machine to machine.) (In programming languages, an exception is an abrupt, potentially multi-level return from a nested procedure -- more on this related but different use of the term later -- and much more in CSC 254.) Remember that processors don't "work" the way people do. They execute madly along at a steady, furious rate, never slowing, stopping, or "noticing" whether they're executing user programs, the kernel, or the idle loop -- or for that matter processing an interrupt. Seen in this light, interrupts are a "normal" event. The processor's instruction fetch unit *polls* for interrupts. If something unusual is happening in the machine, it inserts an interrupt pseudo-instruction into the pipeline instead of the normal instruction. Worst case we have a bogus instruction in the exception handler for bogus instructions and we spend all our time looping through it. Kernel (privileged) v. user (non-privileged) mode. Stuff you can do (only) in kernel mode: change mappings from virtual to physical addresses access I/O devices mask and unmask interrupts exit kernel mode access various special hardware registers having to do with address translation, exception handling, IEEE floating point modes, performance monitoring, etc. Transitions from kernel to user mode are performed explicitly by the operating system, generally at the end of an interrupt handler or kernel call, using a privileged RFE (return from exception) instruction. The processor starts up in kernel mode with address translation turned off, running the bootstrap loader from ROM. The bootstrap loader finds attached hard drives and copies of the OS on them; picks one, and loads it (still running in kernel mode). The OS then initializes and turns on address translation, starts the first user-level programs (including a shell) and switches to user mode. Exceptions ideally occur "between" instructions, though some machines don't always provide a clean break. An exception is said to be *precise* if when the HW traps into the OS every instruction before the exception has completed and no instruction after the exception has had any noticeable effect. An exception is said to be *restartable* if the HW provides the OS with enough information to tell which instructions have completed, to complete those that haven't completed (if any), and to get the pipeline going again in user mode. An exception can be restartable w/out being precise. what does the hardware do when an exception occurs? For faults and aborts, squash the current instruction. squash any subsequent instructions that have started down the pipeline(s) disable further exceptions switch to kernel mode. This induces a change of address space (see below) a change of stack pointer (%esp) push the address at which execution can resume onto the (kernel) stack push other processor state, including the condition codes CISC machines generally put interrupt state onto the kernel stack. RISC machines tend to put this state in special registers instead. jump to a pre-defined address On the x86 there is a table in the kernel address space that lists the address of the exception-handling routines, one for each class of exception. The base address of the table is in a special hardware register (readable/writable only in kernel mode). An alternative approach, taken on many RISC machines, is to have a single exception handler address. The class of exception is put into another special register, and the OS arranges for the code of the handler to consist of a switch statement that uses the contents of that register as argument. All of this happens atomically. The return-from-exception instruction restores processor state from the (kernel) stack pops the return address into a temporary special register returns to user mode (this includes switching %esp and the memory map) puts the return address back into the PC continues Note that an exception handler can "return" to a different place and even a different address space by changing the return address and other state on the kernel stack before executing the RFE instruction. This is how context switches happen. Under Windows 9X/ME or MacOS <=9, there was a single address space that included all user processes and almost all of the operating system. Operating system calls were ordinary subroutine calls. In a "real" operating system (Linux, Windows 2000/XP/Vista/7/8, MacOS X), user-level processes all run in different address spaces, so they can't hurt each other, and a big chunk of the operating system (the *kernel*, which includes everything required to implement protection) runs in kernel mode. Syscall (trap) instructions are littered through /usr/lib/clib.a. User programs don't generally execute them in-line. The stuff in section 2 of the Unix manual consists of routines that package stuff up, maybe do some error checking, execute a syscall instruction, and then re-package results from the kernel for return to the user program. Communication of parameters to and from the kernel is usually done in ISA registers on a RISC machine, and on the (user) stack in a CISC machine. Many OSes choose to make the kernel-mode address map a superset of the user-mode address map: it includes the kernel and the most recently-running user program. This makes it easy on a CISC machine to access parameters on the user stack. If the kernel wants to run a different user-level program it switches address maps while in kernel mode, making a different user part of the address space visible, but not interfering with the kernel-mode part of the space, which overlaps in all address maps. Privileged instructions also allow the kernel to read and write locations in an arbitrary address space. This is slow, but useful if the current user's space is not in the kernel's map, or if the kernel needs to, say, copy information from one user space to another. More details about the x86 Up to 256 exception classes (slots in the exception jump table) 0-31 are hardware defined, and the same on all x86 systems. 0 divide by zero or divide overflow 13 general protection fault -- attempt to use invalid address 14 page fault -- attempt to use address not currently in RAM 18 machine check -- hardware error (abort: not recoverable) 32-255 are OS-specific. Most are associated with I/O devices. By convention (not enforced by HW) 128 is traditionally used for system calls. The OS will expect to find the syscall number and parameters on the user-level stack. ======================================== Processes A process is "an instance of a program in execution". Includes address map contents of physical memory in that address map contents of registers, including stack pointers (kernel and user) and PC environment variables (usually in memory) open file/socket descriptors user & group ids Context switches expensive due to both inherent latency and loss of cache/TLB footprint multiprogramming v. multiprocessing time-slicing (preemptive multiprogramming) Private address space Process creation fork duplicate (*not* shared) address space shared open file descriptors (subsequent opens not shared) pid_t fork(void) returns 0 to child; child id to parent (that's how they tell themselves apart) idiom: pid_t child; if (!(child = fork())) { execve(...); } anachronism; wasteful of resources. modern Unixes minimize cost via copy-on-write, but fork is still expensive. Other OSes (e.g. Windows) are better designed in this regard (though not others :-) getpid pid_t getpid(void) pid_t getppid(void) // parent error handling if ((status = syscall(args)) < 0) { fprintf(stderr, "program: %s\n", strerror(status)); exit(-1); } Always check all return codes from syscalls. Create a handy wrapper if you want. Book describes (in Sec. 8.3) a general approach. A simpler though not quite as pretty way is VERIFY(foo(args)); where #define VERIFY(E) \ {int stat = (E); \ if (stat < 0) { \ int err = errno; \ fprintf(stderr, "%s[%d] bad return(%d/%d): %s\n", \ __FILE__, __LINE__, stat, err, strerror(err)); \ exit(-1); \ }} This has the advantage of telling you where in your program you ran into trouble. In some cases you may get more precise information by using 'errno' instead of the status return from your library call -- some calls on some systems return -1 on all errors and put the more accurate value somewhere else. 'errno' is a name you can use to get that more accurate value. It may be a variable or a macro, depending on system. To get the right definition #include . process states running, stopped, terminated (zombie) "reaping" (awaiting) pid_t waitpid(pid_t pid, int *status, int options) pid is the one we want to wait for, or -1 to indicate any child status is the place to put the child's exit status options can be WNOHANG: don't wait; return 0 if no child has terminated yet WUNTRACED: wait until terminated *or* stopped status return encodes various info WIFEXITED(status) true if exited normally (via exit) WEXITSTATUS(status) exit status if exited normally WIFSIGNALED(status) true if exited due to uncaught signal WTERMSIG(status) signal, if any, that caused termination WIFSTOPPED(status) true if stopped (not terminated) WSTOPSIG(status) signal, if any, that caused stop status == ECHILD caller has no children status == EINTR call interrupted by signal exist several variants: wait, wait3, wait4, waitid; not discussed here If a process forgets to reap its terminated children, they continue to consume a slot in the process table (though not other resources). When the forgetful parent itself terminates, the kernel transfers ownership to "init", which runs in the background and periodically calls wait(). sleep unsigned int sleep (unsigned int secs) return is number of seconds actually slept (may be low if interrupted by signal) int pause(void) // wait for signal; returns -1 exit void exit(int status) // does not return // by convention, 0 is normal exit; negative numbers are errors process loading execve (one of several variants of exec) int execve(const char *filename, char *const argv[], char *const envp[]) // doesn't normally return; returns -1 on error start-up arguments argc argv argv[0] is program name (by convention) argv[argc] is null; one typically writes for (int i = 0; i < argc; i++) { // do something with argv[i] } envp no analogue of argc pointers point to NAME=value pairs last pointer is null argv and envp strings are placed at bottom of stack by start-up code. That code calls main as a subroutine, and calls exit after main returns. char *getenv(const char *name) // returns 0 if not found int setenv(const char *name, const char *newvalue, int overwrite) // if not overwrite, return -1 if found void unsetenv(const char *name) startup stack contents: bottom 1 null-terminated environment strings 2 null-terminated argument strings 3 0 4 last environment pointer ... 5 first environment pointer 6 0 7 last argument pointer ... 8 first argument pointer 9 < stuff used by dynamic linker > 10 envp (points to 5) 11 argv (points to 8) 12 argc 13 < stack frame for main > top process groups processes organized into groups A process group represents a bunch of processes working together, e.g. in a series of pipes. Process groups matter for signal handling (coming up below). pid_t getpgrp(void) // process group id pid_t getpgid(pid_t pid) // somebody else's process group id // (mine if pid == 0) int setpgid(pid_t pid, pid_t pgid) // change process group id of process pid to pgid // change my process group id if pid == 0 // (there are restrictions on who is allowed to do this) Process groups organized into "login sessions". Each login session has a "controlling tty". One process of the group is in the "foreground"; the others are in the background. Foreground processes can access the terminal (stdin, out, err). Background processes get a signal if they try to access the terminal. job control each terminal (xterm, ssh connection, etc.) has an associated process group. If you type ^C, ^Z, or ^Y from the terminal, every process in that process group gets the SIGINT or SIGSTP. (^Y is delayed suspend -- takes affect only when job tries to read from stdin) The standard Unix shell manages processes by putting its children in different process groups, and then assigning the terminal to the group it wants to be in the "foreground". (By definition, the foreground process group is the one that owns the terminal. Others are background process groups.) (You might be tempted to leave/put the shell and the foreground job in the same process group, but this will cause trouble later if you want to move the job to the background: you'll want to give it a new process group, but it may have spawned a whole process tree under it, and you can't easily change all the processes in the tree at once.) The kernel sends a SIGTTIN signal to (all processes of the process group of) a background process that tries to read form the terminal. It optionally sends a signal to a background group that tries to write to the terminal; this is controlled by the tostop termio (stty) setting, normally off by default. *** In the shell assignment as defined by the authors, you were not required to change ownership of the terminal. (The CMU reference solution does not.) That meant your processes wouldn't be able to read from the terminal. You were to leave the terminal attached to the shell, which would catch SIGINT and SIGSTP, and forward them to the process group it is *pretending* is in the foreground. We're requiring you to fix this this year. We'll be fixing the reference version, so they'll match. The book is confusing in its discussion of this mechanism (p. 740). It suggests that "real" shells work the way the one in the CMU version of the assignment does, but this isn't really the case. To assign a terminal to a process group (i.e., make it the foreground process group): #include int status = tcsetpgrp(0, child_gpid); // results in SIGTTOU if caller is in the background You can also query the foreground process group: foreground_gpid = tcgetpgrp(0); pthreads processes v. kernel threads v. user-level threads ======================================== Signals Essentially user-level software interrupts. Used to induce asynchronous control transfer. All modern OSes have something like signals. We'll focus on the Unix variety, but the underlying concepts carry over to Windows and other OSes. About 30 different signal values in most Unix variants, including Linux. Some particularly important examples: SIGSEGV use of invalid address SIGBUS misaligned address SIGILL illegal instruction SIGINT ^C SIGTSTP ^Z SIGCONT continue SIGKILL termination signal; can't be caught or ignored SIGHUP loss of terminal connection (hangup) SIGIO asynchronous I/O completion SIGALRM timer expiration (previously requested with alarm()) SIGTTIN attempt to read from terminal from background SIGTTOU attempt to write to terminal from background SIGWINCH change in GUI window size SIGCHILD termination or stop of child book gives whole Linux list in Figure 8.25 (p. 737) or try 'man 7 signal' NB: kill program and kill() function do NOT necessarily kill a process; they just send it a signal. The name is unfortunate. Signals are *delivered* by the kernel in response to various events, such as those listed above. They are *received* by the process when unblocked (unmasked). A delivered but unreceived signal is said to be *pending*. Like hardware exceptions, signals are received one at a time can be blocked (masked) are held for later receipt when masked but are not queued (are lost if delivered when already pending) Signals can be delivered to individual processes or to all processes of a given process group (or, in the case of loss of terminal connection, all processes of all process groups of a given login session). Typically a process group id is the same as the process id of one of its members. unix> kill -9 12345 send signal 9 (SIGKILL) to process 12345 unix> kill -9 -12345 send signal 9 to all processes in process group 12345 There are of course rules on who can send what kinds of signals to whom. See the man pages. A process can wait for a signal with pause(). It can install a *handler* function for a given signal (or arrange to ignore certain signals) with signal() or sigaction(). The former is simpler but inconsistent across Unix versions; the latter is more portable. To use these, include . #include typedef void sighandler_t(int) sighandler_t *signal(int signum, sighandler_t *handler) handler can be SIG_IGN ignore signal (assuming this is allowed) SIG_DFL restore default behavior function address install handler previous value is returned (or SIG_ERR on error) Handler will be called asynchronously when the signal is received (as soon as it has been delivered and is not blocked). The single argument to the handler is the signal number. (This allows the same handler to be used for different signals.) When the handler returns the program will usually continue with the instruction after the one it executed immediately before receiving the signal. Exception: if blocked in the kernel for a "long" system call in some variants of Unix (e.g. Solaris), the syscall will return with an EINTR error rather than continuing/restarting automatically (as it does in Linux). Simple example. If you type ^C at most programs they will terminate. But they can arrange not to: #include void my_handler(int sig) { printf("caught signal %d\n", sig); } int main() { if (signal(SIGINT, my_handler) == SIG_ERR) unix_error("signal error'); // note possibility that old handler address might appear // negative; VERIFY macro above can't be used while (1) { pause(); printf("continuing\n"); } } You can kill this with SIGKILL (and various other things) but not SIGINT. The book has several more examples. For the current assignment you will have to do a lot of signal handling, to notice when children terminate or stop, so you can reap them, or inform the user that they have stopped. (In the original CMU version of the assignment, you would also use signals to catch keyboard events [^C, ^Y ^Z] and "pass them on" to the appropriate child process group. This year we're doing job control right, so you don't have to pass them on, but you do have to ignore ^Z and ^Y, and kill current input line & reprompt on ^C.) ---------------------------------------- Problem: signal() has inconsistent semantics across Unix variants: - are slow syscalls automatically restarted when interrupted by a signal? (OW calls interrupted this way return EINTR) - are signals blocked during handler? - is handler still established after it returns, as opposed to having to be reestablished explicitly? - is a blocked signal held for later delivery? The sigaction() routine is newer and move verbose. It allows all these things to be explicitly controlled. Figure 8.34 (p. 752) in the book gives a Signal() wrapper for signal() that answers YES to all four of the above questions. Blocking signals: . int sigemptyset(sigset_t *set); initializes a sigset_t mask for use in subsequent functions. . int sigaddset(sigset_t *set, int signum); adds a signal to the list of ones you care about in a mask. . int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 'how' can be SIG_BLOCK, SIG_UNBLOCK, or SIG_SETMASK. Lets you say that you are -- or are not -- interested in signals of a particular type at the moment. SIG_SETMASK sets the blocked signals to precisely the specified set. If you add a signal to the ignore set (via calling sigprocmask with SIG_BLOCK and a mask containing the signal), then even if you have a signal handler registered for that signal, it won't be called. Warnings: - Blocked signals are delivered but kept pending. If more than one is delivered, it is LOST. So you can't count signals; you can only tell that *at least one* has occurred. So, for example, if you set up a handler for SIGCHLD, it has to reap _all_ available children in a loop. - Signal handlers should be short and sweet. Acquiring a lock while inside a signal handler can lead to deadlock -- BAD! - Granularity of response to SIGALRM: if I have to wait for my process to be rescheduled, and quanta on my machine are 10000 uSecs, then setting an alarm for 250 uSecs is pointless if I have to wait through multiple quanta to be rescheduled. Defaults: Absent a call to signal() or sigaction(), every signal has a *default* action. Some signals (e.g. SIGCHILD and SIGWINCH) are ignored by default. Others (e.g. SIGSEGV and SIGHUP) cause the kernel to terminate the process, in some cases (e.g. SIGSEGV but not SIGHUP) dumping a "core" file for post-mortem analysis by a debugger. A few (e.g. SIGTTIN) cause the process to stop until they get a subsequent SIGCONT signal. One (SIGKILL) cannot be handled or ignored; it always terminates the process. [ Comparison between MS Windows Events and Unix Signals: [ . Signals: only 2 user-defined signals; thousands of user-defined events [ . Windows events allow bidirectional communication [ . PostMessage() and SendMessage() return a DWORD (unsigned long) that [ can hold any value you want to define as the response. Such as the [ number of times the user has typed 'Q' into a buffer. [ . Signals return only 0 or -1 to indicate whether they were delivered [ . Windows Events cannot be blocked (but they can be ignored) [ . Windows events are queued -- can have more than one of the same type [ pending; can get more of same type while processing one ======================================== Non-local jumps "Poor man's exceptions" -- the C approximation of what you get in C++, Java, ML, Ada, Common Lisp, ... #include int setjmp(jmp_buf jb); sigsetjmp(sigjmp_buf jb, int savesigs); returns once, maybe twice void longjmp(jmp_buf jb, int retval); void siglongjmp(sigjmp_buf jb, int retval); doesn't return; causes setjmp to return again Setjmp sets you up to "catch" a longjmp: jmp_buf jb; if (err = setjmp(jb) == 0) { // do what you normally want to do foo(); } else { // handle error, using code passed back through err } ... foo() { ... longjmp(jb, err); } Warnings: (1) setjmp is usually not a normal function. It's typically implemented as a macro or special-cased by the compiler. Note in particular that jmp_buf is used to store state, but is not passed by reference. (2) Upon an "abnormal" return from a setjmp, global and static variables will have whatever value they had at the time the longjmp occurred, but the value of local variables is *undefined*, unless they have been declared volatile. This is not nearly as nice as the guarantees in C++, Java, etc. setjmp()/longjmp() vs. language-level exceptions: - no unwinding of stack -- direct restoration of former state - destructors of objects in stack not called - easy to leak memory; side effects not undone automatically - only local (stack-based) variables restored; globals, statics, dynamic vars unchanged (because they aren't stack based) - somewhat faster return (from not having to unwind the stack as much) Like goto, setjmp()/longjmp() should only be used when they improve the readability of the program or the performance gain is critical. sigsetjmp and siglongjmp are interoperable with setjmp and longjmp. The difference is that sigsetjmp saves the signal state of the process (what is masked, what has handlers, what is ignored, etc.) in addition to the stack state, and siglongjmp restores this state.