7.3 Code: Scheduling
The last section looked at the low-level details of
swtch
;
now let’s take
swtch
as a given and examine
switching from one process’s kernel thread
through the scheduler to another process.
The scheduler exists in the form of a special thread per CPU, each running the
scheduler
function.
This function is in charge of choosing which process to run next.
A process
that wants to give up the CPU must
acquire its own process lock
p->lock
,
release any other locks it is holding,
update its own state
(p->state
),
and then call
sched
.
You can see this sequence in
yield
(kernel/proc.c:512),
sleep
and
exit.
sched
double-checks some of those requirements
(kernel/proc.c:496-501)
and then checks an implication:
since a lock is held, interrupts should be disabled.
Finally,
sched
calls
swtch
to save the current context in
p->context
and switch to the scheduler context in
cpu->context
.
swtch
returns on the scheduler’s stack
as though
scheduler
’s
swtch
had returned
(kernel/proc.c:466).
The scheduler continues its
for
loop, finds a process to run,
switches to it, and the cycle repeats.
We just saw that xv6 holds
p->lock
across calls to
swtch
:
the caller of
swtch
must already hold the lock, and control of the lock passes to the
switched-to code. This arrangement is unusual: it’s more common for
the thread that acquires a lock to also release it.
Xv6’s context switching must break this convention because
p->lock
protects invariants on the process’s
state
and
context
fields that are not true while executing in
swtch
.
For example, if
p->lock
were not held during
swtch
,
a different CPU might decide
to run the process after
yield
had set its state to
RUNNABLE
,
but before
swtch
caused it to stop using its own kernel stack.
The result would be two CPUs running on the same stack,
which would cause chaos.
Once yield
has started to modify a running process’s state
to make it
RUNNABLE
,
p->lock
must remain held until the invariants are restored:
the earliest correct release point is after
scheduler
(running on its own stack)
clears
c->proc
.
Similarly, once
scheduler
starts to convert a RUNNABLE
process to
RUNNING
,
the lock cannot be released until the process’s kernel thread
is completely running (after the
swtch
,
for example in
yield
).
The only place a kernel thread gives up its CPU is in
sched
,
and it always switches to the same location in scheduler
, which
(almost) always switches to some kernel thread that previously called
sched
.
Thus, if one were to print out the line numbers where xv6 switches
threads, one would observe the following simple pattern:
(kernel/proc.c:466),
(kernel/proc.c:506),
(kernel/proc.c:466),
(kernel/proc.c:506),
and so on.
Procedures that intentionally transfer control to each other via thread
switch are sometimes referred to as
coroutines;
in this example,
sched
and
scheduler
are co-routines of each other.
There is one case when the scheduler’s call to
swtch
does not end up in
sched
.
allocproc
sets the context ra
register of a new process to
forkret
(kernel/proc.c:524),
so that its first swtch
“returns”
to the start of that function.
forkret
exists to release the
p->lock
;
otherwise, since the new process needs to
return to user space as if returning from fork
,
it could instead start at
usertrapret
.
scheduler
(kernel/proc.c:445)
runs a loop:
find a process to run, run it until it yields, repeat.
The scheduler
loops over the process table
looking for a runnable process, one that has
p->state
==
RUNNABLE
.
Once it finds a process, it sets the per-CPU current process
variable
c->proc
,
marks the process as
RUNNING
,
and then calls
swtch
to start running it
(kernel/proc.c:461-466).