CS110: Principles of Computer Systems
Winter 2021-2022
Stanford University
Instructors: Nick Troccoli and Jerry Cain
Creating processes and running other programs
Inter-process communication and Pipes
Signals
Virtual Memory
assign3: implement multiprocessing programs like "trace" (to trace another program's behavior) and "farm" (parallelize tasks)
assign4: implement your own shell!
A signal is a way to notify a process that an event has occurred
Some common signals (some 30 types are supported on Linux systems):
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 insight: when a child changes state, the kernel sends a SIGCHLD signal to its parent.
static const size_t kNumChildren = 5;
static size_t numChildrenDonePlaying = 0;
static void reapChild(int sig) {
waitpid(-1, NULL, 0);
numChildrenDonePlaying++;
}
int main(int argc, char *argv[]) {
printf("Let my five children play while I take a nap.\n");
signal(SIGCHLD, reapChild);
for (size_t kid = 1; kid <= kNumChildren; kid++) {
if (fork() == 0) {
sleep(3 * kid); // sleep emulates "play" time
printf("Child #%zu tired... returns to parent.\n", kid);
return 0;
}
}
while (numChildrenDonePlaying < kNumChildren) {
printf("At least one child still playing, so parent nods off.\n");
snooze(5); // custom fn to sleep uninterrupted
printf("Parent wakes up! ");
}
printf("All children accounted for. Good job, parent!\n");
return 0;
}
static const size_t kNumChildren = 5;
static size_t numChildrenDonePlaying = 0;
static void reapChild(int sig) {
waitpid(-1, NULL, 0);
numChildrenDonePlaying++;
}
int main(int argc, char *argv[]) {
printf("Let my five children play while I take a nap.\n");
signal(SIGCHLD, reapChild);
for (size_t kid = 1; kid <= kNumChildren; kid++) {
if (fork() == 0) {
sleep(3); // sleep emulates "play" time
printf("Child #%zu tired... returns to parent.\n", kid);
return 0;
}
}
while (numChildrenDonePlaying < kNumChildren) {
printf("At least one child still playing, so parent nods off.\n");
snooze(5); // custom fn to sleep uninterrupted
printf("Parent wakes up! ");
}
printf("All children accounted for. Good job, parent!\n");
return 0;
}
What happens if all children sleep for the same amount of time? (E.g. change line 15 from sleep(3 * kid) to sleep(3)).
Problem: a signal handler is called if one or more signals are sent.
Solution: signal handler should clean up as many children as possible.
static void reapChild(int sig) {
waitpid(-1, NULL, 0);
numChildrenDonePlaying++;
}
Let's add a loop to reap as many children as possible.
static void reapChild(int sig) {
while (true) {
pid_t pid = waitpid(-1, NULL, 0);
if (pid < 0) break;
numChildrenDonePlaying++;
}
}
Let's add a loop to reap as many children as possible.
Problem: this may block if other children are taking longer! We only want to clean up children that are done now. Others will signal later. (DEMO)
Question: what does the waitpid loop do if one child terminates but other children are still running?
static void reapChild(int sig) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break;
numChildrenDonePlaying++;
}
}
Let's add a loop to reap as many children as possible.
Solution: use 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.
Note: the kernel blocks additional signals of that type while a signal handler is running (they are sent later).
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!Consider the following program, job-list-broken.c:
// 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;
}
Cause: there is a race condition with the signal handler. It is possible for the child to execute and terminate before the parent adds the job to the job list.
Therefore, the signal handler will be called to remove the job before the parent adds the job!
Signal handlers are difficult to use properly, and the consequences can be severe. Many regard signals to be one of the worst parts of Unix’s design.
This installment of Ghosts of Unix Past explains why asynchronous signal handling can be such a headache. Main point: can be executed at bad times (while the main execution flow is in the middle of a malloc call, or accessing a complex data structure).
We will designate times in our program where we stop doing other work and handle any pending signals.
We will designate times in our program where we stop doing other work and handle any pending signals.
sigwait()
sigwait() can be used to wait (block) on a signal to come in:
int sigwait(const sigset_t *set, int *sig);
Cannot wait on SIGKILL or SIGSTOP, nor synchronous signals like SIGSEGV or SIGFPE.
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);
// Create a set of SIGINT and SIGTSTP
sigset_t monitoredSignals;
sigemptyset(&monitoredSignals);
sigaddset(&monitoredSignals, SIGINT);
sigaddset(&monitoredSignals, SIGTSTP);
sigwait()
Here's a program that overrides the behavior for Ctl-z to print a message instead:
int main(int argc, char *argv[]) {
sigset_t monitoredSignals;
sigemptyset(&monitoredSignals);
sigaddset(&monitoredSignals, SIGTSTP);
printf("Just try to Ctl-z me!\n");
while (true) {
int delivered;
sigwait(&monitoredSignals, &delivered);
printf("\nReceived signal %d: %s\n", delivered, strsignal(delivered));
}
return 0;
}
sigwait()
Problem: what if the user hits Ctl-z before we reach line 9, or between sigwait calls? It won't be handled by our code!
int main(int argc, char *argv[]) {
sigset_t monitoredSignals;
sigemptyset(&monitoredSignals);
sigaddset(&monitoredSignals, SIGTSTP);
printf("Just try to Ctl-z me!\n");
while (true) {
int delivered;
sigwait(&monitoredSignals, &delivered);
printf("\nReceived signal %d: %s\n", delivered, strsignal(delivered));
}
return 0;
}
sigwait()
int main(int argc, char *argv[]) {
sigset_t monitoredSignals;
sigemptyset(&monitoredSignals);
sigaddset(&monitoredSignals, SIGTSTP);
sleep(2);
printf("Just try to Ctl-z me!\n");
while (true) {
int delivered;
sigwait(&monitoredSignals, &delivered);
printf("\nReceived signal %d: %s\n", delivered, strsignal(delivered));
sleep(2);
}
return 0;
}
Problem: what if the user hits Ctl-z before we reach line 9, or between sigwait calls? It won't be handled by our code!
We will designate times in our program where we stop doing other work and handle any pending signals.
The sigprocmask function lets us temporarily block signals of the specified types. Instead, they will be queued up and delivered when the block is removed.
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
Side note: forked children inherit blocked signals! We may wish to remove a block in the child.
Here's the same program from before, but blocking SIGTSTP as soon as possible:
int main(int argc, char *argv[]) {
sigset_t monitoredSignals;
sigemptyset(&monitoredSignals);
sigaddset(&monitoredSignals, SIGTSTP);
sigprocmask(SIG_BLOCK, &monitoredSignals, NULL);
printf("Just try to Ctl-z me!\n");
while (true) {
int delivered;
sigwait(&monitoredSignals, &delivered);
printf("\nReceived signal %d: %s\n", delivered, strsignal(delivered));
}
return 0;
}
Wait - if we call sigwait while signals are blocked, what happens?
Key insight: sigwait() doesn't care about blocked signals when it is called.
Next time: multiprocessing wrap-up and virtual memory