Principles of Computer Systems
Autumn 2019
Stanford University
Computer Science Department
Lecturers: Chris Gregg and
Philip Levis
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++;