You are expected to understand this. CS 111 Operating Systems Principles, Spring 2005

Midterm Sample Questions Solutions

Tom Thumb found the Lab 1 "shell" project just too hard, so he decided to write a "mini-shell" instead. His mini-shell can only run one sub-program, with mandatory input redirection. For example, the command "minish wc -l foo.txt" should run the command "wc -l < foo.txt". When the sub-command exits, the mini-shell will print out "Done!". Here's his code:

int
main(int argc, char **argv)
{
    int infd;
    pid_t p;

    // Open the last argv[] argument for redirection
    infd = open(argv[argc - 1], O_RDONLY);
    dup2(infd, 0);
    
    if ((p = fork()) == 0) {
        argv[argc - 1] = NULL;      // clear out redirection
        execvp(argv[1], &argv[2]);  // run subcommand!
    }

    printf("Done!\n");
}

Needless to say, Tom Thumb isn't a very good programmer! Help the poor fellow out.

Question. List at least 3 error conditions that Tom has forgotten to check for.

Answer. Here are just some of the problems, most common ones first. There are also some plain old bugs in the code that are not related to error conditions. Here are some:

Chastened, Tom returns to the drawing board. Here's his second try.

void
sigchld_handler(int signal_value)
{
    int status;
    (void) wait(&status);
    printf("Done!\n");
    exit(0);
}

int
main(int argc, char **argv)
{
    int infd;
    pid_t p;

    // Open the last argv[] argument for redirection
    infd = open(argv[argc - 1], O_RDONLY);
    dup2(infd, 0);
    
    if ((p = fork()) == 0) {
        argv[argc - 1] = NULL;      // clear out redirection
        execvp(argv[1], &argv[2]);  // run subcommand!
    }

    signal(SIGCHLD, &sigchld_handler);
    while (1)
        /* Do nothing */;
}

This is better, but not much.

Question. Does Tom's code contain any race conditions, or "Heisenbugs"? If yes, describe a sequence of events that will trigger a race condition. If no, explain why not.

Answer. Yes, the code contains a race condition. For example, if the child finishes executing before the signal handler can be installed, the parent will loop forever, because it will never receive a signal from the child.

Question. Describe the performance consequences of Tom's waiting strategy (the while loop in main). Could Tom do better? Why and how? Refer to specific operating system structures discussed in class.

Answer. The parent is busy-waiting while the child executes. This means the parent is competing with the child for CPU cycles even though it is not doing anything productive. It would be better if the parent blocks until the child finishs. Tom can do this using a wait() or waitpid() call in place of signal().

Question. Describe at least two fundamentally different circumstances where Tom's minishell will never return.

Answer. Here's a heaping handful.

All your CS 111 TAs love mutexes. In fact, they love mutexes to an insane degree. Last weekend on a late-night skim-milk bender, they wrote three programs that attempt to synchronize on three different mutexes. Here they are:

mutex_t l1, l2, l3;

process_1() {
    acquire(l3);
    acquire(l1);
    release(l1);
    release(l3);
}

process_2() {
    acquire(l2);
    acquire(l3);
    acquire(l2);
    release(l2);
    release(l2);
    release(l3);
}

process_3() {
    acquire(l3);
    acquire(l1);
    acquire(l2);
    release(l1);
    release(l3);
    release(l2);
}

The TAs were supposed to figure out whether these processes contained a deadlock, but cleverly, they've decided to pass off this work to you.

Question. Describe sequences of operations that will lead to deadlock, or argue why no such sequences exist, in the cases when:
  1. mutex_t is a simple mutual-exclusion lock.
  2. mutex_t is a recursive mutual-exclusion lock.
  3. mutex_t is a semaphore allowing at most two processes to hold the lock at a time.
Answer.
  1. Simple mutual-exclusion lock can cause deadlocks due to circular wait. For example, process 2 can acquire l2, and then process 3 can acquire l3 and l1. At this point, no process can proceed. Process 1 and 2 are trying to acquire l3, held by process 3. Process 3 is trying to acquire l2, held by process 2.
  2. Recursive mutual-exclusion locks will prevent the problem of process 2 causing a deadlock by trying to acquire l2 multiple times. However, the circular wait condition as previously described will still cause a deadlock.
  3. With a semaphore that allows up to two processes to hold a lock at a time, deadlock cannot occur. No process can deadlock the system by itself because no process makes more than 2 acquires for the same lock. Between the three processes, there are only two acquires for l1. This means no sequence of acquires will cause a process to block on l1. Additionally, process 1 will run to completion if it can grab l3. The only time process 1 can block is when both process 2 and 3 hold l3. Knowing this, proving process 2 and 3 will never deadlock among themselves is sufficient to prove the whole system will not deadlock.

    Between process 2 and process 3, there are 2 acquires for l3, thus l3 cannot cause either process to block. For l2, if process 2 acquires l2 twice, then it eventually runs to completion. If process 2 acquires l2 once, then process 3 can acquire l2 and run to completion.

Question. Define a lock ordering for the mutexes, and change one or more of the processes to follow that lock ordering, so that no deadlock is possible with recursive mutexes.

Answer. Any lock ordering will work, as long as all processes follow it. For example, the lock ordering can be 3, 2, 1. In this case, we modify the code as follows:
mutex_t l1, l2, l3;

process_1() {
    acquire(l3);
    acquire(l1);
    release(l1);
    release(l3);
}

process_2() {
    acquire(l3);
    acquire(l2);
    acquire(l2);
    release(l2);
    release(l2);
    release(l3);
}

process_3() {
    acquire(l3);
    acquire(l2);
    acquire(l1);
    release(l1);
    release(l2);
    release(l3);
}

Question. Circle one of the following goals that was a motivation behind the Flash server's AMPED design. Justify your choice briefly, referring to specific features of the SPED design from which AMPED was derived.
  1. Performance
  2. Isolation
  3. Portability
  4. Moral virtue
  5. Ease of programming
  6. Small code size
Answer.
  1. Performance - Yes, a motivation. SPED uses a single process to avoid the overhead of context switches for in-memory workloads. However, when a SPED web server accesses the disk, it can block, because most OS mechanisms for non-blocking I/O don't work effectively for disk I/O. AMPED aims to avoid this blocking by farming out disk requests to other processes.
  2. Isolation - Not a motivation. Both SPED and AMPED are event-driven architectures. This means, for the most part, there is only one thread of control; there is little or no . With one process, there is nobody else to protect oneself against. In comparison, individual threads in a multi-threaded architecture has to be concerned with locks and race conditions.
  3. Portability - Yes, a motivation. SPED can be implemented using only a set of standard system calls that is available on many modern operating systems. However, some of these platforms do not support true asynchronous I/O. AMPED avoids this problem by forking off processes to do I/O. This means one can implement AMPED using a small set of standard syscalls without relying on the underlying implementation of I/O calls.
  4. Moral virtue - Lame joke! Boooooooo!
  5. Ease of programming - Not really a motivation. The AMPED architecture, with its multiple processes, farmed-out requests, and shared cache, is if anything harder to program than SPED (which is already pretty hard).
  6. Small code size - Not a motivation. SPED servers will be smaller than AMPED servers, although the difference might not be that large.

Extra Credit. Put a box around the lame joke in the preceding question.