Principles of Computer Systems
Spring 2019
Stanford University
Computer Science Department
Instructors: Chris Gregg and
Phil Levis
"The barman asks what the first one wants, two race conditions walk into a bar."
sleep(3)
so all five children finish at a almost the same time (they are running in parallel, remember).sleep(3 * kid)
has become sleep(3)
cgregg*@myth60$ ./broken-pentuplets
Let my five children play while I take a nap.
At least one child still playing, so dad nods off.
Kid #1 done playing... runs back to dad.
Kid #2 done playing... runs back to dad.
Kid #3 done playing... runs back to dad.
Kid #4 done playing... runs back to dad.
Kid #5 done playing... runs back to dad.
Dad wakes up! At least one child still playing, so dad nods off.
Dad wakes up! At least one child still playing, so dad nods off.
Dad wakes up! At least one child still playing, so dad nods off.
Dad wakes up! At least one child still playing, so dad nods off.
^C # I needed to hit ctrl-c to kill the program that loops forever!
cgregg@myth60$
SIGCHLD
signals are delivered while dad is off the processor, the operating system only records the fact that at one or more SIGCHLD
s came in.SIGCHLD
handler, it must do so on behalf of the one or more signals that may have been delivered since the last time it was on the processor.SIGCHLD
handler needs to call waitpid
and WNOHANG, in a loop, as with:static void reapChild(int unused) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break; // note the < is now a <=
numDone++;
}
}
static void reapChild(int unused) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
numDone++;
}
}
static void reapChild(int unused) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
numDone++;
}
}
static void reapChild(int unused) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
numDone++;
}
}
SIGCHLD
handlers generally have this while
loop structure.
waitpid(-1, &status, WNOHANG)
WNOHANG
being passed in as the third argument.waitpid
can include several flags bitwise-or'ed together.
WUNTRACED
informs waitpid
to block until some child process has either ended or been stopped ("stopped" in this case really means "paused").WCONTINUED
informs waitpid
to block until some child process has either ended or resumed from a stopped state.WUNTRACED | WCONTINUED | WNOHANG
asks that waitpid
return information about a child process that has changed state (i.e. exited, crashed, stopped, or continued) but to do so without blocking.fork
) and asynchronous signal handling (as you do with signal
), concurrency issues and race conditions will creep in unless you code very, very carefully.printf
statements stating where pids would be added to and removed from the job list data structure instead of actually doing it.// job-list-broken.c
static void reapProcesses(int sig) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
printf("Job %d removed from job list.\n", pid);
}
}
char * const kArguments[] = {"date", NULL};
int main(int argc, char *argv[]) {
signal(SIGCHLD, reapProcesses);
for (size_t i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) execvp(kArguments[0], kArguments);
sleep(1); // force parent off CPU
printf("Job %d added to job list.\n", pid);
}
return 0;
}
myth60$ ./job-list-broken
Sun Jan 27 03:57:30 PDT 2019
Job 27981 removed from job list.
Job 27981 added to job list.
Sun Jan 27 03:57:31 PDT 2019
Job 27982 removed from job list.
Job 27982 added to job list.
Sun Jan 27 03:57:32 PDT 2019
Job 27985 removed from job list.
Job 27985 added to job list.
myth60$ ./job-list-broken
Sun Jan 27 03:59:33 PDT 2019
Job 28380 removed from job list.
Job 28380 added to job list.
Sun Jan 27 03:59:34 PDT 2019
Job 28381 removed from job list.
Job 28381 added to job list.
Sun Jan 27 03:59:35 PDT 2019
Job 28382 removed from job list.
Job 28382 added to job list.
myth60$
myth60$ ./job-list-broken
Sun Jan 27 03:57:30 PDT 2019
Job 27981 removed from job list.
Job 27981 added to job list.
Sun Jan 27 03:57:31 PDT 2019
Job 27982 removed from job list.
Job 27982 added to job list.
Sun Jan 27 03:57:32 PDT 2019
Job 27985 removed from job list.
Job 27985 added to job list.
myth60$ ./job-list-broken
Sun Jan 27 03:59:33 PDT 2019
Job 28380 removed from job list.
Job 28380 added to job list.
Sun Jan 27 03:59:34 PDT 2019
Job 28381 removed from job list.
Job 28381 added to job list.
Sun Jan 27 03:59:35 PDT 2019
Job 28382 removed from job list.
Job 28382 added to job list.
myth60$
sleep(1)
call, which allows the child process to churn through its date
program and print the date and time to stdout
.sleep(1)
is removed, it's possible that the child executes date
, exits, and forces the parent to execute its SIGCHLD
handler before the parent gets to its own printf
. The fact that it's possible means we have a concurrency issue.reapProcesses
from running until it's safe or sensible to do so. Restated, we'd like to postpone reapProcesses
from executing until the parent's printf
has returned.sigset_t
type is a small primitive—usually a 32-bit, unsigned integer—that's used as a bit vector of length 32. Since there are just under 32 signal types, the presence or absence of signum
s can be captured via an ordered collection of 0's and 1's.sigemptyset
is used to initialize the sigset_t
at the supplied address to be the empty set of signals. We generally ignore the return value.sigaddset
is used to ensure the supplied signal number, if not already present, gets added to the set addressed by additions
. Again, we generally ignore the return value.sigprocmask
adds (if how
is set to SIG_BLOCK
) or removes (if how
is set to SIG_UNBLOCK
) the signals reachable from set
to/from the set of signals being ignored at the moment. oldset is the location of a sigset_t
that can be updated with the set of signals being blocked at the time of the call, so you can restore it later if you need to.int sigemptyset(sigset_t *set);
int sigaddset(sigset_t *additions, int signum);
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
SIGCHLD
s:NULL
is passed as the third argument to both sigprocmask
calls. That just means that I don't care to hear about what signals were being blocked before the call.static void imposeSIGCHLDBlock() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
}
static void liftSignalBlocks(const vector<int>& signums) {
sigset_t set;
sigemptyset(&set);
for (int signum: signums) sigaddset(&set, signum);
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
// job-list-fixed.c
char * const kArguments[] = {"date", NULL};
int main(int argc, char *argv[]) {
signal(SIGCHLD, reapProcesses);
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
for (size_t i = 0; i < 3; i++) {
sigprocmask(SIG_BLOCK, &set, NULL);
pid_t pid = fork();
if (pid == 0) {
sigprocmask(SIG_UNBLOCK, &set, NULL);
execvp(kArguments[0], kArguments);
}
sleep(1); // force parent off CPU
printf("Job %d added to job list.\n", pid);
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
return 0;
}
myth60$ ./job-list-fixed
Sun Jan 27 05:16:54 PDT 2019
Job 3522 added to job list.
Job 3522 removed from job list.
Sun Jan 27 05:16:55 PDT 2019
Job 3524 added to job list.
Job 3524 removed from job list.
Sun Jan 27 05:16:56 PDT 2019
Job 3527 added to job list.
Job 3527 removed from job list.
myth60$ ./job-list-fixed
Sun Jan 27 05:17:15 PDT 2018
Job 4677 added to job list.
Job 4677 removed from job list.
Sun Jan 27 05:17:16 PDT 2018
Job 4691 added to job list.
Job 4691 removed from job list.
Sun Jan 27 05:17:17 PDT 2018
Job 4692 added to job list.
Job 4692 removed from job list.
myth60$
reapProcesses
is the same as before, so I didn't reproduce it.printf
—that is, it's added the pid to the job list.fork
ed process inherits blocked signal sets, so it needs to lift the block via its own call to sigprocmask(SIG_UNBLOCK, ...)
. While it doesn't matter for this example (date
almost certainly doesn't spawn its own children or rely on SIGCHLD
signals), other executables may very well rely on SIGCHLD
, as signal blocks are retained even across execvp
boundaries.myth60$ ./job-list-fixed
Sun Jan 27 05:16:54 PDT 2019
Job 3522 added to job list.
Job 3522 removed from job list.
Sun Jan 27 05:16:55 PDT 2019
Job 3524 added to job list.
Job 3524 removed from job list.
Sun Jan 27 05:16:56 PDT 2019
Job 3527 added to job list.
Job 3527 removed from job list.
myth60$ ./job-list-fixed
Sun Jan 27 05:17:15 PDT 2018
Job 4677 added to job list.
Job 4677 removed from job list.
Sun Jan 27 05:17:16 PDT 2018
Job 4691 added to job list.
Job 4691 removed from job list.
Sun Jan 27 05:17:17 PDT 2018
Job 4692 added to job list.
Job 4692 removed from job list.
myth60$
int kill(pid_t pid, int signum);
int raise(int signum); // equivalent to kill(getpid(), signum);
kill
and raise
kill
system call. And processes can even send themselves signals using raise
.kill
system call is analogous to the /bin/kill
shell command.kill
implies SIGKILL
implies death.kill
and raise
. Just make sure you call it properly.pid
parameter is overloaded to provide more flexible signaling.pid
is a positive number, the target is the process with that pid.pid
is a negative number less than -1, the targets are all processes within the process group abs(pid)
. We'll rely on this in Assignment 4.pid
can also be 0 or -1, but we don't need to worry about those. See the man page for kill
if you're curious.$ ./simplesh
simplesh> ls -l five-children.c
-rw------- 1 cgregg operator 1670 Sep 23 09:29 five-children.c
simplesh> date
Wed Oct 9 09:48:14 PDT 2019
simplesh>
mysystem
, which was a very simple shell that allowed us to run commands:simplesh
operates as a read-eval-print loop—often called a repl—which itself responds to the many things we type in by forking off child processes.
simplesh
process.ls
, cp
, our own CS110 search
(which we wrote during our second lecture), or even emacs
.emacs &
—is an instruction to execute the new process in the background without forcing the shell to wait for it to finish.simplesh
is presented on the next slide. Where helper functions don't rely on CS110 concepts, I omit their implementations.Here's the core implementation of simplesh (full implementation is right here, and you can run the code on the following slide):
int main(int argc, char *argv[]) {
while (true) {
char command[kMaxCommandLength + 1];
readCommand(command, kMaxCommandLength);
char *arguments[kMaxArgumentCount + 1];
int count = parseCommandLine(command, arguments, kMaxArgumentCount);
if (count == 0) continue;
if (strcmp(arguments[0], "quit") == 0) break; // hardcoded builtin to exit shell
bool isbg = strcmp(arguments[count - 1], "&") == 0;
if (isbg) arguments[--count] = NULL; // overwrite "&"
pid_t pid = fork();
if (pid == 0) execvp(arguments[0], arguments);
if (isbg) { // background process, don't wait for child to finish
printf("%d %s\n", pid, command);
} else { // otherwise block until child process is complete
waitpid(pid, NULL, 0);
}
}
printf("\n");
return 0;
}
What is the main issue with our simplesh?
int main(int argc, char *argv[]) {
while (true) {
char command[kMaxCommandLength + 1];
readCommand(command, kMaxCommandLength);
char *arguments[kMaxArgumentCount + 1];
int count = parseCommandLine(command, arguments, kMaxArgumentCount);
if (count == 0) continue;
if (strcmp(arguments[0], "quit") == 0) break; // hardcoded builtin to exit shell
bool isbg = strcmp(arguments[count - 1], "&") == 0;
if (isbg) arguments[--count] = NULL; // overwrite "&"
pid_t pid = fork();
if (pid == 0) execvp(arguments[0], arguments);
if (isbg) { // background process, don't wait for child to finish
printf("%d %s\n", pid, command);
} else { // otherwise block until child process is complete
waitpid(pid, NULL, 0);
}
}
printf("\n");
return 0;
}
What is the main issue with our simplesh?
Background processes are left as zombies for the lifetime of the shell!
We can handle this with signal handling.
int main(int argc, char *argv[]) {
while (true) {
char command[kMaxCommandLength + 1];
readCommand(command, kMaxCommandLength);
char *arguments[kMaxArgumentCount + 1];
int count = parseCommandLine(command, arguments, kMaxArgumentCount);
if (count == 0) continue;
if (strcmp(arguments[0], "quit") ==) break; // hardcoded builtin to exit shell
bool isbg = strcmp(arguments[count - 1], "&") == 0;
if (isbg) arguments[--count] = NULL; // overwrite "&"
pid_t pid = fork();
if (pid == 0) execvp(arguments[0], arguments);
if (isbg) { // background process, don't wait for child to finish
printf("%d %s\n", pid, command);
} else { // otherwise block until child process is complete
waitpid(pid, NULL, 0);
}
}
printf("\n");
return 0;
}
// simplesh-with-redundancy.c
static void reapProcesses(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0) {;} // nonblocking, iterate until retval is -1 or 0
}
int main(int argc, char *argv[]) {
signal(SIGCHLD, reapProcesses);
while (true) {
// code to initialize command, argv, and isbg omitted for brevity
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], argv);
printf("%s: Command not found\n", argv[0]);
exit(0);
}
if (isbg) {
printf("%d %s\n", pid, command);
} else {
waitpid(pid, NULL, 0);
}
}
printf("\n");
return 0;
}
waitpid
to halt the shell until its foreground process has exited.
waitpid
call to block until that process has terminated.SIGCHLD
handler is invoked, and its waitpid
call is the one that culls the foreground process's resources.SIGCHLD
handler exits, normal execution resumes, and the original call to waitpid
returns -1 to state that there is no trace of a process with the supplied pid
.waitpid
from main
just to block until the foreground process vanishes.waitpid
—i.e. invoking a system call when you know it will fail—the waitpid
call is redundant and replicates functionality better managed in the SIGCHLD
handler.
waitpid
in one place: the SIGCHLD
handler.stsh
) where multiple processes are running in the foreground as part of a pipeline (e.g. more words.txt | tee copy.txt | sort | uniq
)waitpid
from only one place.// simplesh-with-race-and-spin.c
static pid_t fgpid = 0; // global, intially 0, and 0 means no foreground process
static void reapProcesses(int sig) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
if (pid == fgpid) fgpid = 0; // clear foreground process
}
}
static void waitForForegroundProcess(pid_t pid) {
fgpid = pid;
while (fgpid == pid) {;}
}
int main(int argc, char *argv[]) {
signal(SIGCHLD, reapProcesses);
while (true) {
// code to initialize command, argv, and isbg omitted for brevity
pid_t pid = fork();
if (pid == 0) execvp(argv[0], argv);
if (isbg) {
printf("%d %s\n", pid, command);
} else {
waitForForegroundProcess(pid);
}
}
printf("\n");
return 0;
}
fgpid
to hold the process is of the foreground process. When there's no foreground process, fgpid
is 0.
reapProcesses
, we have to choice but to make fgpid
a global.fgpid
is set to hold that process's pid. The shell then blocks by spinning in place until fgpid
is cleared by reapProcesses
.waitpid
code to reside in the handler and nowhere else.reapProcesses
is invoked on its behalf before
normal execution flow updates fgpid
. If that happens, the shell will spin forever and never advance up to the shell prompt. This is a race condition, and race conditions are no-nos.while (fgpid == pid) {;}
is also a no-no. This allows the shell to spin on the CPU even when it can't do any meaningful work.
simplesh
to yield the CPU and to only be considered for CPU time when there's a chance the foreground process has exited.