zettelkasten

Search IconIcon to open search
Dark ModeDark Mode

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
    1. Exceptions
      • responds to change in system state
      • Impl: hardware and OS
  • Higher level mechanisms
    1. Context switch
      • Impl: OS and hardware timer
    2. Signals
      • Impl: OS
    3. Nonlocal jumps (setjmp(), longjmp())
      • Impl: C runtime library

H2 Exception

transfer control to kernel to respond to some event

Pasted image 20230808141705.png

  • User code -exception-> kernel
  • Kernel does something (exception handler), then
    1. return to current instruction $I_{curr}$ (think retry)
    2. return to next instruction $I_{next}$
    3. abort
    4. (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
  • 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
    • 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

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
  • 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

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 ignore
  • SIG_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 calling printf in handler will result in both printf waiting for each other
  • 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 or 1, not increment

H3 Blocking and unblocking

  • Implicit blocking
    • Kernel auto blocks signal currently being processed
      • e.g. SIGINT can’t interrupt SIGINT handler
  • Manual
    • sigprocmask - to block
    • sigfillset - to mask everything
    • sigaddset - to add signal to mask
    • sigemptyset - to make empty mask
    • sigdelset - to delete signal from mask
    • sig...

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 wasteful
  • while (!pid) {pause()} - could get signal between checking pid and pause
  • while (!pid) {sleep(1)} - could slower to respond
  • while (!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 id
  • setpgid() change group of a process

Killing process vs killing group:

  • kill -9 24818 kill process 24818
  • kill -9 -24817 kill everything in process group 24818