Principles of Computer Systems
Winter 2020
Stanford University
Computer Science Department
Lecturers: Chris Gregg and
Nick Troccoli
mysystem spawns a child process to perform some task and waits for it to complete.execvp, because we know that if it returns at all, it returns a -1. If that happens, we need to handle the error and make sure the child process terminates, via an exposed exit(0) call.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(const char *command) {
pid_t pid = fork();
if (pid == 0) {
char *arguments[] = {"/bin/sh", "-c", (char *) command, NULL};
execvp(arguments[0], arguments);
printf("Failed to invoke /bin/sh to execute the supplied command.");
exit(0);
}
int status;
waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : -WTERMSIG(status);
}mysystem function is the first example of fork, execvp, and waitpid all work together to do something genuinely useful.
mysystem is operationally a miniature terminal.fork, waitpid, and execvp work in practice.stsh for Stanford shell—to imitate the functionality of the shell (csh, bash, zsh, tcsh, etc.) you've been using since you started using Unix.pipe system call, and how it creates a communication channels between two processes.dup2 system call, and how it allows a process to manipulate its file descriptor table.mysystem is just a simple read-eval loop: it relies on a real shell (sh) to parse arguments and do all of the other things shells do
stsh
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 is particularly useful for allowing parent processes to communicate with spawned child processes.
int pipe(int fds[]);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 work?
pipe works and how arbitrary data can be passed over from one process to a second, let's consider the following program (which you can find here, or run on the next slide):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;
}pipe and fork work together in this example?
pipe allocates two descriptors, one for reading and one for writingfork call creates a child process, which has a shallow copy of the parent's fds array.
fork call, anything printed to fds[1]is readable from the parent's fds[0]and the child's fds[0].fds[1].fds[0]before it writes to anything to fds[1]because it will never use it; close it doesn't linger around as long as the parent does
fds[1] before it reads from fds[0]
fds[0] and child never uses fds[1]
pipe, fork, dup2, execvp, close, and waitpid, we can implement the subprocess function, which relies on the following record definition and is implemented to the following prototype (full implementation of everything is right here):
subprocess executes the provided command (assumed to be a '\0'-terminated C string) by calling "/bin/sh -c <command>"as we did in our mysystem implementation.
subprocess returns a subprocess_t with the process’s pid and a descriptor called supplyfd.supplyfd from its standard input
typedef struct {
pid_t pid;
int supplyfd;
} subprocess_t;
subprocess_t subprocess(const char *command);stdin, sorts them, and writes the output to standard out:int main(int argc, char *argv[]) {
subprocess_t sp = subprocess("/usr/bin/sort");
const char *words[] = {
"felicity", "umbrage", "susurration", "halcyon",
"pulchritude", "ablution", "somnolent", "indefatigable"
};
for (size_t i = 0; i < sizeof(words)/sizeof(words[0]); i++) {
dprintf(sp.supplyfd, "%s\n", words[i]);
}
close(sp.supplyfd);
int status;
pid_t pid = waitpid(sp.pid, &status, 0);
return pid == sp.pid && WIFEXITED(status) ? WEXITSTATUS(status) : -127;
}int main(int argc, char *argv[]) {
subprocess_t sp = subprocess("/usr/bin/sort");
const char *words[] = {
"felicity", "umbrage", "susurration", "halcyon",
"pulchritude", "ablution", "somnolent", "indefatigable"
};
for (size_t i = 0; i < sizeof(words)/sizeof(words[0]); i++) {
dprintf(sp.supplyfd, "%s\n", words[i]);
}
close(sp.supplyfd);
int status;
pid_t pid = waitpid(sp.pid, &status, 0);
return pid == sp.pid && WIFEXITED(status) ? WEXITSTATUS(status) : -127;
}| stdin |
| stdout |
| stderr |
parent
| stdin |
| stdout |
| stderr |
child
terminal in
pipe read
terminal out
terminal err
supplyfd
pipe write
dup2(2): copy a file descriptor into another file descriptor, closing the replaced file descriptor's entry if needed
dup2(int oldfd, int newfd)dup2 to copy the pipe read file descriptor into child's standard inputsubprocess (error checking intentionally omitted for brevity):subprocess_t. That way, the parent knows where to publish text so it flows to the read end of the pipe, across the parent process/child process boundary. This is bonafide interprocess communication.dup2 to bind the read end of the pipe to its own standard input. Once the reassociation is complete, fds[0] can be closed.subprocess_t subprocess(const char *command) {
int fds[2];
pipe(fds);
subprocess_t process = { fork(), fds[1] };
if (process.pid == 0) {
close(fds[1]);
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
char *argv[] = {"/bin/sh", "-c", (char *) command, NULL};
execvp(argv[0], argv);
}
close(fds[0]);
return process;
}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++;
}
}user
stack
kernel
user
stack
kernel
signal
user
stack
kernel
kernel
stack
user
stack
kernel
kernel
stack
system call
user
stack
kernel
kernel
stack
system call
user CPU context
user
stack
kernel
kernel
stack
system call
user CPU context
user
stack
kernel
kernel
stack
system call
user CPU context
sysreturn
user
stack
kernel
kernel
stack
system call
user CPU context
sysreturn
signal
sysreturn
user
stack
kernel
kernel
stack
system call
user CPU context
user
stack
kernel
kernel
stack
system call
user CPU context
Text
counter_1++;
gettimeofday(&now, NULL);
counter_2++;