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
cpustateandproc: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* pis on the run queue for CPUcif:pis part of the linked list headed atc->runq_, orpis running on CPUc.
Normally a single
procis 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
procmay 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* pto CPUc’s run queue, callc->enqueue(p). This checks thatphas never been scheduled before. It also setsp->runq_cpu_ = c->cpuindex_, markingcasp’s home CPU. -
RQI5b. To reschedule a
proc* pon 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. Aprocwith 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 newprochas a nonnullproc::regs_pointer. It is safe to access that member and modify its contents, but only up to the point that the newprocis 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, itsprocmust 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::scheduleis called [SUI3, SUI4].
Process state invariants
-
PSI1.
proc::pstate_indicates a task’s status. A task withpstate_ == proc::ps_runnableis called runnable; the handout scheduler only resumes runnable tasks. The valueproc::ps_blockedis 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_runnablein 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_blockedtoproc::ps_runnableby 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_runnabletoproc::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
exitinvolves 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_runnabletoproc::ps_blocked—are very difficult to get right. To manage such transitions, consider introducing otherprocmember 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 itsyieldstateor 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,pis not running anywhere. Onlypitself can changep->pstate_fromproc::ps_runnable, so we knowp->pstate_will remainproc::ps_runnableuntil 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 afterpis resumed.
- A task that is actively running may have a
Process table invariants
- PTABLEI1. Read or write access to the process table
ptablerequires theptable_lockspinlock.
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_ == 0toref_ > 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::loadfunction initially acquires thebufcache::lock_, then acquires a slot’sbcslot::lock_and releases the globalbufcache::lock_. -
The
bufcache::loadfunction, 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_ != nullptrandstate_ == s_cleanors_dirty. -
The contents of a
bcslot::buf_are protected bywrite_owner_, which is a mutual-exclusion lock implemented bybcslot::lock_buffer()andbcslot::unlock_buffer().