This page lists synchronization invariants that hold in Chickadee. See also spinlocks.
Run queue invariants
-
RQI1. A CPU’s run queue is implemented by members in
cpustate
andproc
:cpustate::current_
, the kernel task currently running on a CPU.cpustate::runq_
, the head of the linked list of tasks waiting to run on a CPU.proc::runq_links_
, the links used to connectcpustate::runq_
.proc::runq_cpu_
, the index of the CPU on which a task should run.cpustate::runq_lock_
, a per-CPU spinlock.
-
RQI2. A
proc* p
is on the run queue for CPUc
if:p
is part of the linked list headed atc->runq_
, orp
is running on CPUc
.
Normally a single
proc
is either running or scheduled, but a process may be both running and scheduled simultaneously. (So it is possible thatp == c->current_ && p->runq_links_.linked()
.) -
RQI3. A
proc
may be on at most one CPU’s run queue at a time. This CPU is called its home CPU.proc::runq_cpu_
is the index of the home CPU. -
RQI4.
cpustate::current_
holds the currently-running kernel task on a CPU.c->current_
may only be modified byc->schedule()
, thecpustate::schedule()
function for that same CPU, while it holdsc->runq_lock_
.- RQI4a. Because modifications to
c->current_
are limited, a kernel task may safely examine%gs:(8)
, which holdsc->current_
for the CPU currently running that task. (The kernel task is running on CPUc
, soc->schedule()
is not, guaranteeing thatc->current_
will not be written concurrently.) However, kernel tasks must not examine other CPUs’current_
members without acquiring the relevantrunq_lock_
s.
- RQI4a. Because modifications to
-
RQI5.
cpustate::runq_
andproc::runq_links_
form the linked list of runnable tasks.-
RQI5a. To add a new
proc* p
to CPUc
’s run queue, callc->enqueue(p)
. This checks thatp
has never been scheduled before. It also setsp->runq_cpu_ = c->cpuindex_
, markingc
asp
’s home CPU. -
RQI5b. To reschedule a
proc* p
on its home CPU’s run queue, callc->reenqueue(p)
. This checks, among other things, thatp->runq_cpu_ == c->cpuindex_
. -
RQI5c.
c->runq_lock_
protectsc->runq_
and everyrunq_links_
associated with that CPU. Bothcpustate::enqueue()
andcpustate::reenqueue()
acquirec->runq_lock_
.
-
-
RQI6. In Chickadee handout code,
proc::runq_cpu_
never changes after it is initialized bycpustate::enqueue()
.proc::unblock()
usesproc::runq_cpu_
to find a task’s home CPU; it must readsrunq_cpu_
before acquiring any lock (becauserunq_cpu_
is needed to find the relevantcpustate::runq_lock_
to lock). When an object is accessed from multiple CPUs without locks, the First Law of Synchronization says all such accesses must be reads. Enabling process migration among CPUs would require modifyingproc::runq_cpu_
, and therefore additional synchronization objects to protect accesses to it.
Suspension invariants
-
SUI1. The
proc::regs_
andproc::yields_
members are used to store resumption state. At most one of these members can be nonnull at a time. Aproc
with nonnullregs_
oryields_
is called resumable. -
SUI2. A nonnull
proc::regs_
orproc::yields_
must point into the kernel task stack containing theproc
(so their address must be on the same memory page as theproc
). -
SUI3. The
proc::regs_
member may be accessed only in one of the following ways:-
A running kernel task may modify its
proc::regs_
if interrupts are disabled and remain disabled until the next call tocpustate::schedule()
viaproc::yield_noreturn()
. -
After
proc::init_user()
is called, the newproc
has a nonnullproc::regs_
pointer. It is safe to access that member and modify its contents, but only up to the point that the newproc
is scheduled bycpustate::enqueue()
[RQI5a]. -
The
cpustate::schedule()
function may access and clearproc::regs_
by callingproc::resume()
.
-
-
SUI4. The
proc::yields_
member may be accessed only in one of the following ways:-
A running kernel task may modify its
proc::yields_
(asproc::yield()
does) if interrupts are disabled and remain disabled until the next call tocpustate::schedule()
viaproc::yield_noreturn()
. -
The
cpustate::schedule()
function may access and clearproc::yields_
by callingproc::resume()
.
-
-
SUI5. The
proc::resume()
function can only be called bycpustate::schedule()
. -
SUI6. The
proc::yield()
function must be called with no spinlocks held. -
SUI7. The
proc::yield_noreturn()
function must be called with no spinlocks held and with interrupts disabled. Furthermore, itsproc
must be either resumable or non-runnable.
Scheduling invariants
-
SCI1. The
cpustate::schedule()
function must be called on the current CPU’s CPU stack, with no spinlocks held, and with interrupts disabled. (Theproc::yield_noreturn()
function switches to the CPU stack.) -
SCI2. A task (that is, a
proc
) can run on at most one CPU at a time.For user tasks, the “task” encompasses both privileged and unprivileged modes of execution: it is illegal for CPU 0 to execute a particular thread in unprivileged mode while CPU 1 executes on the corresponding kernel task stack in privileged mode. This is because the unprivileged code could trap at any time, which would cause two CPUs to simultaneously execute the same kernel task.
-
SCI3. A CPU should not run in kernel mode with interrupts disabled indefinitely. To prevent yielding processes from blocking out interrupts,
proc::yield()
may enable interrupts for a single instruction before transferring control tocpustate::schedule()
.Note that
proc::yield_noreturn()
does not enable interrupts in the same way. This is becauseproc::yield_noreturn()
may be called on a resumable task [SUI7]; and if a resumable task is executing on a CPU, the relevant CPU must have interrupts disabled untilcpustate::schedule
is called [SUI3, SUI4].
Process state invariants
-
PSI1.
proc::pstate_
indicates a task’s status. A task withpstate_ == proc::ps_runnable
is called runnable; the handout scheduler only resumes runnable tasks. The valueproc::ps_blocked
is used for tasks that may run in the future, but are currently blocked. You may add other values. -
PSI2.
proc::pstate_
has atomic type, so it may be examined at any time without causing undefined behavior. Many contexts, including the memviewer andcpustate::schedule()
, examineproc::pstate_
. -
PSI3.
proc::pstate_
may be changed to and fromproc::ps_runnable
in the following ways.-
A kernel task may change its own
pstate_
to any value. -
Any context may change any task’s
p->pstate_
fromproc::ps_blocked
toproc::ps_runnable
by callingp->unblock()
.
proc::unblock()
uses an atomic compare-exchange operation. This is because the task itself might be running on another CPU and in the process of changingp->pstate_
to another value, such asproc::ps_faulted
. In such cases, the task’s own choice of state should win.A task marks itself as non-runnable (that is, changes
p->pstate_
fromproc::ps_runnable
toproc::ps_blocked
, or another value) as it prepares to block. Generally, a call toproc::yield()
orproc::yield_noreturn()
quickly follows the change ofpstate_
. -
-
PSI4. A task may change its
pstate_
while interrupts are enabled, but this requires care. If a task is non-runnable when an interrupt occurs, then the task will not be resumed after the interrupt. Thus, a task preparing to block must place itself on a wait queue, or otherwise make itself available for later waking, either (1) while interrupts are disabled, or (2) before changing itspstate_
. -
PSI5. You may introduce more states; for instance, our handling of
exit
involves new states and state transitions. But be careful! A kernel context should change a different task’s state only by calling itsproc::unblock()
. Other state transitions—for instance, changing another task’s state fromproc::ps_runnable
toproc::ps_blocked
—are very difficult to get right. To manage such transitions, consider introducing otherproc
member variables (with their own synchronization invariants) that signal a kernel task should change its own state. -
PSI6. Tasks are generally unblocked via wait queues and
wait_queue::notify_all()
. However, a task may be unblocked directly, even if it is enqueued on some wait queue, by callingp->unblock()
. This makes the task runnable without changing its wait queue status. Your wait queue implementation must handle such tasks. -
PSI7. Only runnable tasks may be resumed. A task that is not runnable must not be resumed. (The
proc::resume()
method resumes a task by loading either itsyieldstate
or itsregstate
. Afterproc::resume()
, either the kernel task starts running on its kernel task stack, or the corresponding user context starts running on its stack.proc::resume()
is called in exactly one place in the kernel, namelycpustate::schedule()
.)- A task that is actively running may have a
pstate_
other thanproc::ps_runnable
. This happens, for example, when a process takes a page fault, or as a kernel task decides whether or not to block. However,cpustate::schedule()
must not resume any task whosepstate_
does not equalproc::ps_runnable
.
Here’s why the handout code obeys this invariant.
-
While
cpustate::schedule()
chooses a task to resume, it holds itsrunq_lock_
. This protects tasks in its run queue from being removed from its queue or rescheduled on other CPUs. -
cpustate::schedule()
chooses a task withp->pstate_ == proc::ps_runnable
. Since this task is on the current CPU’s run queue, which is locked, we know the task is not currently running on any other CPU. It is not running on this CPU either (becausecpustate::schedule()
is running). Therefore,p
is not running anywhere. Onlyp
itself can changep->pstate_
fromproc::ps_runnable
, so we knowp->pstate_
will remainproc::ps_runnable
until therunq_lock_
is released. -
cpustate::schedule()
releases itsrunq_lock_
before resuming the chosen process. However, it setscpustate::current_
to the chosen process before releasing the lock. This prevents any other CPU from runningp
, and ensures thatp->pstate_
will not change until afterp
is resumed.
- A task that is actively running may have a
Process table invariants
- PTABLEI1. Read or write access to the process table
ptable
requires theptable_lock
spinlock.
Page table invariants
-
PGI1. You may free a process’s page table pages only if (1) the process is not present in
ptable
, or (2) the process’s page table lock is locked for writing (forcingproc::lock_pagetable_read()
to spin).Note that in our handout code,
proc::lock_pagetable_read()
does nothing. We provide it as a hook for you to complete, in case you want to implement finer-grained page table management.
Buffer cache invariants (Pset 4)
The buffer cache, bufcache
, comprises a set of slots. The bufcache has a
lock, lock_
. Each slot has type bcslot
, and contains a state state_
, a
lock lock_
, a disk block number bn_
, a reference count ref_
, a write
owner write_owner_
(initially nullptr
), and a pointer to the cached data
buf_
. These data structures have nontrivial synchronization invariants
because disks are slow, making fine-grained synchronization important for
performance. As always, you can change these invariants, but understand the
current invariants first.
-
The
bcslot::ref_
andbcslot::state_
members are atomic and can be read without locks. -
Changing a
bcslot::state_
member requires the slot’sbcslot::lock_
. -
Allocating a slot requires both the global buffer cache lock,
bufcache::lock_
, and the relevant slot’slock_
.- Thus, if a task holds
bufcache::lock_
, it can be sure that no other task will cause a slot to transition fromref_ == 0
toref_ > 0
.
- Thus, if a task holds
-
Dereferencing a slot (
--slot->ref_
) can happen at any time and requires no locks. -
Loaded slots have read-only
bcslot::bn_
andbcslot::buf_
members. If a slot is referenced, itsbn_
andbuf_
can be read without locking.-
Once a slot has been allocated, its
bn_
does not change. -
Once a slot is loaded, its
buf_
does not change. -
All nonempty slots have distinct
bn_
s. -
All loaded slots have distinct, nonnull
buf_
s, because each block is cached in different memory. -
Allocating or freeing a slot may change its
bn_
andbuf_
. These operations require the slot’slock_
, and require thatref_ == 0
.
-
-
bufcache::lock_
precedes allbcslot::lock_
s in the lock order. For example, thebufcache::load
function initially acquires thebufcache::lock_
, then acquires a slot’sbcslot::lock_
and releases the globalbufcache::lock_
. -
The
bufcache::load
function, which is the only way to obtain a new slot reference, yields until the relevant slot is fully loaded. Thus, if a kernel task holds a slot reference, that slot hasbuf_ != nullptr
andstate_ == s_clean
ors_dirty
. -
The contents of a
bcslot::buf_
are protected bywrite_owner_
, which is a mutual-exclusion lock implemented bybcslot::lock_buffer()
andbcslot::unlock_buffer()
.