Final Exam. May 8, 2001. Question 5 -- Answers and Grading

12 points total

Answer

Ok -- many students didn't seem to get the question. This is a very OS-ish kind of set up. You are managing a disk. You field add_disk_transation() calls when users want to do a disk transaction. However, the disk can only do one transaction at a time. When the disk is doing a transaction, it lets you know that it's done by calling disk_done().

Here's a very simple piece of code that does the C-LOOK scheduling algorithm from the book. It manages a red-black tree of the transactions, and keeps a global pointer to the node that it is currently servicing. When it's done, it moves onto the next node.


static JRB DT_tree = NULL;
static JRB DT_pointer = NULL;
static int disk_busy = 0;

add_disk_transaction(DT_struct *dts) 
{

  /* If the disk is busy, just insert the transaction into the tree */
  if (disk_busy) {
    jrb_insert_int(DT_tree, dts->block_number, new_jval_v(dts));

  /* Otherwise, create the tree if it doesn't exist yet */
  } else {
    if (DT_tree == NULL) {
      DT_tree = make_jrb();
    }

    /* Put the transaction into the tree, and start the disk */
    DT_pointer = jrb_insert_int(DT_tree, dts->block_number, new_jval_v(dts));
    disk_busy = 1;
    start_disk(dts->block_number, dts->buf, dts->readwrite);
  }
}

disk_done()
{
  DT_struct *dts;

  /* Wake up the thread who was waiting for the disk transaction */
  dts = (DT_struct) DT_pointer->val.v;
  V_kt_sem(dts->sem);

  /* Find the next transaction.  Kill the previous transaction */
  DT_pointer = jrb_next(DT_pointer);
  jrb_delete_node(jrb_prev(DT_pointer));

  /* If we're at the end of the tree, go back to the beginning */

  if (DT_pointer == DT_tree) DT_pointer = jrb_next(DT_pointer);

  /* If the tree is empty, mark the disk as idle and return */

  if (DT_pointer == DT_tree) {
    disk_busy = 0;
    return;
  }

  /* Otherwise, start the next transaction */
  dts = (DT_struct) DT_pointer->val.v;
  start_disk(dts->block_number, dts->buf, dts->readwrite);
}
This kind of scheduling is pretty much the best, as it doesn't waste head motions, it keeps the head seek times to a minimum, and it does not favor one part of the disk over another. See the book (chapter 13) for more text concerning disk scheduling.

A few of you had a different flow of control. You had a global semaphore to make sure that the disk was idle, and always called start_disk() in add_disk_transaction(). And some of you used the red-black tree differently.

Here's code that does both. Now that I look at it, I think that the flow of control here is much cleaner than mine -- well done, those of you who thought of it!

static JRB DT_tree = NULL;
static int current_block;
static kt_sem disk_sem;
static DT_struct *current;

add_disk_transaction(DT_struct *dts) 
{
  JRB tmp;

  /* Initialize if necessary */
  if (DT_tree == NULL) {
    DT_tree = make_jrb();
    current_block = 0;
    disk_sem = make_kt_sem(1);
  }

  /* Put the job in the tree */
  jrb_insert_int(DT_tree, dts->block_number, new_jval_v(dts));

  /* Now block until the disk is idle */
  P_kt_sem(disk_sem);

  /* Now choose the next job and start it */

  tmp = jrb_find_gte_int(DT_tree, current_block, &found);
  if (found == 0) tmp = jrb_first(DT_tree);
  current = (DT_struct *) tmp->val.v;
  jrb_delete_node(tmp);
  current_block = current->block_number+1; /* This prevents starvation */
  start_disk(current->block_number, current->buf, current->readwrite);
}

disk_done()
{
  /* Wake up the thread who was waiting for the disk transaction */
  V_kt_sem(current->sem);

  /* Signal that the disk is idle */
  V_kt_sem(disk_sem);
}

Grading