Principles of Computer Systems
Winter 2020
Stanford University
Computer Science Department
Instructors: Chris Gregg and
Nick Troccoli
Creating processes and running other programs
Inter-process communication
Signals
Race Conditions
fork()
pid_t pidOrZero = fork();
// both parent and child run code here onwards
printf("This is printed by two processes.\n");
waitpid()
A function that a parent can call to wait for its child to exit:
pid_t waitpid(pid_t pid, int *status, int options);
The function returns when the specified child process exits.
execvp
is a function that lets us run another program in the current process.
It runs the specified program executable, completely cannibalizing the current process.
path
identifies the name of the executable to be invoked.argv
is the argument vector that should be passed to the new executable's main
function.path
and argv[0]
end up being the same exact string.execvp
fails to cannibalize the process and install a new executable image within it, it returns -1 to express failure.execvp
succeeds, it never returns in the calling process. execvp
has many variants (execle
, execlp
, and so forth. Type man
execvp
to see all of them). We generally rely on execvp
in this course.int execvp(const char *path, char *argv[]);
What does the following code output, assuming execvp executes successfully?
int main(int argc, char *argv[]) {
char *args[] = {"/bin/ls", "-l", "/usr/class/cs110", NULL};
execvp(args[0], args);
printf("Hello world!\n");
return 0;
}
mysystem is our own version of the built-in function system.
static int mysystem(char *command) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
// If we are the child, execute the shell command
char *arguments[] = {"/bin/sh", "-c", command, NULL};
execvp(arguments[0], arguments);
// If the child gets here, there was an error
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
// If we are the parent, wait for the child
int status;
waitpid(pidOrZero, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -WTERMSIG(status);
}
}
static int mysystem(char *command) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
// If we are the child, execute the shell command
char *arguments[] = {"/bin/sh", "-c", command, NULL};
execvp(arguments[0], arguments);
// If the child gets here, there was an error
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
// If we are the parent, wait for the child
int status;
waitpid(pidOrZero, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -WTERMSIG(status);
}
}
Line 2: First, fork off a child process.
static int mysystem(char *command) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
// If we are the child, execute the shell command
char *arguments[] = {"/bin/sh", "-c", command, NULL};
execvp(arguments[0], arguments);
// If the child gets here, there was an error
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
// If we are the parent, wait for the child
int status;
waitpid(pidOrZero, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -WTERMSIG(status);
}
}
Lines 4-6: In the child, execute the /bin/sh program, which can execute any shell command.
static int mysystem(char *command) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
// If we are the child, execute the shell command
char *arguments[] = {"/bin/sh", "-c", command, NULL};
execvp(arguments[0], arguments);
// If the child gets here, there was an error
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
// If we are the parent, wait for the child
int status;
waitpid(pidOrZero, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -WTERMSIG(status);
}
}
Line 8: The child will only get to this line if execvp fails.
static int mysystem(char *command) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
// If we are the child, execute the shell command
char *arguments[] = {"/bin/sh", "-c", command, NULL};
execvp(arguments[0], arguments);
// If the child gets here, there was an error
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
// If we are the parent, wait for the child
int status;
waitpid(pidOrZero, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -WTERMSIG(status);
}
}
Lines 11-13: In the parent, wait for the child to terminate.
static int mysystem(char *command) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
// If we are the child, execute the shell command
char *arguments[] = {"/bin/sh", "-c", command, NULL};
execvp(arguments[0], arguments);
// If the child gets here, there was an error
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
// If we are the parent, wait for the child
int status;
waitpid(pidOrZero, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -WTERMSIG(status);
}
}
Lines 14-18: In the parent, after the child terminates, return its status.
int main(int argc, char *argv[]) {
char command[kMaxLineLength];
while (true) {
printf("> ");
fgets(command, sizeof(command), stdin);
// If the user entered Ctl-d, stop
if (feof(stdin)) {
break;
}
// Remove the \n that fgets puts at the end
command[strlen(command) - 1] = '\0';
int commandReturnCode = mysystem(command);
printf("return code = %d\n", commandReturnCode);
}
printf("\n");
return 0;
}
Our first-shell program is a loop in main that parses the user input and passes it to mysystem.
fork
, execvp
, and waitpid
to do this!stsh
("Stanford shell") with much of the functionality of real Unix shells.Shells have a variety of supported commands:
static void executeCommand(char *command, bool inBackground) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
// If we are the child, execute the shell command
char *arguments[] = {"/bin/sh", "-c", command, NULL};
execvp(arguments[0], arguments);
// If the child gets here, there was an error
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
// If we are the parent, either wait or return immediately
if (inBackground) {
printf("%d %s\n", pidOrZero, command);
} else {
waitpid(pidOrZero, NULL, 0);
}
}
static void executeCommand(char *command, bool inBackground) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
// If we are the child, execute the shell command
char *arguments[] = {"/bin/sh", "-c", command, NULL};
execvp(arguments[0], arguments);
// If the child gets here, there was an error
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
// If we are the parent, either wait or return immediately
if (inBackground) {
printf("%d %s\n", pidOrZero, command);
} else {
waitpid(pidOrZero, NULL, 0);
}
}
Line 1: Now, the caller can optionally run the command in the background.
static void executeCommand(char *command, bool inBackground) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
// If we are the child, execute the shell command
char *arguments[] = {"/bin/sh", "-c", command, NULL};
execvp(arguments[0], arguments);
// If the child gets here, there was an error
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
// If we are the parent, either wait or return immediately
if (inBackground) {
printf("%d %s\n", pidOrZero, command);
} else {
waitpid(pidOrZero, NULL, 0);
}
}
Lines 11-16: The parent waits on a foreground child, but not a background child.
int main(int argc, char *argv[]) {
char command[kMaxLineLength];
while (true) {
printf("> ");
fgets(command, sizeof(command), stdin);
// If the user entered Ctl-d, stop
if (feof(stdin)) {
break;
}
// Remove the \n that fgets puts at the end
command[strlen(command) - 1] = '\0';
if (strcmp(command, "quit") == 0) break;
bool isbg = command[strlen(command) - 1] == '&';
if (isbg) {
command[strlen(command) - 1] = '\0';
}
executeCommand(command, isbg);
}
printf("\n");
return 0;
}
In main, on lines 15-22, we check for the "quit" command, and also for whether to run the command in the background.
Now we can answer the following questions:
pipe
system call takes an uninitialized array of two integers and populates it with two file descriptors such that everything written to fds[1]
can be read from fds[0]
.pipe
can allow parent processes to communicate with spawned child processes.
int pipe(int fds[]);
Here's an example program showing how pipe works (which you can find here):
int main(int argc, char *argv[]) {
int fds[2];
pipe(fds);
pid_t pid = fork();
if (pid == 0) {
close(fds[1]);
char buffer[6];
read(fds[0], buffer, sizeof(buffer));
printf("Read from pipe bridging processes: %s.\n", buffer);
close(fds[0]);
return 0;
}
close(fds[0]);
write(fds[1], "hello", 6);
waitpid(pid, NULL, 0);
close(fds[1]);
return 0;
}
Here's an example program showing how pipe works (which you can find here):
int main(int argc, char *argv[]) {
int fds[2];
pipe(fds);
pid_t pid = fork();
if (pid == 0) {
close(fds[1]);
char buffer[6];
read(fds[0], buffer, sizeof(buffer));
printf("Read from pipe bridging processes: %s.\n", buffer);
close(fds[0]);
return 0;
}
close(fds[0]);
write(fds[1], "hello", 6);
waitpid(pid, NULL, 0);
close(fds[1]);
return 0;
}
Lines 2-3: We ask the operating system to create a pipe for us. This gives us two file descriptors, one for reading and one for writing.
Tip: you learn to read before you learn to write (read = fds[0], write = fds[1]).
Here's an example program showing how pipe works (which you can find here):
int main(int argc, char *argv[]) {
int fds[2];
pipe(fds);
pid_t pid = fork();
if (pid == 0) {
close(fds[1]);
char buffer[6];
read(fds[0], buffer, sizeof(buffer));
printf("Read from pipe bridging processes: %s.\n", buffer);
close(fds[0]);
return 0;
}
close(fds[0]);
write(fds[1], "hello", 6);
waitpid(pid, NULL, 0);
close(fds[1]);
return 0;
}
Lines 13-14: after forking, in the parent we close the reader FD, and write to the writer FD to send a message to the child.
Here's an example program showing how pipe works (which you can find here):
int main(int argc, char *argv[]) {
int fds[2];
pipe(fds);
pid_t pid = fork();
if (pid == 0) {
close(fds[1]);
char buffer[6];
read(fds[0], buffer, sizeof(buffer));
printf("Read from pipe bridging processes: %s.\n", buffer);
close(fds[0]);
return 0;
}
close(fds[0]);
write(fds[1], "hello", 6);
waitpid(pid, NULL, 0);
close(fds[1]);
return 0;
}
Lines 15-16: after sending a message, we wait for the child to receive it and terminate. Then we clean it up and close the writer FD.
Here's an example program showing how pipe works (which you can find here):
int main(int argc, char *argv[]) {
int fds[2];
pipe(fds);
pid_t pid = fork();
if (pid == 0) {
close(fds[1]);
char buffer[6];
read(fds[0], buffer, sizeof(buffer));
printf("Read from pipe bridging processes: %s.\n", buffer);
close(fds[0]);
return 0;
}
close(fds[0]);
write(fds[1], "hello", 6);
waitpid(pid, NULL, 0);
close(fds[1]);
return 0;
}
Line 4: when we fork, the child gets an identical copy of the parent's file descriptor table. This means its file descriptor table entries point to the same open file table entries.
This means the open file table entries for the two pipe FDs both have reference counts of 2.
Here's an example program showing how pipe works (which you can find here):
int main(int argc, char *argv[]) {
int fds[2];
pipe(fds);
pid_t pid = fork();
if (pid == 0) {
close(fds[1]);
char buffer[6];
read(fds[0], buffer, sizeof(buffer));
printf("Read from pipe bridging processes: %s.\n", buffer);
close(fds[0]);
return 0;
}
close(fds[0]);
write(fds[1], "hello", 6);
waitpid(pid, NULL, 0);
close(fds[1]);
return 0;
}
Lines 6-8: after forking, the child closes the writer FD and reads 6 bytes from the reader FD.
Here's an example program showing how pipe works (which you can find here):
int main(int argc, char *argv[]) {
int fds[2];
pipe(fds);
pid_t pid = fork();
if (pid == 0) {
close(fds[1]);
char buffer[6];
read(fds[0], buffer, sizeof(buffer));
printf("Read from pipe bridging processes: %s.\n", buffer);
close(fds[0]);
return 0;
}
close(fds[0]);
write(fds[1], "hello", 6);
waitpid(pid, NULL, 0);
close(fds[1]);
return 0;
}
Lines 9-11: after reading data, it prints it to the screen, closes the reader FD, and terminates.
This method of communication between processes relies on the fact that file descriptors are duplicated when forking.
This is how a shell can support piping between processes (e.g. cat file.txt | uniq | sort):
cat
uniq
sort
terminal in
terminal out
pipe1
pipe2
Process | stdin | stdout |
---|---|---|
cat | terminal | pipe1[1] |
uniq | pipe1[0] | pipe2[1] |
sort | pipe2[0] | terminal |
int pipe1[2]; int pipe2[2]; pipe(pipe1); pipe(pipe2);
pipe
, fork
, dup2
, execvp
, close
, and waitpid
, we can implement the subprocess
function, which spawns a child process that we can communicate with (full implementation of everything is right here).stdin |
stdout |
stderr |
parent
stdin |
stdout |
stderr |
child
All processes are configured with FDs 0-2 for STDIN, STDOUT and STDERR, respectively.
stdin |
stdout |
stderr |
parent
stdin |
stdout |
stderr |
child
terminal in
pipe read
terminal out
terminal err
supplyfd
pipe write
dup2 lets us make a copy of a file descriptor entry and put it in another file descriptor index. If the second parameter is an already-open file descriptor, it is closed before being used. We can use dup2
to copy the pipe read file descriptor into child's standard input!
dup2(fds[0], STDIN_FILENO);
NULL
pointer.SIGSEGV
, informally known as a segmentation fault (or a SEGmentation Violation, or SIGSEGV
, for short).SIGSEGV
terminates the program and generates a core dump.SIGSEGV
is 11).SIGFPE
: whenever a process commits an integer-divide-by-zero (and, in some cases, a floating-point divide by zero on older architectures), the kernel hollers and issues a SIGFPE
signal to the offending process. By default, the program handles the SIGFPE
by printing an error message announcing the zero denominator and generating a core dump.SIGINT
: when you type ctrl-c, the kernel sends a SIGINT
to the foreground process group. The default handler terminates the process group.SIGTSTP
: when you type ctrl-z, the kernel issues a SIGTSTP
to the foreground process group. The foreground process group is halted until a SIGCONT
signal.SIGPIPE
: when a process attempts to write data to a pipe after the read end has closed, the kernel delivers a SIGPIPE
. The default SIGPIPE
handler prints a message identifying the pipe error and terminates the program.$ grep error file.txt > errors.txt & [1] 4287 $ [1]+ Done grep error file.txt > errors.txt
SIGCHLD
signal to the process's parent.
SIGCHLD
handlers can call waitpid
, which tells them the pids of child processes that gave changed state. If the child process terminated, either normally or abnormally, the waitpid
also cleans up/frees the child.SIGCHLD
handler.wait
)static const size_t kNumChildren = 5;
static size_t numDone = 0;
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 <= 5; kid++) {
if (fork() == 0) {
sleep(3 * kid); // sleep emulates "play" time
printf("Child #%zu tired... returns to dad.\n", kid);
return 0;
}
}
reapChild
, handles each of the SIGCHLD
signals delivered as each child process exits.signal
prototype doesn't allow for state to be shared via parameters, so we have no choice but to use global variables. // code below is a continuation of that presented on the previous slide
while (numDone < kNumChildren) {
printf("At least one child still playing, so dad nods off.\n");
sleep(5);
printf("Dad wakes up! ");
}
printf("All children accounted for. Good job, dad!\n");
return 0;
}
static void reapChild(int unused) {
waitpid(-1, NULL, 0);
numDone++;
}
SIGCHLD
handler is invoked 5 times, each in response to some child process finishing up.cgregg@myth60$ ./five-children
Let my five children play while I take a nap.
At least one child still playing, so dad nods off.
Child #1 tired... returns to dad.
Dad wakes up! At least one child still playing, so dad nods off.
Child #2 tired... returns to dad.
Child #3 tired... returns to dad.
Dad wakes up! At least one child still playing, so dad nods off.
Child #4 tired... returns to dad.
Child #5 tired... returns to dad.
Dad wakes up! All children accounted for. Good job, dad!
cgregg@myth60$
sleep(3 * kid)
is now sleep(3)
so all five children flashmob dad when they're all done.
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$
waitpid
repeatedly fixes the problem, but it changes the behavior of the program.
waitpid
can prevent dad from returning to his nap. For real programs, this means they can't continue to do work (e.g., respond to shell commands.waitpid
to only reap children that have exited but to return without blocking, even if there are more children still running. We use WNOHANG
for this, as with:
waitpid
with WNOHANG
in the main loop?
waitpid
is called promptly, not determined by main loop.static void reapChild(int unused) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break; // note the < is now a <=
numDone++;
}
}
Next time: more signals