- Basis for user-level threads package
- Straight-threads implementation
- no interrupts
- everything in thread contexts
- one processor
Thread Control Block
Thread object includes space to hold
th thread's stack and the object also contains
- thread's context
- thread state
- id
- pointers for linking onto queues
- Current Thread
- Run Queue
- Mutex Queues
void thread_switch( ) {
thread_t NextThread, OldCurrent;
NextThread = dequeue(RunQueue);
OldCurrent = CurrentThread;
CurrentThread = NextThread;
swapcontext(&OldCurrent->context, &NextThread->context);
// We're now in the new thread's context
}
What if there are no runnable threads; i.e., what if the
RunQueue is empty?
The thread object includes the context which includes a pointer
to the thread's current stack frame.
- context
- stack
- stack frame
Mutex lock and unlock could be implemented for this basic system like
this:
Why isn't
void mutex_lock(mutex_t *m) {
if (m->locked) {
enqueue(m->queue, CurrentThread);
thread_switch();
} else
m->locked = 1;
}
void mutex_unlock(mutex_t *m) {
if (queue_empty(m->queue))
m->locked = 0;
else
enqueue(runqueue, dequeue(m->queue));
}
Don't we need to do something to make these operations
atomic?
How do we ensure that all processors are utilized?
What about synchronization?
Compare and swap instruction
This executes as the following, but is atomic!
int CAS(int *ptr, int old, int new) {
int tmp = *ptr;
if (*ptr == old)
*ptr = new
return tmp;
}
void spin_lock(int *mutex) {
while(!CAS(mutex, 0, 1))
;
}
void spin_unlock(int *mutex) {
*mutex = 0;
}
The atomic guarantee of CAS slows down processors. So some
checking is done without it first:
void spin_lock(int *mutex) {
while (1) {
if (*mutex == 0) {
// the mutex was at least momentarily unlocked
if (!CAS(mutex, 0, 1)
break; // we have locked the mutex
// some other thread beat us to it, so try again
}
}
}
For multiprocessors, spin locks should be restricted to
protecting very short code.
Spin locks are not useful for uniprocessors.
The following works on uniprocessors.
(This is closer to the project's uthread_mutex_t struct.)
void blocking_lock(mutex_t *mut) {
if (mut->holder != 0)
enqueue(mut->wait_queue, CurrentThread);
thread_switch();
} else
mut->holder = CurrentThread;
}
void blocking_unlock(mutex_t *mut) {
if (queue_empty(mut->wait_queue))
mut->holder = 0;
else {
mut->holder = dequeue(mut->wait_queue);
enqueue(RunQueue, mut->holder);
}
}
Does this work for multiprocessors?
void blocking_lock(mutex_t *mut) {
spin_lock(mut->spinlock);
if (mut->holder != 0)
enqueue(mut->wait_queue,
CurrentThread);
spin_unlock(mut->spinlock);
thread_switch();
} else {
mut->holder = CurrentThread;
spin_unlock(mut->spinlock);
}
}
void blocking_unlock(mutex_t *mut) {
spin_lock(mut->spinlock);
if (queue_empty(
mut->wait_queue)) {
mut->holder = 0;
else {
mut->holder =
dequeue(mut->wait_queue);
enqueue(RunQueue,
mut->holder);
}
spin_unlock(mut->spinlock);
}
Suppose
- Mutex locked, but its wait queue is empty
- Holder is unlocking it
- Another thread is trying to lock it.
Fast mutexes
- Safe, efficient kernel conditional queueing in Linux
- All operations performed atomically
-
futex_wait(futex_t *futex, int val)
- if futex->val is equal to val, then sleep
- otherwise return
-
futex_wake(futex_t *futex)
- wakes one waiting thread if there is any.
int atomic_inc(int *val)
- add 1 to *val, return its original value
int atomic_dec(int *val)
- subtract 1 from *val, return its original value
void lock(futex_t *futex) {
int c;
while ((c = atomic_inc(&futex->val)) != 0)
futex_wait(futex, c+1);
}
void unlock(futex_t *futex) {
futex->val = 0;
futex_wake(futex);
}
void lock(futex_t *futex) {
int c;
if ((c = CAS(&futex->val, 0, 1) != 0)
do {
if (c == 2 || (CAS(&futex->val, 1, 2) != 1))
futex_wait(futex, 2);
while ((c = CAS(&futex->val, 0, 2)) != 0))
}
void unlock(futex_t *futex) {
if (atomic_dec(&futex->val) != 1) {
futex->val = 0;
futex_wake(futex);
}
}
Basic problem:
Interrupts are asynchronous to the interrupted thread.
Interrupt handlers don't have control blocks for saving their
state.
So switching to another interrupt handler or to another thread
and later continuing an interrupt handler doesn't work.
That is, thread_switch (or an analogous interrupt_switch)
doesn't work.
- Non-preemptive kernels
-
- threads running in privileged mode yield the processor only
voluntarily
- Preemptive kernels
- threads running in privileged mode may be forced to yield the processor
int x;
void AccessXThread()
{
int oldIPL;
oldIPL = setIPL(IHLevel);
x = x + 1;
setIPL(oldIPL);
}
void AccessXInterrupt()
{
...
x = x + 1;
...
}
For preemptive kernel, synchronization is more complicated.
Interrupt handlers run with interrupts masked
both when executed in interrupt context or thread context
may interfere with handling of other interrupts
Solution
do minimal work now
do rest later without interrupts masked
void TopLevelInterruptHandler(int dev) {
InterruptVector[dev](); // call appropriate handler
if (PreviousContext == ThreadContext) {
UnMaskInterrupts();
while(!Empty(WorkQueue)) {
Work = DeQueue(WorkQueue);
Work();
}
}
}
void NetworkInterruptHandler() {
// deal with interrupt
EnQueue(WorkQueue, MoreWork);
}
We'll come back to this, but for now look at signal handling
which is simpler but has some of the same characteristics.
Basic idea is to set up the user stack so that the handler is
called as a subroutine and so that when it returns, normal
execution of the thread may continue.
Complications:
- saving and restoring registers
- signal mask
Signal handling semantics requires that
the signal that caused handler to execute is masked while in
the handler and unmasked when the handler returns.
Problems 4, 5, 7, 11a, and 11b
Does this implement mutexes correctly? Why or why not?
kmutex_lock(mutex_t *mut) {
if (mut->locked) {
enqueue(mut->wait_queue, CurrentThread);
thread_switch();
}
mut->locked = 1;
}
kmutex_unlock(mutex_t *mut) {
mut->locked = 0;
if (!queue_empty(mut->wait_queue))
enqueue(RunQueue, dequeue(mut->wait_queue));
}
Assuming that all threads are either runnable or waiting on
a mutex what can you say if there are no threads on the run
queue with this version of thread_switch?
void thread_switch( ) {
thread_t NextThread, OldCurrent;
NextThread = dequeue(RunQueue);
OldCurrent = CurrentThread;
CurrentThread = NextThread;
swapcontext(&OldCurrent->context, &NextThread->context);
// We're now in the new thread's context
}
Explain why the following implementation the semaphore P and V
operations in terms of futexes doesn't work.
void P(futex_t *f) {
/* f->val contains the value of the semaphore */
int v;
while (1) {
while ((v = f->val) > 0) {
/* semaphore is positive */
if (CAS(&f->val, v, v-1) == v) {
/* we were able to decrement it without
interference */
return;
}
/* try again while semaphore is still positive
}
/* semaphore is zero: wait for a V operation
futex_wait(f, 0);
}
}
void V(futex_t *f) {
if (atomic_inc(&f->val) == 0) {
/* semaphore was zero: there might be
waiting threads */
futex_wake(f);
}
return;
}