Exceptional Control Flow(8)
Synchronizing Flows to Avoid Nasty Concurrency Bugs
an example code:
void handler(int sig) { pid_t pid; while ((pid = waitpid(-1, NULL, 0)) > 0) /* Reap a zombie child */ deletejob(pid); /* Delete the child from the job list */ if (errno != ECHILD) unix_error("waitpid error"); int main(int argc, char **argv) { int pid; Signal(SIGCHLD, handler); initjobs(); /* Initialize the job list */ while (1) { /* Child process */ if ((pid = Fork()) == 0) { Execve("/bin/date", argv, NULL); } /* Parent process */ addjob(pid); /* Add the child to the job list */ } exit(0); }
This is an example of a classic synchronization error known as a race. In this case, the race is between the call to addjob in the main routine and the call to deletejob in the handler. If addjob wins the race, then the answer is correct. If not, the answer is incorrect.
Figure 8.37 shows one way to eliminate the race in Figure 8.36. By blocking SIGCHLD signals before the call to fork and then unblocking them only after we have called addjob, we guarantee that the child will be reaped after it is added to the job list.
Notice that children inherit the blocked set of their parents, so we must be careful to unblock the SIGCHLD signal in the child before calling execve.
A wrapper for fork that randomly determines the order in which the parent and child execute, key words:
... /* Call the real fork function */ if ((pid = fork()) < 0) return pid; if (pid == 0) { /* Child */ if(bool) { usleep(secs); } } else { /* Parent */ if (!bool) { usleep(secs); } } /* Return the PID like a normal fork call */ return pid; ...
Nonlocal Jumps
C provides a form of user-level exceptional control flow, called a nonlocal jump, that transfers control directly from one function to another currently executing function without having to go through the normal call-and-return sequence.
The setjmp function saves the current calling environment in the env buffer, for later use by longjmp, and returns a 0.
The calling environment includes the program counter, stack pointer, and general purpose registers.
The longjmp function restores the calling environment from the env buffer and then triggers a return from the most recent setjmp call that initialized env.
The setjmp then returns with the nonzero return value retval.
The setjmp function is called once, but returns multiple times:
once when the setjmp is first called and the calling environment is stored in the env buffer,
and once for each corresponding longjmp call.
On the other hand, the longjmp function is called once, but never returns.
An important application of nonlocal jumps is to permit an immediate return from a deeply nested function call, usually as a result of detecting some error condition.
If an error condition is detected deep in a nested function call, we can use a nonlocal jump to return directly to a common localized error handler instead of laboriously unwinding the call stack.
Figure 8.39 shows an example of how this might work. The main routine first calls setjmp to save the current calling environment,
and then calls function foo, which in turn calls function bar. If foo or bar encounter an error, they return immediately from the setjmp via a longjmp call.
The nonzero return value of the setjmp indicates the error type, which can then be decoded and handled in one place in the code.
/*
Nonlocal jump example.
This example shows the framework for using nonlocal jumps to recover from error conditions
in deeply nested functions without having to unwind the entire stack.
*/
#include "csapp.h" jmp_buf buf; int error1 = 0; int error2 = 1; void foo(void), bar(void); int main()
{ int rc; rc = setjmp(buf);
if(rc==0) foo(); else if (rc == 1) printf("Detected an error1 condition in foo\n"); else if (rc == 2) printf("Detected an error2 condition in foo\n"); else printf("Unknown error condition in foo\n"); exit(0); } /* Deeply nested function foo */ void foo(void) { if (error1) longjmp(buf, 1); bar();
} void bar(void) { if (error2) longjmp(buf, 2); }
Another important application of nonlocal jumps is to branch out of a signal handler to a specific code location
Figure 8.40 shows a simple program that illustrates this basic technique.
The program uses signals and nonlocal jumps to do a soft restart whenever the user types ctrl-c at the keyboard.
The sigsetjmp and siglongjmp functions are versions of setjmp and longjmp that can be used by signal handlers.
/* A program that uses nonlocal jumps to restart itself when the user types ctrl-c. */ #include "csapp.h" 2 sigjmp_buf buf; void handler(int sig) { siglongjmp(buf, 1); } int main() { Signal(SIGINT, handler); if (!sigsetjmp(buf, 1)) printf("starting\n"); else printf("restarting\n"); while(1) { Sleep(1); printf("processing...\n"); } exit(0) }
The initial call to the sigsetjmp function saves the calling environment and signal context (including the pending and blocked signal vectors)when the pro- gram first starts.
The main routine then enters an infinite processing loop. When the user types ctrl-c, the shell sends a SIGINT signal to the process, which catches it.
Instead of returning from the signal handler, which would pass control back to the interrupted processing loop, the handler performs a nonlocal jump back to the beginning of the main program.
Tools for Manipulating Processes
/proc:
A virtual filesystem that exports the contents of numerous kernel data structures in an ASCII text form that can be read by user programs.
For example, type “cat /proc/loadavg” to see the current load average on your Linux system.
Summary
Exceptional control flow (ECF) occurs at all levels of a computer system and is a basic mechanism for providing concurrency in a computer system.