CS110: Principles of Computer Systems
Winter 2021-2022
Stanford University
Instructors: Nick Troccoli and Jerry Cain
Illustration courtesy of Roz Cyrus.
Creating processes and running other programs
Inter-process communication
Signals
Race Conditions
assign3: implement multiprocessing programs like "trace" (to trace another program's behavior) and "farm" (parallelize tasks)
assign4: implement your own shell!
fork()
pid_t pidOrZero = fork();
// both parent and child run code here onwards
printf("This is printed by two processes.\n");
How can the parent and child use the same address to store different data?
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);
waitpid()
int main(int argc, char *argv[]) {
printf("Before.\n");
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
sleep(2);
printf("I (the child) slept and the parent still waited up for me.\n");
} else {
pid_t result = waitpid(pidOrZero, NULL, 0);
printf("I (the parent) finished waiting for the child. This always prints last.\n");
}
return 0;
}
$ ./waitpid
Before.
I (the child) slept and the parent still waited up for me.
I (the parent) finished waiting for the child. This always prints last.
$
waitpid()
The output will be the same every time! The parent will always wait for the child to finish before continuing.
int main(int argc, char *argv[]) {
pid_t pid = fork();
if (pid == 0) {
printf("I'm the child, and the parent will wait up for me.\n");
return 110; // contrived exit status (not a bad number, though)
} else {
int status;
int result = waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child exited with status %d.\n", WEXITSTATUS(status));
} else {
printf("Child terminated abnormally.\n");
}
return 0;
}
}
Pass in the address of an integer as the second parameter to get the child's status.
$ ./separate
I am the child, and the parent will wait up for me.
Child exited with status 110.
$
A parent process should always wait on its children processes.
waitpid()
(Wait, what?)
What if we want to wait for children in the order in which they were created?
Check out the abbreviated program below (link to full program at the bottom):
int main(int argc, char *argv[]) {
pid_t children[kNumChildren];
for (size_t i = 0; i < kNumChildren; i++) {
children[i] = fork();
if (children[i] == 0) return 110 + i;
}
for (size_t i = 0; i < kNumChildren; i++) {
int status;
pid_t pid = waitpid(children[i], &status, 0);
assert(WIFEXITED(status));
printf("Child with pid %d accounted for (return status of %d).\n", children[i], WEXITSTATUS(status));
}
return 0;
}
$ ./reap-in-fork-order
Child with pid 12649 accounted for (return status of 110).
Child with pid 12650 accounted for (return status of 111).
Child with pid 12651 accounted for (return status of 112).
Child with pid 12652 accounted for (return status of 113).
Child with pid 12653 accounted for (return status of 114).
Child with pid 12654 accounted for (return status of 115).
Child with pid 12655 accounted for (return status of 116).
Child with pid 12656 accounted for (return status of 117).
$
A parent can call fork
multiple times, but must reap all the child processes.
Demo: Let's see how we might use this.
The most common use for fork is not to spawn multiple processes to split up work, but instead to run a completely separate program under your control and communicate with it.
execvp()
execvp
is a function that lets us run another program in the current process.
int execvp(const char *path, char *argv[]);
execvp()
It runs the executable at the specified path, completely cannibalizing the current process.
To run another executable, we must specify the (NULL-terminated) arguments to be passed into its main function, via the argv parameter.
execvp
has many variants (execle
, execlp
, and so forth. Type man
execvp
for more). We rely on execvp
in CS110.
int main(int argc, char *argv[]) {
char *args[] = {"/bin/ls", "-l", "/usr/class/cs110/lecture-examples", NULL};
execvp(args[0], args);
printf("This only prints if an error occurred.\n");
return 0;
}
$ ./execvp-demo
total 26
drwx------ 2 troccoli operator 2048 Jan 11 21:03 cpp-primer
drwx------ 3 troccoli operator 2048 Jan 15 12:43 cs107review
drwx------ 2 troccoli operator 2048 Jan 13 14:15 filesystems
drwx------ 2 troccoli operator 2048 Jan 13 14:14 lambda
drwxr-xr-x 3 poohbear root 2048 Nov 19 13:24 map-reduce
drwx------ 2 poohbear root 4096 Nov 19 13:25 networking
drwxr-xr-x 2 poohbear root 6144 Jan 22 08:58 processes
drwxr-xr-x 2 poohbear root 2048 Oct 29 06:57 threads-c
drwxr-xr-x 2 poohbear root 4096 Oct 29 06:57 threads-cpp
$
execvp()
execvp
is a function that lets us run another program in the current process.
int execvp(const char *path, char *argv[]);
A shell is essentially a program that repeats asking the user for a command and running that command (Demo: first-shell-soln.c)
system()
The built-in system function can execute a given shell command.
int system(const char *command);
int main(int argc, char *argv[]) {
int status = system(argv[1]);
printf("system returned %d\n", status);
return 0;
}
$ ./system-demo "ls -l"
total 26
drwx------ 2 troccoli operator 2048 Jan 11 21:03 cpp-primer
drwx------ 3 troccoli operator 2048 Jan 15 12:43 cs107review
drwx------ 2 troccoli operator 2048 Jan 13 14:15 filesystems
drwx------ 2 troccoli operator 2048 Jan 13 14:14 lambda
drwxr-xr-x 3 poohbear root 2048 Nov 19 13:24 map-reduce
drwx------ 2 poohbear root 4096 Nov 19 13:25 networking
drwxr-xr-x 2 poohbear root 6144 Jan 21 19:38 processes
drwxr-xr-x 2 poohbear root 2048 Oct 29 06:57 threads-c
drwxr-xr-x 2 poohbear root 4096 Oct 29 06:57 threads-cpp
system returned 0
$
mysystem()
We can implement our own version of system with fork(), waitpid() and execvp()!
int mysystem(const char *command);
One twist; not all shell commands are executable programs, and some need parsing.
execvp
inside parent and forgo the child process altogether? Becauseexecvp
would consume the calling process, and that's not what we want.static int mysystem(char *command) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
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);
return WIFEXITED(status) ? WEXITSTATUS(status) : -WTERMSIG(status);
}
Here's the implementation, with minimal error checking (the full version is linked at the bottom):
mysystem()
first-shell
Takeaways
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:
Let's make an updated version of mysystem called executeCommand.
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, we add two additional things:
Note that a background child isn't reaped! This is a problem - one we'll learn how to fix soon.
Next time: interprocess communication
What if we want to spawn a single child and wait for that child before spawning another child?
static const int kNumChildren = 8;
int main(int argc, char *argv[]) {
for (size_t i = 0; i < kNumChildren; i++) {
pid_t pidOrZero = fork();
if (pidOrZero == 0) {
printf("Hello from child %d!\n", getpid());
return 110 + i;
}
int status;
pid_t pid = waitpid(pidOrZero, &status, 0);
if (WIFEXITED(status)) {
printf("Child with pid %d exited normally with status %d\n", pid, WEXITSTATUS(status));
} else {
printf("Child with pid %d exited abnormally\n", pid);
}
}
return 0;
}
Check out the abbreviated program below (link to full program at bottom):