CS560 Lecture notes -- KThreads Lecture #1

  • Jim Plank, Rich Wolski
  • CS560: Operating Systems
  • Directory: http://www.cs.utk.edu/~mbeck/classes/cs560/560/notes/KThreads1
  • Lecture notes -- html: http://www.cs.utk.edu/~mbeck/classes/cs560/560/notes/KThreads1/lecture.html

    KThreads -- a simple, non-preemptive threads library

    Our last topic in threads for a little bit will be the KThreads library. This is a simple, non-preemptive threads library written by professor Wolski.

    Finding things, linking

    The source code for KThreads may be found in http://www.cs.utk.edu/~mbeck/classes/cs560/560/src/kthreads

    You use KThreads by including kt.h, which is in the directory http://www.cs.utk.edu/~mbeck/classes/cs560/560/include, then linking with http://www.cs.utk.edu/cs560/560/objs/libkosutil.a and http://www.cs.utk.edu/cs560/560/objs/libhdr.a. These libraries are all compiled for Solaris. They do work under Linux, but you have to do all the library compiling yourself. Perhaps if the TA's are feeling generous, they will do it for you...


    The KThreads Interface

    The KThreads interface is extremely simple. Here are all the calls that it supports:
    
    void *kt_fork(void (*func)(void *), void *arg);
    void  kt_join(void *kt);
    void  kt_exit();
    void *kt_self();
    void  kt_sleep(int secs);
    void  kt_yield();
    
    void kt_joinall();
    void kt_kill(void *t);
    
    typedef void *kt_sem;
    kt_sem  make_kt_sem(int initval);
    void    kill_kt_sem(kt_sem ksem);
    int     kt_getval(kt_sem s);
    void    P_kt_sem(kt_sem ksem);
    void    V_kt_sem(kt_sem ksem);
    
    
    The first six procedures (fork, join, exit, self, sleep and yield) should all be familiar to you. Kt_joinall() is a useful routine. It specifies for the calling thread to block until there are no more runnable threads. That means that all other threads are either finished, or blocked on a semaphore (see below). I don't know what kt_kill() does. Don't use it.

    The final five functions implement general semaphores, which are a different kind of synchronization primitive. Their functionality is equivalent to monitors and condition variables, since you can implement each with the other. However, they work differently.

    A semaphore is a data structure that contains an integer value. To create one, you call make_kt_sem() and pass it the initial value that you'd like it to have. This value must be greater than or equal to zero. To destroy a semaphore, and have it free all the memory that it has allocated, call kill_kt_sem().

    There are three procedures that act on semaphores:

    1. kt_getval() returns the value of the semaphore.
    2. P_kt_sem(s) performs two actions: It first decrements the value. Then, if the value is less than zero, it blocks.
    3. V_kt_sem(s) also performs two actions: It first increments the value. Then, if the value is less than or equal to zero, it selects on thread that is blocked, and unblocks it.

    There are times when semaphores are more convenient to use than monitors/condition variables, and times when the opposite is true. Since the first part of lab 3 has you implement monitors and condition variables with semaphores, you'll be able to use both throughout the semester.


    Example #1: The Printer Queue Simulator in Kthreads

    I'll give you two simple Kthreads examples. The first is solution number 5 from the Printer Simulation Lecture.

    printqsim.c, printqsim.h and ps5.c have been modified so that they now work with Kthreads. Copy them and the makefile and compile them:

    UNIX> make
    gcc -g -I/blugreen/homes/plank/cs560/include -c printqsim.c
    gcc -g -I/blugreen/homes/plank/cs560/include -c ps5.c
    gcc -g -I/blugreen/homes/plank/cs560/include -o ps5 printqsim.o ps5.o /blugreen/homes/plank/cs560/objs/libkosutil.a /blugreen/homes/plank/cs560/objs/libfdr.a
    UNIX> ps5 5 3 5 5 5 3
       0: user  0/000: Sleeping for  2 seconds
       0: user  1/000: Sleeping for  9 seconds
       0: user  2/000: Sleeping for  4 seconds
       0: user  3/000: Sleeping for  5 seconds
       0: user  4/000: Sleeping for  2 seconds
       0: prnt  0/000: ready to print
       0: prnt  1/000: ready to print
       0: prnt  2/000: ready to print
       2: user  4/000: Submitting a job with size 2
       2: user  4/001: Sleeping for  7 seconds
       2: user  0/000: Submitting a job with size 4
       2: user  0/001: Sleeping for  2 seconds
       2: prnt  0/000: Printing job   0 from user  4 size   2
       2: prnt  2/000: Printing job   0 from user  0 size   4
       ....
    
    Here are the relevant changes. First, all the proper changes to kt_fork(), kt_exit() and kt_sleep() were made in printqsim.c. Then, instead of having a monitor and condition variables in ps5.c, we just have two semaphores. One for jobs in the buffer, and one for empty slots in the buffer:
    typedef struct {
      Job **b;
      int head;
      int tail;
      int njobs;
      kt_sem job_sem;
      kt_sem slot_sem;
    } Buffer;
    
    We initialize the job semaphore to zero, since there are no jobs in the buffer at startup, and the slot semaphore to bufsize, since all buffer slots are available:
    void initialize_v(Spq *s)
    {
      Buffer *b;
    
      b = (Buffer *) malloc(sizeof(Buffer));
      b->b = (Job **) malloc(sizeof(Job *)*s->bufsize);
      b->head = 0;
      b->tail = 0;
      b->njobs = 0;
      b->job_sem = make_kt_sem(0);
      b->slot_sem = make_kt_sem(s->bufsize);
      s->v = (void *) b;
    }
    
    Then, in submit_job(), we first call P() on the slot semaphore, since we need an empty slot in the buffer to submit the job. Then we submit the job, and finally call V() on the job semaphore, since there is now a new job in the buffer:
    void submit_job(Spq *s, Job *j)
    {
      Buffer *b;
    
      b = (Buffer *) s->v;
    
      P_kt_sem(b->slot_sem);
    
      b->b[b->head] = j;
      b->head = (b->head + 1) % s->bufsize;
      b->njobs++;
    
      V_kt_sem(b->job_sem);
    }
    
    Note, we don't need a mutex to protect the code that adds the job to the buffer. Why? Because the KThreads system is non-preemptive, and therefore we don't have to worry about the CPU being taken from us unless we specifically block.

    Finally, here's the code for get_print_job(). It is like submit_job(), only this time it calls P() on the job semaphore, and V() on the slot semaphore:

    Job *get_print_job(Spq *s)
    {
      Buffer *b;
      Job *j;
    
      b = (Buffer *) s->v;
    
      P_kt_sem(b->job_sem);
    
      j = b->b[b->tail];
      b->tail = (b->tail + 1) % s->bufsize;
      b->njobs--;
    
      V_kt_sem(b->slot_sem);
    
      return j;
    }
    

    Example #2: DPhil4

    The second example is the asymmetrical solution of the Dining Philosopher's lecture. The three files are: dphil_skeleton.c, dphil.h and dphil_4.c.

    The changes are pretty obvious -- change the mutexes to semaphores that are initialized to one. And remove the blockmon mutex, since our threads are non-preemptive.