IPC
This commit is contained in:
+39
-4
@@ -67,21 +67,56 @@ exist, which is what a real-time scheduler needs.
|
||||
- **Round-robin within a level.** When a task is descheduled it goes to the *back*
|
||||
of its level's queue, so equal-priority tasks share the CPU fairly.
|
||||
|
||||
## Sleeping and the idle task
|
||||
|
||||
A task can **block** — give up the CPU until an event, rather than busy-wait
|
||||
(busy-waiting is the enemy of a real-time system: it wastes cycles a
|
||||
higher-priority task should get). The first form is time-based: **`sleep(ms)`**
|
||||
marks the task blocked with a wake deadline and switches away. On every tick the
|
||||
timer wakes any task whose deadline has passed (a bounded scan, so it stays
|
||||
deterministic), which makes it ready again; the scheduler then runs it when its
|
||||
priority comes up. `sleep` measures its deadline on the [calibrated
|
||||
clock](device-interrupts.md), so it's real time.
|
||||
|
||||
When *every* task is blocked, something still has to run — so there's an **idle
|
||||
task** at the lowest priority that just `hlt`s until the next interrupt (see
|
||||
[halting.md](halting.md)). Because it's always runnable, the scheduler always has a
|
||||
task to pick, and the "nothing to run" case never arises.
|
||||
|
||||
## Event-based blocking
|
||||
|
||||
The other form of blocking is waiting for an **event** rather than a duration. A
|
||||
**wait queue** is a set of tasks parked until something happens: `wait(wq)` blocks
|
||||
the caller on it, `wake(wq)` moves the highest-priority waiter back to ready
|
||||
(preempting if it now outranks the running task). A task links into a wait queue
|
||||
through the same field the ready queues use — it's in exactly one queue at a time.
|
||||
These are the primitives locks, semaphores and [IPC](ipc.md) are built on.
|
||||
|
||||
Blocking safely needs **composable critical sections**. A blanket `cli`/`sti` pair
|
||||
doesn't nest: an IPC channel that `cli`s and then calls `wait` would have `wait`'s
|
||||
`sti` re-enable interrupts too early, mid-operation. So the blocking primitives use
|
||||
`saveInterrupts` / `restoreInterrupts` — capture the interrupt flag, disable, and
|
||||
later restore *only if it was set* — which nests correctly. The invariant that
|
||||
makes it all work: `schedule()` is always entered with interrupts disabled, so a
|
||||
task always resumes from a switch with interrupts disabled and can restore its
|
||||
caller's state.
|
||||
|
||||
## Verifying it
|
||||
|
||||
Two tests (see [testing.md](testing.md)) prove the two guarantees:
|
||||
Three tests (see [testing.md](testing.md)) prove the guarantees:
|
||||
|
||||
- **`sched`** spawns three tasks that busy-loop *without ever yielding*. They all
|
||||
make progress — which can only happen if the timer is **preempting** between them
|
||||
and the context switch is correct (nothing yields voluntarily).
|
||||
- **`priority`** (with preemption off, for determinism) spawns tasks at three
|
||||
priorities; they run and exit **highest-priority first** — `[6, 4, 2]`.
|
||||
- **`sleep`** blocks a task for 50 ms and checks the elapsed time on the clock — a
|
||||
real block (the idle task runs meanwhile), not a busy-wait.
|
||||
- **`event`** blocks a task on a wait queue; waking it (from another task) resumes
|
||||
it, and since it's higher priority it preempts immediately.
|
||||
|
||||
## What's next (not done here)
|
||||
|
||||
- **Blocking and sleep.** Right now a task can only yield or exit; it can't wait for
|
||||
a condition or a duration. `sleep(ms)` (on the calibrated clock) and blocking
|
||||
come next, and are what a real-time task really needs.
|
||||
- **Priority inheritance.** Once tasks block on shared resources (locks, IPC),
|
||||
danos will need it to bound priority inversion — a [real-time](vision.md)
|
||||
requirement.
|
||||
|
||||
Reference in New Issue
Block a user