CS560 Midterm Exam, March 12, 2009

Answer all questions

Question 1

This solution comes directly from the lecture notes:
  1. Philosopher 0 is hungry and gets the chopsticks.
  2. Philosopher 4 is hungry and waits.
  3. Philosopher 2 is hungry and gets the chopsticks.
  4. Philosophers 1 and 3 are hungry and wait.
  5. Philosopher 2 is sated and puts down the chopsticks.
  6. Philosopher 3 now gets the chopsticks.
  7. Philosopher 0 is sated and puts down the chopsticks.
  8. Philosopher 1 now gets the chopsticks.
  9. Philosophers 0 and 2 are hungry and wait.
  10. Philosopher 1 is sated and puts down the chopsticks.
  11. Philosopher 0 now gets the chopsticks.
  12. Philosopher 3 is sated and puts down the chopsticks.
  13. Philosopher 2 now gets the chopsticks.
  14. Philosophers 1 and 3 are hungry and wait.
  15. Repeat from step 5
The process is illustrated below. Note how philosopher 4 is never able to get the chopsticks because both are never available:

There are other solutions. For example, a four-philosopher table can work too -- suppose eating times are long and thinking times are short. Then philosophers 0 and 2 can starve philosophers 1 and 3 so long as either philosopher 0 or 2 gets the chopsticks first.

Grading

Six points. You had to specify the order in which the philosophers drop he chopsticks to receive full credit.

Question 2

Grading


Question 3

Like all blocking calls in the CBThread library, the basic structure is to figure out what to do with the current thread, and then to call block_myself(). Although this thread is never going to execute again, its state may need to be maintained for another thread to call cbthread_wait() on it.

The operations are as follows:

Grading


Question 4

You can fix that last situation by treating all jobs with a range of predictions in a FIFO manner and by elongating their time quanta. That fixes the fairness issue, and also gives more CPU time to the CPU-bound jobs.

Grading


Question 5

The strategy is to keep a red-black tree keyed on the name. When a sender sends a message, it should look at the red-black tree for the name. If it's not there, then the ZB_call struct should be put on the tree. Same for when a receiver posts a receive.

Now, suppose that the name is on the tree and it is of the proper type (e.g. a sender is sending a message and a receiving ZB_call is on the tree, or a receiver is trying to receive and a sending ZB_call is on the tree). Then you take the ZB_call off the tree, perform the proper memcpy() and have both processes return.

The subtlety about dealing with multiple senders and receivers of messages with the same name was a little cruel. You can deal with it by having the red-black tree's val fields be dllists and handle it that way. I'm going to show both implementations. Both will use the following modified jos.h:

#include "simulator.h"
#include "dllist.h"
#include "jrb.h"
#include "cbthread.h"

typedef struct {
  int regs[NumTotalRegs];
} PCB;

typedef struct {
  char *name;
  char *physical_ptr;
  int size;
  PCB *caller;
  int send_or_receive;  /* 'S' or 'R' */
} ZB_call; 

typedef struct {
  Dllist readyq;     /* Ready queue */
  PCB *running;     /* Pointer to the currently running process's PCB */
  PCB *nulljob;     /* Pointer to a 'null' job -- we'll set running to 
                       this when we run noop() */
  JRB zb_waiters;   /* The red-black tree of waiting senders/receivers */
} JOS_GLOBALS;

extern JOS_GLOBALS JG;

extern void JOS_Scheduler();

extern void process_zb(void *arg);

I assume that zb_waiters is initialized in the JOS initialization.

Here's the easy solution (q5-ans.c) q5-ans.c

#include "jos.h"

void process_zb(void *arg)
{
  ZB_call *zb, *other;
  JRB tmp;
  int nbytes;

  zb = (ZB_call *) arg;
  tmp = jrb_find_str(JG.zb_waiters, zb->name);

  /* If there's no matching call in the tree, insert the ZB_call and return */

  if (tmp == NULL) {
    jrb_insert_str(JG.zb_waiters, zb->name, new_jval_v((void *) zb));
    return;   /* Note, this will have the scheduler run. */
  }

  /* If the other call in the tree is of the same type, insert the ZB_call and return */

  other = (ZB_call *) tmp->val.v;
  if (other->send_or_receive == zb->send_or_receive) {
    jrb_insert_str(JG.zb_waiters, zb->name, new_jval_v((void *) zb));
    return;  
  }

  /* Otherwise, we have a match.  First remove it from the tree: */

  jrb_delete_node(tmp);

  /* Next, copy the message. */

  nbytes = zb->size;
  if (other->size < nbytes) nbytes = other->size;
  if (zb->send_or_receive == 'S') {
    memcpy(other->physical->ptr, zb->physical->ptr, nbytes);
  } else {
    memcpy(zb->physical->ptr, other->physical->ptr, nbytes);
  }

  /* Set the return value (it goes is Register 2, but any one would do for this exam). */

  zb->caller->regs[Reg2] = nbytes;
  other->caller->regs[Reg2] = nbytes;

  /* Put the two PCB's onto the ready queue */

  dll_append(JG.readyq, new_jval_v((void *) zb->caller));
  dll_append(JG.readyq, new_jval_v((void *) other->caller));

  /* Finally, free up the memory */

  free(zb);
  free(other);
  return;
}

The hard solution just uses a dllist as the val field (q5-hard-ans.c):

#include jos.h

void process_zb(void *arg)
{
  ZB_call *zb, *other;
  JRB tmp;
  Dllist l;
  int nbytes;

  zb = (ZB_call *) arg;
  tmp = jrb_find_str(JG.zb_waiters, zb->name);

  /* If there's no matching call in the tree, insert the ZB_call and return */

  if (tmp == NULL) {
    l = new_dllist();
    dll_append(l, new_jval_v((void *) zb));
    jrb_insert_str(JG.zb_waiters, zb->name, new_jval_v((void *) l));
    return;   
  }

  l = (dllist) tmp->val.v;
  other = (ZB_call *) l->flink;

  /* If the other calls in the tree are of the same type, append the ZB_call and return */

  if (other->send_or_receive == zb->send_or_receive) {
    dll_append(l, new_jval_v((void *) zb));
    return;  
  }

  /* Otherwise, we have a match.  First remove it from the list/tree: */

  dll_delete_node(l->flink);
  if (dll_empty(l)) {
    dll_free_tree(l);
    jrb_delete_node(tmp);
  }

  /* The rest of the code is the same as before. */

  nbytes = zb->size;
  if (other->size < nbytes) nbytes = other->size;
  if (zb->send_or_receive == 'S') {
    memcpy(other->physical->ptr, zb->physical->ptr, nbytes);
  } else {
    memcpy(zb->physical->ptr, other->physical->ptr, nbytes);
  }

  /* Set the return value (it goes is Register 2, but any one would do for this exam). */

  zb->caller->regs[Reg2] = nbytes;
  other->caller->regs[Reg2] = nbytes;

  /* Put the two PCB's onto the ready queue */

  dll_append(JG.readyq, new_jval_v((void *) zb->caller));
  dll_append(JG.readyq, new_jval_v((void *) other->caller));

  /* Finally, free up the memory */

  free(zb);
  free(other);
  return;
}

You could have used a semaphore instead -- have two JRB trees -- one of senders and one of receivers. The logic gets convoluted, unfortunately, and a list is much eaiser to implement.

Grading

Clearly, this question was too hard. Sorry.

I've allocated 18 points to the question, broken into three point sections on the details to which you should have tried to attend:

You got points in each section according to how well or coherently you addressed the point. If your answer had some merit that is not covered by the sections above, or some problems not addressed above, I adjusted your score at the end.