Section 3 Solutions

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 File Descriptors

Q2: 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?

A2: 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]

Q3: 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?

A3: 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):

Shows parent and child process control blocks, plus open file table.  The open file table has 4 entries: 1. terminal in (mode "r", refcount=2), 2. terminal out (mode "w", refcount=2), 3. terminal error (mode "w", refcount=2), 4. open file (mode "r", refcount=2).  The parent process control block contains the parent file descriptor table; index 0 points to the "terminal in" entry in the open file table, index 1 points to the "terminal out" entry in the open file table, index 2 points to the "terminal error" entry in the open file table, and index 3 points to the "open file" entry in the open file table. The child process control block contains the child file descriptor table; index 0 points to the "terminal in" entry in the open file table, index 1 points to the "terminal out" entry in the open file table, index 2 points to the "terminal error" entry in the open file table, and index 3 points to the "open file" entry in the open file table.

Q4: After what line does the open file table entry for our open file go away? Why?

A4: 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):

Shows parent process control block, plus open file table.  The open file table has 4 entries: 1. terminal in (mode "r", refcount=1), 2. terminal out (mode "w", refcount=1), 3. terminal error (mode "w", refcount=1), 4. open file (mode "r", refcount=1).  The parent process control block contains the parent file descriptor table; index 0 points to the "terminal in" entry in the open file table, index 1 points to the "terminal out" entry in the open file table, index 2 points to the "terminal error" entry in the open file table, and index 3 points to the "open file" entry in the open file table.

Q5: 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?

A5: 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):

Shows parent and child process control blocks, plus open file table.  The open file table has 6 entries: 1. terminal in (mode "r", refcount=2), 2. terminal out (mode "w", refcount=2), 3. terminal error (mode "w", refcount=2), 4. open file (mode "r", refcount=2), 5. pipe read end (mode "r", refcount=2), 6. pipe write end (mode "w", refcount=2).  The parent process control block contains the parent file descriptor table; index 0 points to the "terminal in" entry in the open file table, index 1 points to the "terminal out" entry in the open file table, index 2 points to the "terminal error" entry in the open file table, index 3 points to the "open file" entry in the open file table, index 4 points to the "pipe read end" entry in the open file table, and index 5 points to the "pipe write end" entry in the open file table. The child process control block contains the child file descriptor table; index 0 points to the "terminal in" entry in the open file table, index 1 points to the "terminal out" entry in the open file table, index 2 points to the "terminal error" entry in the open file table, index 3 points to the "open file" entry in the open file table, index 4 points to the "pipe read end" entry in the open file table, and index 5 points to the "pipe write end" entry in the open file table.

Q6: 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?

A6: 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):

Shows parent and child process control blocks, plus open file table.  The open file table has 8 entries: 1. terminal in (mode "r", refcount=2), 2. terminal out (mode "w", refcount=2), 3. terminal error (mode "w", refcount=2), 4. open file (mode "r", refcount=2), 5. first pipe read end (mode "r", refcount=1), 6. first pipe write end (mode "w", refcount=1), 7. second pipe read end (mode "r", refcount=1), 8. second pipe write end (mode "w", refcount=1).  The parent process control block contains the parent file descriptor table; index 0 points to the "terminal in" entry in the open file table, index 1 points to the "terminal out" entry in the open file table, index 2 points to the "terminal error" entry in the open file table, index 3 points to the "open file" entry in the open file table, index 4 points to the "first pipe read end" entry in the open file table, and index 5 points to the "first pipe write end" entry in the open file table. The child process control block contains the child file descriptor table; index 0 points to the "terminal in" entry in the open file table, index 1 points to the "terminal out" entry in the open file table, index 2 points to the "terminal error" entry in the open file table, index 3 points to the "open file" entry in the open file table, index 4 points to the "second pipe read end" entry in the open file table, and index 5 points to the "second pipe write end" entry in the open file table.

3. Copy-on-write

Q7: 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?

A7: 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

  1. [Q1] In various cases, we can choose to either call waitpid with a specific PID, or pass in -1 in a loop, and both would functionally work. With timeout, however, the first waitpid call must use -1 rather than passing in a specific PID. Why is that?

    • The first waitpid call'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.
  2. [Q1] Describe a scenario where the call to kill in your timeout 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 so kill 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 with waitpid.
  3. [Q2.2] Why is there a difference in file descriptor state between making a pipe before the call to fork vs. after?

    • The difference is due to the fact that pipes are duplicated into the child on a call to fork, but any pipes created after a call to fork are kept separate, since they are created in separate processes. Therefore, if we make a pipe and then fork, that pipe will be duplicated into the child process, and both the parent and child can access that same pipe. However, if we fork and then make a pipe, each process is making a separate pipe with separate open file table entries, 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 becuase they are two different pipes (even though they are the same file descriptor numbers in both processes).