Module lilos::exec [−][src]
A system for polling an array of tasks forever, plus Notify
and other
scheduling tools.
Note: for our purposes, a task is an independent top-level future managed by the scheduler polling loop. There are a fixed set of tasks, provided to the scheduler at startup. This is distinct from the casual use of “task” to mean a piece of code that runs concurrently with other code; we’ll use the term “concurrent process” for this. The fixed set of tasks managed by the scheduler can execute an arbitrary number of concurrent processes.
Scheduler entry point
The mechanism for “starting the OS” is run_tasks
.
Time
The executor uses the timekeeping provided by the time
module to enable tasks to be woken at particular times. sleep_until
produces a future that resolves at a particular time, while sleep_for
expresses the time relative to the current time.
As their names imply, these functions can be used to delay the current task
– but they can also be used to impose a timeout on any async operation, by
using select!
.
For the common case of needing to do an operation periodically, consider
every_until
or PeriodicGate
, which try to minimize jitter and drift.
Interrupts, wait, and notify
So, you’ve given the OS an array of tasks that need to each be polled forever. The OS could simply poll every task in a big loop (a pattern known in embedded development as a “superloop”), but this has some problems:
-
By constantly checking whether each task can make progress, we keep the CPU running full-tilt, burning power needlessly.
-
Because any given task may have to wait for every other task to be polled before it gets control, the minimum response latency to events is increased, possibly by a lot.
We can do better.
There are, in practice, two reasons why a task might yield.
-
Because it wants to leave room for other tasks to execute during a long-running operation. In this case, we actually do want to come right back and poll the task. (To do this, use
yield_cpu
.) -
Because it is waiting for an event – a particular timer tick, an interrupt from a peripheral, a signal from another task, etc. In this case, we don’t need to poll the task again until that event occurs.
The OS tracks a wake bit per task. When this bit is set, it means that the task should be polled. Each time through the outer poll loop, the OS will determine which tasks have their wake bits set, clear the wake bits, and then poll the tasks.
(Tasks might be polled even when their bit isn’t set – this is a waste of
energy, but is also something that Rust Future
s are expected to tolerate.
Giving the OS some slack on this dramatically simplifies the implementation.
However, the OS tries to poll the smallest feasible set of tasks each time
it polls.)
The need to set and check wake bits is embodied by the Notify
type,
which provides a kind of event broadcast. Tasks can subscribe to a Notify
,
and when it is signaled, all subscribed tasks get their wake bits set.
Notify
is very low level – the more pleasant abstractions of
[queue
][crate::queue], mutex
, and
sleep_until
/sleep_for
are built on top of it. However, Notify
is
the only OS facility that’s safe to use from interrupt service routines,
making it an ideal way to wake tasks when hardware events occur.
Here is a basic example of using Notify
; see the queue
and mutex
modules for details on the higher-level options.
/// Global notification signal for ethernet interrupts. static ETH_NOTIFY: os::exec::Notify = os::exec::Notify::new(); #[interrupt] fn ETH() { // omitted: code to clear interrupt condition so it doesn't just recur // Signal any tasks waiting for this interrupt. ETH_NOTIFY.notify(); } async fn ethernet_driver() { // ... stuff ... // Wait for the interrupt we care about. Check the status register to // distinguish interrupt conditions and to handle spurious wakeups. ETH_NOTIFY.until(|| dma.dmasr.read().nis()).await; // ... continue ... }
Building your own task notification mechanism
If Notify
doesn’t meet your needs, you can use the wake_task_by_index
and wake_tasks_by_mask
functions to explicitly wake one or more tasks.
Because tasks are required to tolerate spurious wakeups, both of these
functions are safe: spamming tasks with wakeup requests merely wastes
energy and time.
Both of these functions expose the fact that the scheduler tracks wake bits
in a single usize
. When waking a task with index 0 (mask 1 << 0
), we’re
actually waking any task where index % 32 == 0
. Very complex systems with
greater than 32 top-level tasks will thus experience more spurious wakeups.
The advantage of this “lossy” technique is that wake bit manipulation is
very, very cheap.
Idle behavior
When no tasks have their wake bits set, the default behavior is to idle the
processor using the WFI
instruction. You can override this behavior by
starting the scheduler with run_tasks_with_idle
or
run_tasks_with_preemption_and_idle
, which let you substitute a custom
“idle hook” to execute when no tasks are ready.
Adding preemption
By default, the scheduler does not preempt task code: task poll routines are run cooperatively, and ISRs are allowed only in between polls. This increases interrupt response latency, because if an event occurs while polling tasks, all polling must complete before the ISR is run.
Applications can override this by starting the scheduler with
run_tasks_with_preemption
or run_tasks_with_preemption_and_idle
.
These entry points let you set a preemption policy, which allows ISRs
above some priority level to preempt task code. (Tasks still cannot preempt
one another.)
Structs
Notify | A lightweight task notification scheme that can be used to safely route events from interrupt handlers to task code. |
PeriodicGate | Utility for doing something periodically. |
Enums
Interrupts | Selects an interrupt control strategy for the scheduler. |
Constants
ALL_TASKS | Constant that can be passed to |
Functions
every_until | Makes a future periodic, with a termination condition. |
noop_waker | Returns a |
run_tasks | Runs the given futures forever, sleeping when possible. Each future acts as
a task, in the sense of |
run_tasks_with_idle | Extended version of |
run_tasks_with_preemption⚠ | Extended version of |
run_tasks_with_preemption_and_idle⚠ | Extended version of |
sleep_for | Sleeps until the system time has increased by |
sleep_until | Sleeps until the system time is equal to or greater than |
wake_task_by_index | Notifies the executor that the task with the given |
wake_tasks_by_mask | Notifies the executor that any tasks whose wake bits are set in |
yield_cpu | Returns a future that will be pending exactly once before resolving. |