CS360 Final Exam - May 3, 2018 - James S. Plank

Answers and Explanations

Question 1 - 13 points

Straightforward assembler:

x:
    push #4                   / Allocate p
    mv %g1 -> [sp]--          / Push b and 1 on the stack in reverse order
    ld [fp+12] -> %r0
    st %r0 -> [sp]--
    jsr p                     / Jump to p and pop the arguments when you return
    pop #8
    st %r0 -> [fp]            / Store the return value of p
    ld [fp] -> %r0            / Load p and dereference it.
    ld [r0] -> %r0
    ret

Grading:


Question 2 - 20 points

I understand that questions like this are problematic for you. They are problematic for me too. The thing is, I'd rather simply write questions that involve code -- write it or read it and give me output. It's precise, and there are clear correct/incorrect answers. The problem is that with CS360, the latter takes too long for you, and it's hard for you to get it right. So, I write questions like this one.

The other problem with this is that you find yourself panicked, because you feel like you're trying to read my mind. Sorry about that, but I try to make the "problems" here glaringly obvious -- that one who has learned how computers and systems work will spot the problems rather quickly.

In the case of this problem, the experience from which I wanted you to draw was from things you have seen and done in class -- specifically, the first few lectures on threads, the "SSN Server" program, and your chat server lab. In the latter two, the code is very much like this code -- you have multiple client connections, and each is served by a single thread. For that reason, you do not need to protect the file descriptors for the sockets, or the stdio buffers, if you have opened them with fdopen().

I tried to communicate this with the language of the question: "code for an interactive server that interacts with multiple clients, one per thread." You'll also notice that I say that "the server reads two integers from a client connection, uses them to update a shared array, and then writes the array back to the client." Thus, I intended it to be clear that multiple threads would not be reading from the same connection.

Now, many of you did the correct thing -- you saw the pthread_mutex_lock() call between two fscanf() calls, and the alarm bells went off. Good. Then, of course, the class split into two groups: Those who figured this would lead to a race condition on the client connection, and those who saw that the second fscanf() call could block on a client who is slow at producing numbers, and that would lock out the other threads who are accessing the shared array.

The second of these is the correct one, because the explanation of the program has let you know that the first one won't happen. However, I did give you points for the first answer, becuase you are thinking along the correct lines.

Here are the correct answers:

Part A: The first problem is that the second fscanf() call is made while holding the mutex. You'll note that there is no reason why you need to hold the mutex while reading in the value. Instead, you can read both integers and then lock the mutex. The problem will manifest if there is a significant delay from the client between reading the two integers. Then all of the other threads who are trying to access the array (or read their second integers) are locked out.

The second problem is that the fwrite() and fflush() calls are made while holding the mutex. In that call, you are writing 16K bytes, while holding the mutex. If the network connection is slow, that can slow down the other threads.

Part B: To fix the first problem, add an integer variable, and then read the two integers before locking the mutex. Then lock the mutex and insert the integers into the array.

To fix the second problem, you can add a 16K array as a local variable. Then memcpy() the 16K of data into this array and unlock the mutex, before writing the array to the socket connection.

Other Answers:

I saw a lot of other answers to this problem, so I will try to deal with them in turn:

Grading

This was 20 points, split 10 and 10 for each explanation / manifestation and fix.


Question 3 - 20 points

This calls for a mutex, a condition variable and a counter. The counter will keep track of how many threads have currently locked the twotex. Initialize the counter to zero. When a thread wants to lock the twotex, you lock the mutex and check the counter. If its value is two, then you block on the condition variable, until you wake up and the counter is less than two. If its value is less than two, then increment it, unlock the mutex and return. When you unlock the twotex, you lock the mutex, decrement the counter and signal the the twotex, so you block on the condition variable. Otherwise, you unlock the mutex and return. When you unlock the twotex, you signal the condition variable.

Here's the code:

void *new_twotex()
{
  Twotex *t;

  t = (Twotex *) malloc(sizeof(Twotex));
  t->lock = new_mutex();
  t->cond = new_cond();
  t->counter = 0;
  return (void *) t;
}

