Sections Thu Oct 20 to Fri Oct 21
Solutions
1. Subprocess, Take 2
// Conditionally create pipes based on what I/O we need to redirect
int pipes[4] = { kNotInUse, kNotInUse, kNotInUse, kNotInUse };
if (supplyChildInput) pipe(pipes);
if (ingestChildOutput) pipe(pipes + 2);
subprocess_t returnValue = { fork(), pipes[1], pipes[2] };
// If we're the child, rewire our FDs if necessary
if (returnValue.pid == 0) {
// Connect first pipe read end to STDIN
if (supplyChildInput) dup2(pipes[0], STDIN_FILENO);
// Connect second pipe write end to STDOUT
if (ingestChildOutput) dup2(pipes[3], STDOUT_FILENO);
// close all valid file descriptors
for (int i = 0; i < sizeof(pipes) / sizeof(pipes[0]); i++) {
if (pipes[i] != kNotInUse) close(pipes[i]);
}
execvp(argv[0], argv);
cerr << argv[0] << ": Command not found" << endl;
exit(1);
}
if (supplyChildInput) close(pipes[0]);
if (ingestChildOutput) close(pipes[3]);
return returnValue;
Note: if you fail to close the write endpoints in the parent and child processes, the descriptors monitoring the read end of those same pipes (potentially the child’s descriptor 0) never sense an end of file. If standard input of the child process is depending on end of file (i.e. reading as much as possible until it reaches the end of input) and never gets it, the child process will stall forever!
2. Timeout
pid_t timed = fork();
if (timed == 0) {
// This child will run the specified command
execvp(argv[2], argv + 2);
cerr << argv[2] << ": Command not found" << endl;
exit(1);
}
pid_t timer = fork();
if (timer == 0) {
// This child will sleep for n seconds
sleep(atoi(argv[1]));
return 0;
}
// Wait for the first child to finish
int status;
pid_t winner = waitpid(-1, &status, 0);
pid_t runnerUp;
if (winner == timed) runnerUp = timer;
else runnerUp = timed;
// Terminate the runnerup process and clean it up
kill(runnerUp, SIGKILL);
waitpid(runnerUp, NULL, 0);
// Return child exit status if the command finished, else 124
if (winner == timed) {
return WEXITSTATUS(status);
} else {
return 124;
}
3. Incorrect Redirection
What text is actually printed to standard output?
Publishing date and time to file named "one".
What do each of the four files contain?
The contents of one, two and three all contain two lines, one line saying "Publishing date and time to file named X" (where X is the file after it in the list) and another line with the current date and time, and they could be in any order. The contents of four is just one line with the date and time.
The reason for this issue is that we call dup2
in the parent, which redirects its output to files. The first time the function is called, the parent's STDOUT is the terminal, so it prints the first message there. But then it changes its STDOUT to be file "one". So the next time publish
is called, the message "Publishing date and time to file named two" is written to file "one"! Then the dup2
call changes the parent's STDOUT to be file "two". So the next time publish
is called, the message "Publishing date and time to file named three" is written to file "two". This pattern continues for file "three", and file "four" has just the date written to it because there is no subsequent call to publish
after that to print the "Publishing ...." message to file "four".
What we wanted was to call dup2
only in the child process to redirect its standard output, but the parent's stdout should remain the terminal - otherwise it can no longer print, and will be sending output meant for the terminal to files!
How should the program be rewritten so that it works as intended?
Because the child processes (and only the child processes) should be redirecting, you should open
, dup2
, and close
in child-specific code. A happy side effect of the change is that you never mess with STDOUT_FILENO
in the parent if you confine the redirection code to the child.
static void publish(const char *name) {
printf("Publishing date and time to file named \"%s\".\n", name);
if (fork() > 0) return;
int outfile = open(name, O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(outfile, STDOUT_FILENO);
close(outfile);
char *argv[] = { "date", NULL };
execvp(argv[0], argv);
}
4. Copy-on-write
Given how we’ve seen fork
used in class so far (commonly paired with execvp
), why does the copy-on-write approach make more sense?
The vast majority of fork
calls lead the child process to an execvp
call, where the child's address space is fully cleared and replaced with a brand new one. fork
's defense for the copy-on-write
model would be that it's a lot of work to replicate an entire address space only for it to be discarded a few lines later when execvp
is called.
Checkoff Questions
-
[Q1] What would happen if we fail to close the write end of pipes while implementing
subprocess
?- If you fail to close the write endpoints in the parent and child processes, the descriptors monitoring the read end of those same pipes (potentially the child’s descriptor 0) never sense an end of file. If standard input of the child process is depending on end of file (i.e. reading as much as possible until it reaches the end of input) and never gets it, the child process will stall forever!
-
[Q2] Explain why, if both
supplyChildInput
andingestChildOutput
aretrue
, we need to create two pipes, rather than using just one for both.- If we used just one pipe, the input/output from the parent and child would get mangled together. For instance, the parent would be writing to the pipe to send input to the child's STDIN, but at the same time the child could be writing to the pipe to send output back to the parent. In this way, the contents could be interwoven. For that reason, we need two distinct pipes for the two communication channels.
-
[Q3] Describe a scenario where the call to
kill
in yourtimeout
implementation fails and returns a -1 (hint: if a process is no longer running), but then explain why it doesn't matter.- It could be that the runner up child process terminated second, but before the parent executed the
kill
call, and sokill
will error. However, this doesn't matter because our goal was to terminate the process anyway, and we'll still proceed to clean it up withwaitpid
.
- It could be that the runner up child process terminated second, but before the parent executed the