CS110: Principles of Computer Systems
Autumn 2021
Jerry Cain
PDF
Lecture 09: Pipes and Interprocess Communication II
int pipe(int fds[]);
- Last lecture was dedicated to the pipe system call, with a fleeting mention of dup2 toward the end. Here are the prototypes:
- The pipe call allocates two descriptors and places them in fds[0] and fds[1]. The descriptors are configured so that everything written to fds[1] is readable via fds[0]. Any text that's been written to fds[1] but not yet read through fds[0] accumulates in a virtual file managed by the OS.
- The dup2 call rewires the target descriptor so that it references the same file session resources—that is, the same open file table entry—referenced by source.
- Today's first example resides in the previous lecture deck, starting right here.
- Apparently they went with dup2 as a name because dup was already taken.
- dup allocates a new descriptor and configures it to reference the same file session that source references.
- The primary difference is that you choose the target descriptor number with dup2, whereas the OS chooses when you call dup.
- We'll use dup2 more often in CS110, though there are benefits to knowing of both.
int dup2(int source, int target);
int dup(int source);
Lecture 09: Pipes and Interprocess Communication II
- The subprocess example illustrates how an entire executable can be tapped to manage a sizeable fraction of a larger executable's overall goal.
- The relationship between the client of subprocess and the literal process created by calling it is very clearly a parent-child one.
- The Unix pipeline is supported by most shells—in fact, you'll see when you implement one for Assignment 4—by creating two or more sibling concurrent processes so they're chained together via their standard input and output streams.
- More specifically, the standard output of one process is forwarded on to and consumed as the standard input of the next process in the sequence.
- Some examples:
- We're going to focus on pipelines of length 2 right now, and we'll leave pipelines of arbitrary length to Assignment 4.
poohbear@myth51:~$ cat /usr/include/tar.h | wc
112 600 3786
poohbear@myth51:~$ echo -e "pear\ngrape\npeach\napricot\nbanana\napple" | sort | grep ap
apple
apricot
grape
poohbear@myth51:~$ time sleep 5 | sleep 10
real 0m10.004s
user 0m0.006s
sys 0m0.001s
poohbear@myth55:~$ curl -sL "http://cs110.stanford.edu/odyssey.txt" | sed 's/[^a-zA-Z ]/ /g' |
> tr 'A-Z ' 'a-z\n' | grep [a-z] | sort -u |
> comm -23 - <(sort /usr/share/dict/words) | less
poohbear@myth55:~$
Lecture 09: Pipes and Interprocess Communication II
- Let's implement a binary
pipeline
function that codes to the following interface:
-
pipeline
accepts two argument vectors and, assuming both vectors are valid, spawns off twin processes with the added bonus that the standard output of the first is directed to the standard input of the second.- For simplicity, we'll assume all
pipeline
calls are well-formed and work as expected.argv1
andargv2
are each valid,NULL
-terminated argument vectors, andpids
is the base address of an array of length two. - We'll also assume all calls to
pipe
,dup2
,close
,execvp
, and so forth succeed so that you needn't do any error checking whatsoever. -
pipeline
should return without waiting for either of the child processes to finish, and the pids of the two processes are dropped intopids[0]
andpids[1]
. - We'll be sure the two processes running in parallel, so that
takes about 10 seconds.
- For simplicity, we'll assume all
void pipeline(char *argv1[], char *argv2[], pid_t pids[]);
pid_t pids[2];
pipeline({"sleep", "10", NULL}, {"sleep", "10", NULL}, pids);
Lecture 09: Pipes and Interprocess Communication II
- Here's the implementation of my own pipeline function:
- The key difference between this function and prior ones is that two child processes are created—true twinsies—to run executables identified by argv1 and argv2.
- The fork and execvp work is more straightforward than the descriptor work. Note, however, that the child pids are dropped in the two slots of the supplied pids array.
void pipeline(char *argv1[], char *argv2[], pid_t pids[]) {
int fds[2];
pipe(fds);
pids[0] = fork();
if (pids[0] == 0) {
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
execvp(argv1[0], argv1);
}
close(fds[1]); // was only relevant to first child, so close before second fork
pids[1] = fork();
if (pids[1] == 0) {
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
execvp(argv2[0], argv2);
}
close(fds[0]);
}
Lecture 09: Pipes and Interprocess Communication II
- Here's the implementation of my own pipeline function:
- The pipe call must come before the fork calls, because the pipe endpoints need to exist at the time the child processes come into being. The ordering here is crucial, else the child processes won't inherit endpoint descriptors related to the pipe.
void pipeline(char *argv1[], char *argv2[], pid_t pids[]) {
int fds[2];
pipe(fds);
pids[0] = fork();
if (pids[0] == 0) {
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
execvp(argv1[0], argv1);
}
close(fds[1]); // was only relevant to first child, so close before second fork
pids[1] = fork();
if (pids[1] == 0) {
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
execvp(argv2[0], argv2);
}
close(fds[0]);
}
Lecture 09: Pipes and Interprocess Communication II
- Here's the implementation of my own pipeline function:
- The first child process inherits clones of the two pipe endpoints. The first child doesn't need fds[0] for anything meaningful, so it's closed right away. fds[1] is also closed, but only after dup2 uses it to rewire the child's standard output to reference the write end of the pipe.
void pipeline(char *argv1[], char *argv2[], pid_t pids[]) {
int fds[2];
pipe(fds);
pids[0] = fork();
if (pids[0] == 0) {
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
execvp(argv1[0], argv1);
}
close(fds[1]); // was only relevant to first child, so close before second fork
pids[1] = fork();
if (pids[1] == 0) {
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
execvp(argv2[0], argv2);
}
close(fds[0]);
}
Lecture 09: Pipes and Interprocess Communication II
- Here's the implementation of my own pipeline function:
- Once the first child has been spawned, pipeline can close fds[1], since it's of no importance to the second child and doesn't need to be open for the second fork call.
void pipeline(char *argv1[], char *argv2[], pid_t pids[]) {
int fds[2];
pipe(fds);
pids[0] = fork();
if (pids[0] == 0) {
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
execvp(argv1[0], argv1);
}
close(fds[1]); // was only relevant to first child, so close before second fork
pids[1] = fork();
if (pids[1] == 0) {
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
execvp(argv2[0], argv2);
}
close(fds[0]);
}
Lecture 09: Pipes and Interprocess Communication II
- Here's the implementation of my own pipeline function:
- The second child's standard input is rewired to reference the read end of the pipe. Once dup2 and fds[0] have been used to finagle that rewiring, fds[0] can be closed. Note that fds[1] wasn't even inherited across the fork boundary.
void pipeline(char *argv1[], char *argv2[], pid_t pids[]) {
int fds[2];
pipe(fds);
pids[0] = fork();
if (pids[0] == 0) {
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
execvp(argv1[0], argv1);
}
close(fds[1]); // was only relevant to first child, so close before second fork
pids[1] = fork();
if (pids[1] == 0) {
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
execvp(argv2[0], argv2);
}
close(fds[0]);
}
Lecture 09: Pipes and Interprocess Communication II
- Here's the implementation of my own pipeline function:
- Finally, we close fds[0] in the primary process.
- Note that there were a total of five close calls across three processes. This is the cost of creating a pipe ahead of multiple fork calls. There's simply no other programmatic way to enable unidirectional communication channels between sibling processes unless this type of thing is done.
void pipeline(char *argv1[], char *argv2[], pid_t pids[]) {
int fds[2];
pipe(fds);
pids[0] = fork();
if (pids[0] == 0) {
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
execvp(argv1[0], argv1);
}
close(fds[1]); // was only relevant to first child, so close before second fork
pids[1] = fork();
if (pids[1] == 0) {
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
execvp(argv2[0], argv2);
}
close(fds[0]);
}
Lecture 09: Pipes and Interprocess Communication II
- The cascade of close calls seen in the last example is fairly common when coding with multiple processes. In particular, the child processes generally need to close all pipe endpoints they've inherited ahead of their execvp calls.
- There's a second version of pipe called pipe2 with the following prototype:
- A call to pipe2(fds, 0) is functionally equivalent to pipe(fds). A call to pipe2(fds, O_CLOEXEC), however, populates the supplied fds array with pipe endpoint descriptors that automatically close when the surrounding processes calls execvp.
- Presented on the right is
an arguably better version of
our pipeline function, since
this version allows to omit
the close calls in the child
processes. That's permitted,
because the execvp system
call automatically closes any
descriptors that have been
marked as O_CLOEXEC.
void pipeline(char *argv1[], char *argv2[], pid_t pids[]) {
int fds[2];
pipe2(fds, O_CLOEXEC);
pids[0] = fork();
if (pids[0] == 0) {
dup2(fds[1], STDOUT_FILENO);
execvp(argv1[0], argv1);
}
close(fds[1]);
pids[1] = fork();
if (pids[1] == 0) {
dup2(fds[0], STDIN_FILENO);
execvp(argv2[0], argv2);
}
close(fds[0]);
}
int pipe2(int fds[], int flags);
Lecture 09: Preamble to Signals
- Processes start, cycle on/off the CPU, and eventually terminate, but they can also be paused at arbitrary points by what are called job control signals.
- Maybe you’re running a long, CPU-intensive program, and you want to pause it so you can quickly run some much shorter CPU-intensive program without competition.
- Perhaps MacOS will send "pause" signals to programs when it starts running out of physical memory, prompting you to close some apps before resuming them.
- Job control is sometimes used programmatically to synchronize between processes; for example, process A might halt itself to wait for process B to catch up, and then process B will signal process A to continue when it’s ready.
Lecture 09: Preamble to Signals
- We’ll talk more about signals on Monday, so don’t worry much about the details of how this works. Just know you can send a particular signal called SIGSTOP to pause a process and another signal called SIGCONT to continue it.
-
waitpid can be used to observe when a program changes job control states (i.e. exits, crashes, stops, or continues). This is managed via that third flags parameter that, until now, we've always just set to 0.
- Passing WUNTRACED as the third argument to waitpid tells that waitpid to return when a process in the supplied wait set either stops or exits.
-
Passing WCONTINUED as the third argument to waitpid tells that waitpid to return when a process in the supplied wait set either continues or exits.
- Passing in WUNTRACED | WCONTINUED —yes, that's a bitwise or—tells waitpid to return because of any state change whatsoever.
- Passing WUNTRACED as the third argument to waitpid tells that waitpid to return when a process in the supplied wait set either stops or exits.
From the command line
# Pause PID 1234
myth61$ kill -STOP 1234
# Resume PID 1234
myth61$ kill -CONT 1234
Programmatically
// Pause PID 1234
kill(1234, SIGSTOP);
// Resume PID 1234
kill(1234, SIGCONT);
pid_t pid = waitpid(-1, &status, WUNTRACED);
pid_t pid = waitpid(-1, &status, WCONTINUED);
pid_t pid = waitpid(-1, &status, WUNTRACED | WCONTINUED);
Lecture 09: Pipes and Interprocess Communication
By Jerry Cain
Lecture 09: Pipes and Interprocess Communication
- 1,433