Professional Documents
Culture Documents
WWW Cs Rochester
WWW Cs Rochester
edu/courses/252/spring2014/notes/08_exceptions
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
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 1/10
2/4/2017 www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions
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
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 2/10
2/4/2017 www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions
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
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 3/10
2/4/2017 www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions
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 <errno.h>.
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
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 4/10
2/4/2017 www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions
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)
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 5/10
2/4/2017 www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions
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 <unistd.h>
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.
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 6/10
2/4/2017 www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions
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 <signal.h>.
#include <signal.h>
typedef void sighandler_t(int)
sighandler_t *signal(int signum, sighandler_t *handler)
handler can be
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 7/10
2/4/2017 www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions
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 <signal.h>
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.
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 8/10
2/4/2017 www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions
. 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 <setjmp.h>
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
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 9/10
2/4/2017 www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions
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.
http://www.cs.rochester.edu/courses/252/spring2014/notes/08_exceptions 10/10