Exceptional control flow
#lecture note based on 15-213 Introduction to Computer Systems
We don’t yet have a way to control flow if… say… the printer is on fire. We need a way to react to changing system state, not just program state.
H6 Types of control flow
- programme state
- jumps, branches
- call, return
- system state
- receiving data from network
- division by 0
- Control-C on keyboard
- system timer expires
- page fault
H6 Checking approaches
- Inefficient: keep checking
- Efficient: somehow change the control flow without explicit checking
H6 Exceptional Control Flow Abstraction Level
- Low level mechanisms
- Exceptions
- responds to change in system state
- Impl: hardware and OS
- Exceptions
- Higher level mechanisms
- Context switch
- Impl: OS and hardware timer
- Signals
- Impl: OS
- Nonlocal jumps (
setjmp()
,longjmp()
)- Impl: C runtime library
- Context switch
H2 Exception
transfer control to kernel to respond to some event
- User code -exception-> kernel
- Kernel does something (exception handler), then
- return to current instruction $I_{curr}$ (think retry)
- return to next instruction $I_{next}$
- abort
- (signal process, technically will still return(?))
- Exception Table - Table somewhere in memory, pointing to code to handle something given some error code
- Start of exception table is in a special
exception table base register
- Each event has unique exception number, which indexes into the exception table to get handler
- Start of exception table is in a special
- ECF Taxonomy
- Async
- Interrupt
- Something sends signal to a pin on processor telling it to interrupt
- Handler usually returns to next instruction
- Timer interrupt - some cpu clock keeps sending interrupting
- Keyboard interrupt - control C, getting things from internet
- Interrupt
- Sync - when executing
- Traps
- e.g. syscalls, gdb breakpoints
- Usually just return to next
- Faults
- Unintented, maybe recoverable
- Page fault, floating point exceptions
- Usually return to current
- Aborts
- Unintended, unrecoverable
- Illegal instruction, parity error, machine check
- Abort programme
- Traps
- Async
H2 Signals
kernel interrupt processes because of some state change, either initiated by kernel or at some other process’s request
Example state changes:
- Child terminated
- Background process failed
- …
ID Name Default action that causes it
2 SIGINT terminate control c
9 SIGKILL terminate kill (cannot ignore)
11 SIGSEGV terminate seg fault
17 SIGCHLD ignore something happened to one of your children
...
The default actions can be handled differently, allowing recovery
Implementation:
- Data structure
- For each process, a pending vector and a blocked vector
- Each bit corresponds to a signal, and can be either on or off
- Sending
- kernel updates some state in target process’s context
- kernel keeps track of pending signals to each process
- there’s at most 1 pending for each signal, not queued or counted, so target process only receive once
- e.g. maybe something happened to multiple children only one SIGCHILD
- there’s at most 1 pending for each signal, not queued or counted, so target process only receive once
- Receiving
- receiving process forced to react to signal
- when kernel context switches back to a process, pick a pending (viz. not yet received) signal from and trigger handler within the process (usually the least nonzero bit in pending vector)
- Reactions
- Ignore
- Terminate
- Catch - write own handler. Like a jump but from anywhere in middle of execution
- Repeat until all pending signals are processed
- Go to next instruction of the process, if it’s still alive
- Blocking: deciding to do not disturb on certain signals
- done with
sigprocmask
- stays on blocked vector list maintained by kernel
- not receive them until unblocked
- done with
H3 Implementing signal handler
handler_t *signal(int signum, handler_t *handler)
Handler could be SIG_IGN
for ignore, SIG_DFL
for default
Example from bomb lab
void sigint_handler(int sig) /* SIGINT handler */
{
printf("So you think you can stop the bomb with ctrl-c, do you?\n");
sleep(2);
printf("Well...");
fflush(stdout);
sleep(1);
printf("OK. :-)\n");
exit(0);
}
int main(int argc, char** argv)
{
/* Install the SIGINT handler */
if (signal(SIGINT, sigint_handler) == SIG_ERR)
unix_error("signal error");
/* Wait for the receipt of a signal */
pause();
return 0;
}
Note:
- By default handler returns to kernel
- One could expect having to handle another signal while handling another
Some default handlers to pass as handler
:
SIG_IGN
to ignoreSIG_DFT
use default behaviour
H4 Guidelines
- keep handlers simple
- set global flag and return
- only call async-signal-safe functions
- see
man 7 signal-safety
printf
,sprintf
,malloc
,exit
not safe - these things have lock- e.g. getting signal in middle of
printf
and callingprintf
in handler will result in bothprintf
waiting for each other
- e.g. getting signal in middle of
- see
- Save and restore
errno
, because code running prior to signal may rely on this value - Block all signals, to protect shared data structs
- Declare global vars as
volatile
, to prevent compiler putting them in register - Use global flags marked
volatile sig_atomic_t
- e.g. only read or write them
0
or1
, not increment
- e.g. only read or write them
H3 Blocking and unblocking
- Implicit blocking
- Kernel auto blocks signal currently being processed
- e.g. SIGINT can’t interrupt SIGINT handler
- Kernel auto blocks signal currently being processed
- Manual
sigprocmask
- to blocksigfillset
- to mask everythingsigaddset
- to add signal to masksigemptyset
- to make empty masksigdelset
- to delete signal from masksig...
Example:
sigset_t mask, prev_mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT); // add SIGINT to mask
sigprocmask(SIG_BLOCK, &mask, &prev_mask); // set new mask
// run some code that shall not get interrupted by SIGINT
sigprocmask(SIG_SETMASK, &prev_mask, NULL); // restore mask
H3 Explicitly waiting for signal
So that we don’t just while loop and waste resources
while (!pid) {}
- works but wastefulwhile (!pid) {pause()}
- could get signal between checking pid and pausewhile (!pid) {sleep(1)}
- could slower to respondwhile (!pid) {sigsuspend(&prev)}
- blocks, pause, then restore mask
Pseudocode:
while (1) {
sigprocmask(SIG_BLOCK, &mask, &prev); // block
// ...
while (!pid)
sigsuspend(&prev); // wait for previously blocked signal
// ...
sigprocmask(SIG_SETMASK, &prev, NULL); // restore mask
}
H3 Process group
GPID kept track of process group in addition to PID. This allows sending signal to entire group and making all process in the group handle it. E.g. kill a foreground job and all its children.
To send signal to every process in a process group, call kill
but with negative pid
corresponding to the desired gpid
Working with process group ID:
getpgrp()
get current process’s group idsetpgid()
change group of a process
Killing process vs killing group:
kill -9 24818
kill process 24818kill -9 -24817
kill everything in process group 24818