Solutions
1. 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;
return 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;
}
2. Pipes and dup2
Q2: What is the dup2 call doing in the parent code? How does this still end up sending a message to the child process?
A2: The dup2 call is changing STDOUT to be connected to the write end of the pipe, instead of the terminal. This means that from that point onwards, anything the parent process prints to the terminal will actually be fed into the pipe. So when the parent prints the message later, that is sent through the pipe to the child process.
Q3: Try modifying the code to move the pipe creation to directly after the fork call. What happens? Why?
A3: The child no longer receives the message from the parent - this is because the parent and the child now each have their own copy of the pipe, rather than sharing one pipe. This happens because file descriptors are only shared on fork; any file descriptors in the parent at the time of the fork are shared, but file descriptors opened after the call to fork are separate within each process.
Q4: Why do we have the close(fds[1]) line after the call to dup2? Why are we still able to write a message to the pipe after that line executes?
A4: After calling dup2, we now have two file descriptors referring to the write end of the pipe, and we no longer need the original write file descriptor, as we have connected the pipe write end to the parent's STDOUT and will no longer use the original write file descriptor going foroward. For this reason, we can close it, but the pipe remains open, with its write end connected to STDOUT.
Q5: Try adding a sleep(2) call to the parent code immediately before the printf, so that the child will be essentially guaranteed to reach the read call before the parent writes anything. Does this cause any issues? Why or why not?
A5: This doesn't cause any issues (though the program will take 2 seconds longer) - the reason it is ok for the child to call read before anything has been written to the pipe is because read will wait until it receives data, or until "end of file" is reached (for a pipe, this means that all of its write ends are closed across all processes that share it).
3. Copy-on-write
Q6: 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?
A6: 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.
4. Pipes and File Descriptors
Q7: Read through the code to understand the behavior of this program. When you're ready, compile the program with make and run it. What is the output for this program? Will it be the same every time you run it?
A7: the output is listed below; it will be the same every time, because the parent always waits for the child to print before printing the second time, so it will always go parent-child-parent.
parent: [/* This file is ]
child: [a demo of file d]
parent: [escriptors being]
Q8: Imagine that we paused both the parent and child at the line immediately following fork. What would the current reference count be for the open file? Why is that?
A8: The reference count is 2; this is because both the parent and the child have file descriptors referring to this open file table entry. Here's a diagram of what the file descriptor tables and open file table might look like (click on image to open larger version):
Q9: After what line does the open file table entry for our open file go away? Why?
A9: The open file table entry goes away after line 48 executes; this is because by that point, the parent is the only process with a file descriptor referring to it, and when it is closed on line 48, the reference count goes from 1 to 0 and the open file table entry is removed. Here's a diagram of what the file descriptor tables and open file table might look like after the child terminates and on image is cleaned up, but before the parent reaches line 48 (click to open larger version):
Q10: If we paused execution of both the parent and child on the line immediately after the fork, what would the reference count be of the open file table entry for the read end of the pipe? The write end? How many times would we need to close file descriptors to properly close this pipe in the parent and child?
A10: The reference count is 2 for each end; this is because both the parent and the child have file descriptors referring to each end of the pipe. We would need to close both ends of the pipe in both the parent and child to properly close the pipe. Here's a diagram of what the file descriptor tables and open file tabon image le might look like at that point in the program (click to open larger version):
Q11: Now imagine that we moved these two lines to be immediately after (not before!) the fork call. If we paused both the parent and child right after they both created the pipe, what file descriptors and open file table entries would be present as a result of these lines? Why is this different than when this code was executed prior to the fork call?
A11: Since these lines are run by both the parent and the child, both have two file descriptors created, but they point to different open file table entries, becuase each of them creates their own pipe. Therefore, there are 4 new open file table entries, 2 for each pipe, all with reference count 1. (This means that if the parent or child tried to use their pipe to communicate with the other, it wouldn't work becuase they are two different pipes). Here's a diagram of what ton image he file descriptor tables and open file table might look like (click to open larger version):
Checkoff Questions
-
[Q1] In various cases, we can choose to either call
waitpidwith a specific PID, or pass in -1 in a loop, and both would functionally work. Withtimeout, however, the firstwaitpidcall must use -1 rather than passing in a specific PID. Why is that?- The first
waitpidcall's goal is to figure out which process ends first, which is a perfect fit for passing in -1, as it waits for the next of our child processes to finish, whichever it is. Passing in a specific PID wouldn't work because if the one we didn't pass in happened to finish first, there would be no way for us to know.
- The first
-
[Q1] Describe a scenario where the call to
killin yourtimeoutimplementation doesn't terminate the process, 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
killcall, and sokillwill not do anything since the process is already terminated. 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
-
[Q2.2] Why is there a difference in program behavior between making a pipe before the call to
forkvs. after?- The difference is due to the fact that pipes are accessible by the child on a call to
fork, but any pipes created after a call toforkare kept separate, since they are created in separate processes. Therefore, if we make a pipe and thenfork, that pipe will be accessible by both the parent and child processes. However, if weforkand then make a pipe, each process is making a separate pipe, so the pipes are each private to the process that created it - if the parent or child tried to use their pipe to communicate with the other, it wouldn't work because they are two different pipes (even though they are the same file descriptor numbers in both processes).
- The difference is due to the fact that pipes are accessible by the child on a call to



