Principles of Computer Systems
Winter 2021
Stanford University
Computer Science Department
Instructors: Chris Gregg and
Nick Troccoli
https://comic.browserling.com/53
Creating processes and running other programs
Inter-process communication
Signals
Concurrency and Race Conditions
A signal is a way to notify a process that an event has occurred
The operating system sends many signals, but we can also send signals manually.
int kill(pid_t pid, int signum);
// same as kill(getpid(), signum)
int raise(int signum);
waitpid()
Waitpid can be used to wait on children to terminate or change state:
pid_t waitpid(pid_t pid, int *status, int options);
The default behavior is to wait for the specified child process to exit. options lets us customize this further (can combine these flags using | ):
We can have a function of our choice execute when a certain signal is received.
typedef void (*sighandler_t)(int);
...
sighandler_t signal(int signum, sighandler_t handler);
A signal can be received at any time, and a signal handler can execute at any time.
Key Idea: a signal handler is called if one or more signals of a type are sent.
Solution: signal handler should clean up as many children as possible, using WNOHANG, which means don't block. If there are children we would have waited on but aren't, returns 0. -1 typically means no children left.
static void reapChild(int sig) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
numDone++;
}
}
The sigprocmask function lets us temporarily block signals of the specified types. Instead, they will be delivered when the block is removed.
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
sigset_t is a special type (usually a 32-bit int) used as a bit vector. It must be created and initialized using special functions (we generally ignore the return values).
// Initialize to the empty set of signals
int sigemptyset(sigset_t *set);
// Set to contain all signals
int sigfillset(sigset_t *set);
// Add the specified signal
int sigaddset(sigset_t *set, int signum);
// Remove the specified signal
int sigdelset(sigset_t *set, int signum);
static void imposeSIGCHLDBlock() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
}
Concurrency means performing multiple actions at the same time.
fork
) and asynchronous signal handling (e.g. signal
), it's possible to have concurrency issues. These are tricky!// 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$
Symptom: it looks like jobs are being removed from the list before being added! How is this possible?
// 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;
}
Issue: the signal handler is being called before the parent adds to the job list.
Solution: block SIGCHLD from lines 14-17 to force the parent to always add to the job list first.
This is called a critical section - a piece of code that is indivisible. It cannot be interrupted midway by our other code.
block
unblock
// job-list-fixed.c
char * const kArguments[] = {"date", NULL};
int main(int argc, char *argv[]) {
signal(SIGCHLD, reapProcesses);
// Create set with just SIGCHLD
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;
}
Race conditions are a fundamental problem in concurrent code.
☐ Identify shared data that may be modified concurrently. What global variables are used in both the main code and signal handlers?
☐ Document and confirm an ordering of events that causes unexpected behavior. What assumptions are made in the code that can be broken by certain orderings?
☐ Use concurrency directives to force expected orderings. How can we use signal blocking and atomic operations to force the correct ordering(s)?
// simplesh
// The currently-running foreground command PID
static pid_t foregroundPID = 0;
static void waitForForegroundProcess(pid_t pid) {
fgpid = pid;
sigset_t empty;
sigemptyset(&empty);
while (fgpid == pid) {
sigsuspend(&empty);
}
}
static void executeCommand(char *command, bool inBackground) {
// ...(omitted for brevity)...
if (inBackground) {
printf("%d %s\n", pidOrZero, command);
} else {
waitForForegroundCommand(pidOrZero);
}
}
static void reapProcesses(int signum) {
while (true) {
pid_t result = waitpid(-1, NULL, WNOHANG);
if (result <= 0) break;
if (result == foregroundPID) foregroundPID = 0;
}
}
Deadlock is a program state in which no progress can be made - it is caused by code waiting for something that will never happen.
E.g. waitForForegroundProcess loops until foregroundPID is set to 0. But it never will be set to 0 in this case!
We could force this to happen if we put a sleep(1) as the first line of waitForForegroundProcess, and run a very short program in the foreground (e.g., date)
So, what we need to do is to use our signal blocking idea to ensure that the signal
handler does not get called until we are ready (much like in the job list example).
static pid_t fgpid = 0; // 0 means no foreground process
static void reapProcesses(int sig) {
pid_t pid;
while (true) {
pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
if (pid == fgpid) fgpid = 0;
}
}
static void waitForForegroundProcess(pid_t pid) {
fgpid = pid;
sigset_t empty;
sigemptyset(&empty);
while (fgpid == pid) {
sigsuspend(&empty);
}
unblockSIGCHLD();
}
int main(int argc, char *argv[]) {
signal(SIGCHLD, reapProcesses);
while (true) {
// ... (left out for brevity)
blockSIGCHLD();
pid_t pid = forkProcess();
if (pid == 0) {
unblockSIGCHLD();
execvp(argv[0], argv);
printf("%s: Command not found\n", argv[0]);
exit(0);
}
if (isbg) {
printf("%d %s\n", pid, command);
unblockSIGCHLD();
} else {
waitForForegroundProcess(pid);
}
}
printf("\n");
return 0;
}
static pid_t fgpid = 0; // 0 means no foreground process
static void reapProcesses(int sig) {
pid_t pid;
while (true) {
pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
if (pid == fgpid) fgpid = 0;
}
}
static void waitForForegroundProcess(pid_t pid) {
fgpid = pid;
sigset_t empty;
sigemptyset(&empty);
while (fgpid == pid) {
sigsuspend(&empty);
}
unblockSIGCHLD();
}
int main(int argc, char *argv[]) {
signal(SIGCHLD, reapProcesses);
while (true) {
// ... (left out for brevity)
blockSIGCHLD();
pid_t pid = forkProcess();
if (pid == 0) {
unblockSIGCHLD();
execvp(argv[0], argv);
printf("%s: Command not found\n", argv[0]);
exit(0);
}
if (isbg) {
printf("%d %s\n", pid, command);
unblockSIGCHLD();
} else {
waitForForegroundProcess(pid);
}
}
printf("\n");
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
static void bat(int unused) {
printf("pirate\n");
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, bat);
pid_t pid = fork();
if (pid == 0) {
printf("ghost\n");
return 0;
}
kill(pid, SIGUSR1);
printf("ninja\n"); return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
static void bat(int unused) {
printf("pirate\n");
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, bat);
pid_t pid = fork();
if (pid == 0) {
printf("ghost\n");
return 0;
}
kill(pid, SIGUSR1);
printf("ninja\n"); return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
>
of the counter
> 0
test is changed to a >=
, then counter
values of zeroes would be included in each possible output. How many different outputs are now possible? (No need to list the outputs—just present the number.)Consider this program and its execution. Assume that all processes run to completion, all system and printf
calls succeed, and that all calls to printf
are atomic. Assume nothing about scheduling or time slice durations.
int main(int argc, char *argv[]) {
pid_t pid;
int counter = 0;
while (counter < 2) {
pid = fork();
if (pid > 0) break;
counter++;
printf("%d", counter);
}
if (counter > 0) printf("%d", counter);
if (pid > 0) {
waitpid(pid, NULL, 0);
counter += 5;
printf("%d", counter);
}
return 0;
}
>
of the counter
> 0
test is changed to a >=
, then counter
values of zeroes would be included in each possible output. How many different outputs are now possible? (No need to list the outputs—just present the number.)Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way (e.g. fork never fails, and waitpid only returns -1 because there aren’t any child processes at the moment it decides on its return value).
static pid_t pid; // necessarily global so handler1 has
// access to it
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
Now further assume the call to exit(0) has also been removed from the handler2 function . Are there any other potential program outputs? If not, explain why. If so, what are they?
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way (e.g. fork never fails, and waitpid only returns -1 because there aren’t any child processes at the moment it decides on its return value).
counter = 1
counter = 10
counter = 1001
static pid_t pid; // necessarily global so handler1 has
// access to it
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way (e.g. fork never fails, and waitpid only returns -1 because there aren’t any child processes at the moment it decides on its return value).
static pid_t pid; // necessarily global so handler1 has
// access to it
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
So, another possible output would be:
counter = 1
counter = 1001
Consider the following program. Assume that each call to printf flushes its output to the console in full, and further assume that none of the system calls fail in any unpredictable way (e.g. fork never fails, and waitpid only returns -1 because there aren’t any child processes at the moment it decides on its return value).
static pid_t pid; // necessarily global so handler1 has
// access to it
static int counter = 0;
static void handler1(int unused) {
counter++;
printf("counter = %d\n", counter);
kill(pid, SIGUSR1);
}
static void handler2(int unused) {
counter += 10;
printf("counter = %d\n", counter);
exit(0);
}
int main(int argc, char *argv[]) {
signal(SIGUSR1, handler1);
if ((pid = fork()) == 0) {
signal(SIGUSR1, handler2);
kill(getppid(), SIGUSR1);
while (true) {}
}
if (waitpid(-1, NULL, 0) > 0) {
counter += 1000;
printf("counter = %d\n", counter);
}
return 0;
}
Next Time: Introduction to Threads