The kthreads library is at /cs/faculty/rich/cs170/lib/libkt.a, the source is at /cs/faculty/rich/cs170/src/libkt/, and the header is /cs/faculty/rich/cs170/include. You will also need to link to /cs/faculty/rich/cs170/lib/libfdr.a.
void *kt_fork(void (*func)(void *), void *arg);
void kt_exit();
void kt_join(void *kt_id);
void kt_joinall();
void *kt_self();
void kt_yield();
void kt_sleep(int sec);
kt_sem make_kt_sem(int initval);
void kill_kt_sem(kt_sem ksem);
void P_kt_sem(kt_sem ksem);
void V_kt_sem(kt_sem ksem);
kt_yield()
interrupts the current thread
and lets the scheduler run a new one. This primitive is nice in
non-pre-emptive thread systems because it allows a
kind of "polling" of the scheduler.
A thread calling kt_yield()
blocks itself and allows other
threads that can run to go ahead. When no more runnable threads are
available, the yielding thread will be resumed at the point of the yield.
The call kt_sleep()
sleeps the current
thread for a specified time period. Again, because the thread is
non-pre-emptive, the thread will be "awakened" and made runnable after the
specified time, but it will not actually run until it is given the CPU.
The call kt_self()
returns the thread id. No confusion here.
The function
kt_joinall()
is a useful function that causes the current thread to
block until all other threads have either exited or blocked on a semaphore.
You will find that this function is particularly handy in designing your OS.
The semaphore primitives are exactly as we discussed.
make_kt_sem()
creates a semaphore with a value greater than or equal to zero, and
kill_kt_sem()
destroys (and frees) it. P_kt_sem()
decrements the semaphore's value by 1, and if it is negative blocks it.
V_kt_sem()
increments the value by one, and it if it is zero or less
it unblocks one thread.
setjmp()
and longjmp()
. If you don't
understand the man pages now, at some point before this class ends read them
and you will certainly be able to see how they work.
KThread *ktRunning;
- currently running thread.KThread *ktJoinall;
- current thread doing a joinall.Dllist ktRunnable;
- fifo list of all runable threads.JRB ktSleeping;
- sorted list of sleeping threads.JRB ktBlocked;
- sorted list of blocked threads.JRB ktActive;
- searchable list of all threads.void (*func)(void *);
- thread function.void *arg;
- thread function argument.int id;
- unique thread id.int state;
- state of the thread: BLOCKED, RUNNING, RUNABLE, SLEEPINGvoid *stack;
- thread's stack.jmp_buf jmpbuf;
- thread's jump buffer.int value;
- the semaphores value.int id;
- the semaphores unique value.
The scheduler, called KtSched()
is the core of the threads
system. It is
called whenever a thread is abdicating the CPU and its job is to manage these
queues, and set the next runnable thread.
Since this system is non-premptive,
threads will only abdicate the cpu on their own initative. The calls
that do this are kt_join(), kt_exit(), kt_sleep(), kt_yield(), kt_joinall(),
and P_kt_sem()
. When a
thread abdicates the cpu, KtSched()
takes the thread that is at
the head of the Run Queue and makes it the running thread. It sets
the global pointer and switches from the current thread (which is the
one that is abdicating) to the new "running" thread.
The scheduler does not return untill there is a thread that can
be run or when there are no more threads in the system. We'll discuss this
behavior in greater detail as we discuss the other primitives. The important
thing to know about the scheduler, however, is that its job is to switch from
the currently running thread to the next runnable thread, and to manage the
internal queues.
kt_yield()
since it is now
easy to understand. When a thread calls kt_yield()
is simply
adds itself to the end of the Run Queue and calls ktSched()
.
Here is the source code for kt_yield()
.
void kt_yield() { InitKThreadSystem(); ktRunning->state = RUNNABLE; dll_append(ktRunnable,new_jval_v(ktRunning)); KtSched(); return; }Simple, huh? The call to
InitKThreadSystem()
is there just to
handle the case when kt_yield()
is the first thread call made
by a thread. This idempotent call prevents us from having to make an explicit
initialization call in a KThread code. A caller of kt_yield()
sets its state to RUNNABLE (it will no longer be RUNNING), appends itself to
the end of the Run Queue, and calls the scheduler. If there are other
runnable threads, they will each be run in turn and then, when this thread's
turn comes up it will be run again.
The next function we will cover is kt_fork()
. It is going to
create
a stack and a state for the thread, which we will discuss later. It will then
set the func and arg fields to their approprate values,
choose a unique id for
the thread, set its state to RUNNABLE, and add it to the end of ktRunning.
The easiest function is kt_self()
. It simply returns its unique
thread id of ktRunning typecast to a (void *). The only reason it returns a (void *) is to hide
some of the innerworkings of the library from the user.
kt_sleep(int sec)
is pretty easy too. For this, you set the
state to
SLEEPING, calculate the wakup time
(time(NULL) + sec
), and insert it into ktSleeping keyed
on its wakeup time.
kt_join(void *ktid)
is simmilar. First off, check the thread at
ktid
exists. If it doesn't, then we figure that it has exited and we simply return.
Technically, this allows a caller to join with a thread id that has never
existed, but in order to keep track, we'd have to have a list of all valid
thread ids ever used. We'll leave this as a subtle point.
Next we need to see if there is a thread in ktBlocked keyed on the joinee's id. Each thread can only have one other thread waiting to join with it. Think about that for a minute. Thread A tries to join with Thread B and later Thread C tries to join with Thread B. What do you want to have happen? Your options are
If we make it this far, then we set ktid's joining field to point to ourself, set our state to BLOCKED, and add ourself to the blocked tree keyed on ktid's id.
With this in mind, kt_joinall()
is pretty simple. We do the same as
kt_join()
, but we treat it is if we were joining with a thread with
id 0 which we will never assign internally. The scheduler takes one last look
at the Blocked Queue before it decides to exit,
and if it sees a thread trying to join with 0, it wakes that thread as the
joinall thread. Again, at most one thread can call kt_joinall()
.
Multiple calls casue the program to exit.
Last, kt_exit()
is going to free up all of the data for the
thread
and simply run the scheduler again without putting the current thread back in the
list. I am glossing over the details of this because it is easier said
than done.
The only thing that it needs to do is check to see if it has a joiner. It check
ktBlocked to see if anyone is blocked on its id. If there is, we remove it, set the state
to RUNNABLE, and append it to ktRunnable.
kt_join()
and kt_joinall()
code. When they are created with the make_kt_sem()
call, our semaphores
are going to get their own unique ids. These are not going to overlap with the
kthread ids because we will block exactly the same way as above. We will also set
the semaphore up with an initial value when we create it.
The function P_kt_sem()
will decrement the value of the semaphore,
and if this value is less than zero, it will block the thread. To do this,
it will
set its state to BLOCKED and insert it into ktBlocked keyed on the id of the
semaphore.
Here is the code:
void P_kt_sem(kt_sem iks){ Ksem ks = (Ksem)iks; K_t me = ktRunning; InitKThreadSystem(); ks->val--; if(ks->val < 0) { /* * use the semaphore tid as the blocking key */ ktRunning->ks = ks; BlockKThread(ktRunning,ks->sid); KtSched(); ktRunning->ks = NULL; return; } return; }Again -- the code is fairly simple once you understand that
KtSched()
is doing all of the hard work associated with switching
between threads.
V_kt_sem()
increments the counter on the semaphore and checks to
see if the value is less than or equal to zero. If it is, it searches ktBlocked
for a thread keyed on its id, sets its state to RUNNABLE, and appends it to the
end of ktRunnable. Notice that there is no garuntee that the threads are going to
be ublocked in FIFO order. It would be hard to do, but we couldn't keep up the
nice, generic system we have. So it goes.
Here is the code:
void V_kt_sem(kt_sem iks) { Ksem ks = (Ksem)iks; K_t wake_kt; InitKThreadSystem(); ks->val++; if(ks->val <= 0) { wake_kt = jval_v(jrb_val(jrb_find_int(ktBlocked,ks->sid))); WakeKThread(wake_kt); } return; }Notice that it picks some thread off the list of threads blocked on the semaphore and wakes it up. The wake code is here:
void WakeKThread(K_t kt) { /* * look through the various blocked lists and try to wake the * specified thread */ if (kt->state == RUNNING || kt->state == RUNNABLE || kt->state == DEAD) return; jrb_delete_node(kt->blocked_list_ptr); kt->state = RUNNABLE; kt->blocked_list = NULL; kt->blocked_list_ptr = NULL; dll_append(ktRunnable,new_jval_v(kt)); return; }
Last is kill_kt_sem()
. There is not much to say about this except that
it checks to see if there are any threads blocked on the semaphore, and if there
are flags and error and exits.
setjmp()
and
longjmp()
very clearly before we can address these questions.
Unfortunately, we won't do so explicitly, but by the time you implemented
processes in your OS, you'll know enough to be able to work through the man
pages and the innerworking of ktSched()
.