execvp
, Pipes, and Interprocess CommunicationPrinciples of Computer Systems
Spring 2019
Stanford University
Computer Science Department
Instructors: Chris Gregg and
Phil Levis
execvp
? An implementation mysystem
to emulate the behavior of the libc function called system
.
mysystem
function, which executes the supplied command
as if we typed it out in the terminal ourselves, ultimately returning once the surrogate command
has finished.command
exits normally (either via an exit
system call, or via a normal return statement from main
), then our mysystem
implementation should return that exact same exit value.SIGSEGV
).execvp
, Pipes, and Interprocess Communicationmysystem
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);
}
execvp
, Pipes, and Interprocess Communicationexecvp
, Pipes, and Interprocess Communicationfgets
is a somewhat overflow-safe variant on scanf
that knows to read everything up through and including the newline character.
mysystem
.static const size_t kMaxLine = 2048;
int main(int argc, char *argv[]) {
char command[kMaxLine];
while (true) {
printf("> ");
fgets(command, kMaxLine, stdin);
if (feof(stdin)) break;
command[strlen(command) - 1] = '\0'; // overwrite '\n'
printf("retcode = %d\n", mysystem(command));
}
printf("\n");
return 0;
}
mysystem
implementation is working as expected:execvp
, Pipes, and Interprocess Communicationmysystem
function is the first example I've provided where 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
and dup2
system calls, and how they can be used to introduce communication channels between the different processes.execvp
, Pipes, and Interprocess Communicationsimplesh
.
fork
, waitpid
, and execvp
I can think of: a miniature shell not unlike those you've been using since the day you first logged into a myth
machine.simplesh
operates as a read-eval-print loop—often called a repl—which itself responds to the many things we type in by forking off child processes.
simplesh
process.ls
, cp
, our own CS110 search
(which we wrote during our second lecture), or even emacs
.emacs &
—is an instruction to execute the new process in the background without forcing the shell to wait for it to finish.simplesh
is presented on the next slide. Where helper functions don't rely on CS110 concepts, I omit their implementations.execvp
, Pipes, and Interprocess CommunicationHere's the core implementation of simplesh (full implementation is right here, and you can run the code on the following slide):
execvp
, Pipes, and Interprocess Communicationint main(int argc, char *argv[]) {
while (true) {
char command[kMaxCommandLength + 1];
readCommand(command, kMaxCommandLength);
char *arguments[kMaxArgumentCount + 1];
int count = parseCommandLine(command, arguments, kMaxArgumentCount);
if (count == 0) continue;
if (strcmp(arguments[0], "quit") ==) break; // hardcoded builtin to exit shell
bool isbg = strcmp(arguments[count - 1], "&") == 0;
if (isbg) arguments[--count] = NULL; // overwrite "&"
pid_t pid = fork();
if (pid == 0) execvp(arguments[0], arguments);
if (isbg) { // background process, don't wait for child to finish
printf("%d %s\n", pid, command);
} else { // otherwise block until child process is complete
waitpid(pid, NULL, 0);
}
}
printf("\n");
return 0;
}
execvp
, Pipes, and Interprocess Communicationpipe
system call.
pipe
system call takes an uninitialized array of two integers—let’s call it fds
—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.
execvp
, Pipes, and Interprocess Communicationint pipe(int fds[]);
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):execvp
, Pipes, and Interprocess Communicationint 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;
}
execvp
, Pipes, and Interprocess Communicationpipe
and fork
work together in this example?
fds
is shared with the call to pipe
. pipe
allocates two descriptors, setting the first to draw from a resource and the second to publish to that same resource. pipe
then plants copies of those two descriptors into indices 0 and 1 of the supplied array before it returns.fork
call creates a child process, which itself inherits 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]
.execvp
, Pipes, and Interprocess Communicationpipe
and fork
work together in this example?
fds[0]
before it writes to anything to fds[1]
to emphasize the fact that the parent has no interest in reading anything from the pipe.fds[1]
before it reads from fds[0]
to emphasize the fact that the it has zero interest in publishing anything to the pipe. It's imperative all write endpoints of the pipe be closed if not being used, else the read end will never know if more text is to come or not.write
in the parent presses all six bytes of "hello"
('\0'
included) in a single call. Similarly, I assume the one call to read
pulls in those same six bytes into its local buffer
with just the one call.close
calls as I do in both the child and the parent before allowing them to exit.execvp
, Pipes, and Interprocess Communicationpipe
, 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.
command
to finish, subprocess
returns a subprocess_t
with the command
process’s pid
and a single descriptor called supplyfd
.supplyfd
field with the understanding that that same data can be ingested verbatim by the child's stdin
.execvp
, Pipes, and Interprocess Communicationtypedef struct {
pid_t pid;
int supplyfd;
} subprocess_t;
subprocess_t subprocess(const char *command);
subprocess
should work.
subprocess
works for us, we'll have an easier time understanding the details of its implementation.stdin
and publishes everything it reads to its stdout
in sorted order:execvp
, Pipes, and Interprocess Communicationint 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;
}
subprocess_t
running sort
and publishes eight fancy SAT words to supplyfd
, knowing those words flow through the pipe to the child's stdin
. supplyfd
down by passing it to close
. The reference count of the open file entry referenced by supplyfd
is demoted from 1 to 0 with that close
call. That sends an EOF to the process that tries in ingest data from the other end of the pipe.waitpid
call until the child exits. When the child exits, the parent assumes all of the words have been printed in sorted order to stdout
.execvp
, Pipes, and Interprocess Communicationcgregg@myth60$ ./subprocess
ablution
felicity
halcyon
indefatigable
pulchritude
somnolent
susurration
umbrage
cgregg@myth60$
subprocess
(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.execvp
, Pipes, and Interprocess Communicationsubprocess_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 represented internally by some number (e.g. 11). In fact, C #define
s SIGSEGV
to be the number 11.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
to the foreground process (and by default, that foreground is terminated).SIGTSTP
to the foreground process (and by default, the foreground process is halted until a subsequent SIGCONT
signal instructs it to continue).SIGPIPE
to the offending process. The default SIGPIPE
handler prints a message identifying the pipe error and terminates the program.SIGCHLD
signal to the process's parent.
waitpid
call.SIGCHLD
handler to be asynchronously invoked whenever a child process changes state.SIGCHLD
handlers almost always include calls to waitpid
, which can be used to surface the pids of child processes that've changed state. If the child process of interest actually terminated, either normally or abnormally, the waitpid
also culls the zombie the relevant child process has become.SIGCHLD
handler.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
, of course, 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$
SIGCHLD
signals are delivered while dad is off the processor, the operating system only records the fact that at one or more SIGCHLD
s came in.SIGCHLD
handler, it must do so on behalf of the one or more signals that may have been deliveredsince the last time it was on the processor.SIGCHLD
handler needs to call waitpid
in a loop, as with:static void reapChild(int unused) {
while (true) {
pid_t pid = waitpid(-1, NULL, 0);
if (pid < 0) break;
numDone++;
}
}
reapChild
implementation seemingly fixes the pentuplets
program, but it changes the behavior of the first five-children
program.
SIGCHLD
handler will call waitpid
once, and it will return the pid of the first child.SIGCHLD
handler will then loop around and call waitpid
a second time.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:static void reapChild(int unused) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break; // note the < is now a <=
numDone++;
}
}
SIGCHLD
handlers generally have this while
loop structure.
if (pid < 0)
test to if (pid <= 0)
.WNOHANG
being passed in as the third argument.waitpid
can include several flags bitwise-or'ed together.
WUNTRACED
informs waitpid
to block until some child process has either ended or been stopped.WCONTINUED
informs waitpid
to block until some child process has either ended or resumed from a stopped state.WUNTRACED | WCONTINUED | WNOHANG
asks that waitpid
return information about a child process that has changed state (i.e. exited, crashed, stopped, or continued) but to do so without blocking.