void lock_twotex(void *t) 
{
  Twotex *x;

  x = (Twotex *) t;
  PML(x->lock);
  while (x->counter == 2) PCW(x->cond, x->lock);
  x->counter++;
  PMU(x->lock);
  return;
}
void unlock_twotex(void *t)
{
  Twotex *x;

  x = (Twotex *) t;
  PML(x->lock);
  x->counter--;
  PCS(x->cond);
  PMU(x->lock);
  return;
}

void delete_twotex(void *t)
{
  Twotex *x;

  x = (Twotex *) t;
  free(x->lock);
  free(x->cond);
  free(x);
}

You need a while loop in lock_twotex(), rather than an if statement. This is the for the same reason as in the printer simulation.

There were a lot of answers that fit a pattern, which I call "M1-M2" answers. With these, you typically had two mutexes and a counter. You would check the counter, and:

There were a few variants of this, such as trying to replace the counter with a mutex. There are two problems with this answer. The first is that the mutexes aren't doing anything. Mutexes are there to protect shared data, not to actively block threads. The second, and bigger problem is that with this solution, you didn't protect the shared data, so you had a race condition with your counters, your mutexes and your condition variables.

Another answer is one that I called "Simple M1-M2". This is where you simply had two mutexes and locked them. This is incorrect for obvious reasons.

Grading


Question 4 - 25 points

What a fun program. The simplest thing to do here is to open three pipes: Here we go: q4.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

void cp(int p[2])
{
  close(p[0]);
  close(p[1]);
}

int main()
{
  int P_AE[2];
  int P_EY[2];
  int P_YA[2];
  int status;
  char line[100];
  FILE *from, *to;

  while(1) {
    pipe(P_AE);
    pipe(P_EY);
    pipe(P_YA);

    if (fork() == 0) {     /* This process will do AI_Aaron. */
      dup2(P_AE[1], 1);    /* Standard output goes to AI_Erin */
      dup2(P_YA[0], 0);    /* Standard input comes from your program. */
      cp(P_AE);            /* Close all of those extraneous pipes. */
      cp(P_EY);
      cp(P_YA);
      execlp("AI-Aaron", "AI-Aaron", NULL);  /* Exec and done. */
      perror("execlp-AI-Aaron");
      exit(1);
    }

    /* You don't need an "else" because the child process either goes away
       via execlp, or it exits because execlp failed. */

    if (fork() == 0) {    /* This process will do AI-Erin. */
      dup2(P_EY[1], 1);    /* Standard output goes to your program*/
      dup2(P_AE[0], 0);    /* Standard input comes from AI-Aaron. */
      cp(P_AE);            /* Close all of those extraneous pipes. */
      cp(P_EY);
      cp(P_YA);
      execlp("AI-Erin", "AI-Erin", NULL);  /* Exec and done. */
      perror("execlp-AI-Erin");
      exit(1);
    }

    cp(P_AE);        /* Close all extraneous file descriptors. */
    close(P_YA[0]);  /* You are using P_YA[1] to write to Aaron. */
    close(P_EY[1]);  /* You are using P_EY[0] to read from Erin. */

    from = fdopen(P_EY[0], "r");
    to = fdopen(P_YA[1], "w");

    while(fgets(line, 100, from) != NULL) {
      fputs(line, to);
      fflush(to);
    }
    fputs(line, stdout);
    fflush(stdout);
    fclose(from);
    fclose(to);
    close(P_YA[1]);  /* fclose should do this, but it can't hurt. */
    close(P_EY[0]);

    wait(&status);    /* Both processes should be dead at this point. */
    wait(&status);
  }
}

I've written AI-Aaron.c and AI-Erin.c so that you can test it out.

Grading


Question 5 - 15 points

A socket is a mechanism implemented by the operating system so that clients and servers may communicate with each other on the same machine, on a local network, or even on the Internet. A serving process requests for the operating system to serve a socket on a given port (which is a number). The operating system returns a file descriptor to the process. At that point, the serving process can accept any number of connections from clients. Each connection is given a file descriptor by the operating system, and the server reads from and writes to the connection with the system calls read() and write().

A client requests to make a connection to a server, again through the operating system, this time specifying the machine to which it wants to connect (the "host") and the port number. If successful, the operating system returns a file descriptor, which the client uses to read and write in the same manner as the server.

Grading

This was 15 points, which was pretty much assigned according to how good your answer was. Sorry that I can't be more precise than that.