355 lines
8.1 KiB
HTML
355 lines
8.1 KiB
HTML
|
<title>L9</title>
|
||
|
<html>
|
||
|
<head>
|
||
|
</head>
|
||
|
<body>
|
||
|
|
||
|
<h1>Coordination and more processes</h1>
|
||
|
|
||
|
<p>Required reading: remainder of proc.c, sys_exec, sys_sbrk,
|
||
|
sys_wait, sys_exit, and sys_kill.
|
||
|
|
||
|
<h2>Overview</h2>
|
||
|
|
||
|
<p>Big picture: more programs than processors. How to share the
|
||
|
limited number of processors among the programs? Last lecture
|
||
|
covered basic mechanism: threads and the distinction between process
|
||
|
and thread. Today expand: how to coordinate the interactions
|
||
|
between threads explicitly, and some operations on processes.
|
||
|
|
||
|
<p>Sequence coordination. This is a diferrent type of coordination
|
||
|
than mutual-exclusion coordination (which has its goal to make
|
||
|
atomic actions so that threads don't interfere). The goal of
|
||
|
sequence coordination is for threads to coordinate the sequences in
|
||
|
which they run.
|
||
|
|
||
|
<p>For example, a thread may want to wait until another thread
|
||
|
terminates. One way to do so is to have the thread run periodically,
|
||
|
let it check if the other thread terminated, and if not give up the
|
||
|
processor again. This is wasteful, especially if there are many
|
||
|
threads.
|
||
|
|
||
|
<p>With primitives for sequence coordination one can do better. The
|
||
|
thread could tell the thread manager that it is waiting for an event
|
||
|
(e.g., another thread terminating). When the other thread
|
||
|
terminates, it explicitly wakes up the waiting thread. This is more
|
||
|
work for the programmer, but more efficient.
|
||
|
|
||
|
<p>Sequence coordination often interacts with mutual-exclusion
|
||
|
coordination, as we will see below.
|
||
|
|
||
|
<p>The operating system literature has a rich set of primivites for
|
||
|
sequence coordination. We study a very simple version of condition
|
||
|
variables in xv6: sleep and wakeup, with a single lock.
|
||
|
|
||
|
<h2>xv6 code examples</h2>
|
||
|
|
||
|
<h3>Sleep and wakeup - usage</h3>
|
||
|
|
||
|
Let's consider implementing a producer/consumer queue
|
||
|
(like a pipe) that can be used to hold a single non-null char pointer:
|
||
|
|
||
|
<pre>
|
||
|
struct pcq {
|
||
|
void *ptr;
|
||
|
};
|
||
|
|
||
|
void*
|
||
|
pcqread(struct pcq *q)
|
||
|
{
|
||
|
void *p;
|
||
|
|
||
|
while((p = q->ptr) == 0)
|
||
|
;
|
||
|
q->ptr = 0;
|
||
|
return p;
|
||
|
}
|
||
|
|
||
|
void
|
||
|
pcqwrite(struct pcq *q, void *p)
|
||
|
{
|
||
|
while(q->ptr != 0)
|
||
|
;
|
||
|
q->ptr = p;
|
||
|
}
|
||
|
</pre>
|
||
|
|
||
|
<p>Easy and correct, at least assuming there is at most one
|
||
|
reader and at most one writer at a time.
|
||
|
|
||
|
<p>Unfortunately, the while loops are inefficient.
|
||
|
Instead of polling, it would be great if there were
|
||
|
primitives saying ``wait for some event to happen''
|
||
|
and ``this event happened''.
|
||
|
That's what sleep and wakeup do.
|
||
|
|
||
|
<p>Second try:
|
||
|
|
||
|
<pre>
|
||
|
void*
|
||
|
pcqread(struct pcq *q)
|
||
|
{
|
||
|
void *p;
|
||
|
|
||
|
if(q->ptr == 0)
|
||
|
sleep(q);
|
||
|
p = q->ptr;
|
||
|
q->ptr = 0;
|
||
|
wakeup(q); /* wake pcqwrite */
|
||
|
return p;
|
||
|
}
|
||
|
|
||
|
void
|
||
|
pcqwrite(struct pcq *q, void *p)
|
||
|
{
|
||
|
if(q->ptr != 0)
|
||
|
sleep(q);
|
||
|
q->ptr = p;
|
||
|
wakeup(q); /* wake pcqread */
|
||
|
return p;
|
||
|
}
|
||
|
</pre>
|
||
|
|
||
|
That's better, but there is still a problem.
|
||
|
What if the wakeup happens between the check in the if
|
||
|
and the call to sleep?
|
||
|
|
||
|
<p>Add locks:
|
||
|
|
||
|
<pre>
|
||
|
struct pcq {
|
||
|
void *ptr;
|
||
|
struct spinlock lock;
|
||
|
};
|
||
|
|
||
|
void*
|
||
|
pcqread(struct pcq *q)
|
||
|
{
|
||
|
void *p;
|
||
|
|
||
|
acquire(&q->lock);
|
||
|
if(q->ptr == 0)
|
||
|
sleep(q, &q->lock);
|
||
|
p = q->ptr;
|
||
|
q->ptr = 0;
|
||
|
wakeup(q); /* wake pcqwrite */
|
||
|
release(&q->lock);
|
||
|
return p;
|
||
|
}
|
||
|
|
||
|
void
|
||
|
pcqwrite(struct pcq *q, void *p)
|
||
|
{
|
||
|
acquire(&q->lock);
|
||
|
if(q->ptr != 0)
|
||
|
sleep(q, &q->lock);
|
||
|
q->ptr = p;
|
||
|
wakeup(q); /* wake pcqread */
|
||
|
release(&q->lock);
|
||
|
return p;
|
||
|
}
|
||
|
</pre>
|
||
|
|
||
|
This is okay, and now safer for multiple readers and writers,
|
||
|
except that wakeup wakes up everyone who is asleep on chan,
|
||
|
not just one guy.
|
||
|
So some of the guys who wake up from sleep might not
|
||
|
be cleared to read or write from the queue. Have to go back to looping:
|
||
|
|
||
|
<pre>
|
||
|
struct pcq {
|
||
|
void *ptr;
|
||
|
struct spinlock lock;
|
||
|
};
|
||
|
|
||
|
void*
|
||
|
pcqread(struct pcq *q)
|
||
|
{
|
||
|
void *p;
|
||
|
|
||
|
acquire(&q->lock);
|
||
|
while(q->ptr == 0)
|
||
|
sleep(q, &q->lock);
|
||
|
p = q->ptr;
|
||
|
q->ptr = 0;
|
||
|
wakeup(q); /* wake pcqwrite */
|
||
|
release(&q->lock);
|
||
|
return p;
|
||
|
}
|
||
|
|
||
|
void
|
||
|
pcqwrite(struct pcq *q, void *p)
|
||
|
{
|
||
|
acquire(&q->lock);
|
||
|
while(q->ptr != 0)
|
||
|
sleep(q, &q->lock);
|
||
|
q->ptr = p;
|
||
|
wakeup(q); /* wake pcqread */
|
||
|
release(&q->lock);
|
||
|
return p;
|
||
|
}
|
||
|
</pre>
|
||
|
|
||
|
The difference between this an our original is that
|
||
|
the body of the while loop is a much more efficient way to pause.
|
||
|
|
||
|
<p>Now we've figured out how to use it, but we
|
||
|
still need to figure out how to implement it.
|
||
|
|
||
|
<h3>Sleep and wakeup - implementation</h3>
|
||
|
<p>
|
||
|
Simple implementation:
|
||
|
|
||
|
<pre>
|
||
|
void
|
||
|
sleep(void *chan, struct spinlock *lk)
|
||
|
{
|
||
|
struct proc *p = curproc[cpu()];
|
||
|
|
||
|
release(lk);
|
||
|
p->chan = chan;
|
||
|
p->state = SLEEPING;
|
||
|
sched();
|
||
|
}
|
||
|
|
||
|
void
|
||
|
wakeup(void *chan)
|
||
|
{
|
||
|
for(each proc p) {
|
||
|
if(p->state == SLEEPING && p->chan == chan)
|
||
|
p->state = RUNNABLE;
|
||
|
}
|
||
|
}
|
||
|
</pre>
|
||
|
|
||
|
<p>What's wrong? What if the wakeup runs right after
|
||
|
the release(lk) in sleep?
|
||
|
It still misses the sleep.
|
||
|
|
||
|
<p>Move the lock down:
|
||
|
<pre>
|
||
|
void
|
||
|
sleep(void *chan, struct spinlock *lk)
|
||
|
{
|
||
|
struct proc *p = curproc[cpu()];
|
||
|
|
||
|
p->chan = chan;
|
||
|
p->state = SLEEPING;
|
||
|
release(lk);
|
||
|
sched();
|
||
|
}
|
||
|
|
||
|
void
|
||
|
wakeup(void *chan)
|
||
|
{
|
||
|
for(each proc p) {
|
||
|
if(p->state == SLEEPING && p->chan == chan)
|
||
|
p->state = RUNNABLE;
|
||
|
}
|
||
|
}
|
||
|
</pre>
|
||
|
|
||
|
<p>This almost works. Recall from last lecture that we also need
|
||
|
to acquire the proc_table_lock before calling sched, to
|
||
|
protect p->jmpbuf.
|
||
|
|
||
|
<pre>
|
||
|
void
|
||
|
sleep(void *chan, struct spinlock *lk)
|
||
|
{
|
||
|
struct proc *p = curproc[cpu()];
|
||
|
|
||
|
p->chan = chan;
|
||
|
p->state = SLEEPING;
|
||
|
acquire(&proc_table_lock);
|
||
|
release(lk);
|
||
|
sched();
|
||
|
}
|
||
|
</pre>
|
||
|
|
||
|
<p>The problem is that now we're using lk to protect
|
||
|
access to the p->chan and p->state variables
|
||
|
but other routines besides sleep and wakeup
|
||
|
(in particular, proc_kill) will need to use them and won't
|
||
|
know which lock protects them.
|
||
|
So instead of protecting them with lk, let's use proc_table_lock:
|
||
|
|
||
|
<pre>
|
||
|
void
|
||
|
sleep(void *chan, struct spinlock *lk)
|
||
|
{
|
||
|
struct proc *p = curproc[cpu()];
|
||
|
|
||
|
acquire(&proc_table_lock);
|
||
|
release(lk);
|
||
|
p->chan = chan;
|
||
|
p->state = SLEEPING;
|
||
|
sched();
|
||
|
}
|
||
|
void
|
||
|
wakeup(void *chan)
|
||
|
{
|
||
|
acquire(&proc_table_lock);
|
||
|
for(each proc p) {
|
||
|
if(p->state == SLEEPING && p->chan == chan)
|
||
|
p->state = RUNNABLE;
|
||
|
}
|
||
|
release(&proc_table_lock);
|
||
|
}
|
||
|
</pre>
|
||
|
|
||
|
<p>One could probably make things work with lk as above,
|
||
|
but the relationship between data and locks would be
|
||
|
more complicated with no real benefit. Xv6 takes the easy way out
|
||
|
and says that elements in the proc structure are always protected
|
||
|
by proc_table_lock.
|
||
|
|
||
|
<h3>Use example: exit and wait</h3>
|
||
|
|
||
|
<p>If proc_wait decides there are children to be waited for,
|
||
|
it calls sleep at line 2462.
|
||
|
When a process exits, we proc_exit scans the process table
|
||
|
to find the parent and wakes it at 2408.
|
||
|
|
||
|
<p>Which lock protects sleep and wakeup from missing each other?
|
||
|
Proc_table_lock. Have to tweak sleep again to avoid double-acquire:
|
||
|
|
||
|
<pre>
|
||
|
if(lk != &proc_table_lock) {
|
||
|
acquire(&proc_table_lock);
|
||
|
release(lk);
|
||
|
}
|
||
|
</pre>
|
||
|
|
||
|
<h3>New feature: kill</h3>
|
||
|
|
||
|
<p>Proc_kill marks a process as killed (line 2371).
|
||
|
When the process finally exits the kernel to user space,
|
||
|
or if a clock interrupt happens while it is in user space,
|
||
|
it will be destroyed (line 2886, 2890, 2912).
|
||
|
|
||
|
<p>Why wait until the process ends up in user space?
|
||
|
|
||
|
<p>What if the process is stuck in sleep? It might take a long
|
||
|
time to get back to user space.
|
||
|
Don't want to have to wait for it, so make sleep wake up early
|
||
|
(line 2373).
|
||
|
|
||
|
<p>This means all callers of sleep should check
|
||
|
whether they have been killed, but none do.
|
||
|
Bug in xv6.
|
||
|
|
||
|
<h3>System call handlers</h3>
|
||
|
|
||
|
<p>Sheet 32
|
||
|
|
||
|
<p>Fork: discussed copyproc in earlier lectures.
|
||
|
Sys_fork (line 3218) just calls copyproc
|
||
|
and marks the new proc runnable.
|
||
|
Does fork create a new process or a new thread?
|
||
|
Is there any shared context?
|
||
|
|
||
|
<p>Exec: we'll talk about exec later, when we talk about file systems.
|
||
|
|
||
|
<p>Sbrk: Saw growproc earlier. Why setupsegs before returning?
|