DO NOT MAIL: xv6 web pages
This commit is contained in:
parent
ee3f75f229
commit
f53494c28e
37 changed files with 9034 additions and 0 deletions
354
web/l-coordination.html
Normal file
354
web/l-coordination.html
Normal file
|
@ -0,0 +1,354 @@
|
|||
<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?
|
Loading…
Add table
Add a link
Reference in a new issue