Concurrency
This section discusses no_std
concurrency as usually found on
microcontrollers, and memory safe patterns for sharing memory with / between
interrupt handlers. The focus of this text is on uses of unsafe
code that are
memory safety rather than building safe abstractions.
NOTE: Unlike other chapters, this text has been written assuming that the reader is not familiar with the interrupt mechanism commonly found in microcontrollers. The motivation is making this text accessible to more people who then can audit our
unsafe
code.
Interrupts
In bare metal systems, systems without an OS (operating system), usually the only form of concurrency available are hardware interrupts. An interrupt is a preemption mechanism that works as follows: when an interrupt signal arrives the processor suspends the execution of the current subroutine, (maybe) saves some registers (the current state of the program) to the stack and then jumps to another subroutine called the interrupt handler. When the processor returns from the interrupt handler, it restores the registers that it previously saved on the stack (if any) and then resumes the subroutine that was interrupted. (If you are familiar with POSIX signal handling, the semantics are pretty much the same)
Interrupt signals usually come from peripherals and are fired asynchronously. Some examples of interrupt signals are: a counter reaching zero, an input pin changing its electrical / logical state, and the arrival of a new byte of data. In some multi-core devices a core can send an interrupt signal to a different core.
How the processor locates the right interrupt handler to execute depends on the architecture. In the ARM Cortex-M architecture, there's one handler per interrupt signal and there's a table somewhere in memory that holds function pointers to all interrupt handlers. Each interrupt is given an index in this table. For example, a timer interrupt could be interrupt #0 and an input pin interrupt could be interrupt #1. If we were to depict this as Rust code it would look as follows:
# #![allow(unused_variables)] #fn main() { // `link_section` places this in some known memory location #[link_section = ".interrupt_table"] static INTERRUPT_TABLE: [extern "C" fn(); 32] = [ // entry 0: timer 0 on_timer0_interrupt, // entry 1: pin 0 on_pin0_interrupt, // .. 30 more entries .. ]; // provided by the application author extern "C" fn on_timer0_interrupt() { // .. } extern "C" fn on_pin0_interrupt() { // .. } #}
In another common interrupt model all interrupts signals map to the same interrupt handler (subroutine) and there's a hardware register that the software has to read when it enters the handler to figure out which interrupt signal triggered the interrupt. In this text, we'll focus on the ARM Cortex-M architecture which follows the one handler per interrupt signal model.
Interrupt handling API
The most basic interrupt handling API lets the programmer statically register a function for each interrupt handler only once. On top of this basic API it's possible to implement APIs to dynamically register closures as interrupt handlers. In this text we'll focus on the former, simpler API.
To illustrate this kind of API let's look at the cortex-m-rt
crate (v0.6.7).
It provides two attributes to statically register interrupts: #[exception]
and
#[interrupt]
. The former is for device agnostic interrupts, whose number and
names are the same for all Cortex-M devices; the latter is for device specific
interrupts, whose number and names vary per device / vendor. We'll stick to the
device agnostic interrupts ("exceptions") in our examples.
The following example showcases the system timer (SysTick
) interrupt, which
fires periodically. The interrupt is handled using the SysTick
handler
(function), which prints a dot to the console.
NOTE: The code for the following example and all other examples can be found in the
ci/concurrency
directory at the root of this repository.
// source: examples/systick.rs #![no_main] #![no_std] extern crate panic_halt; use cortex_m::{asm, peripheral::syst::SystClkSource, Peripherals}; use cortex_m_rt::{entry, exception}; use cortex_m_semihosting::hprint; // program entry point #[entry] fn main() -> ! { let mut syst = Peripherals::take().unwrap().SYST; // configures the system timer to trigger a SysTick interrupt every second syst.set_clock_source(SystClkSource::Core); syst.set_reload(12_000_000); // period = 1s syst.enable_counter(); syst.enable_interrupt(); loop { asm::nop(); } } // interrupt handler // NOTE: the function name must match the name of the interrupt #[exception] fn SysTick() { hprint!(".").unwrap(); }
If you are not familiar with embedded / Cortex-M programs the most important
thing to point note here is that the function marked with the entry
attribute
is the entry point of the user program. When the device (re)boots (e.g. it's
first powered) the "runtime" (the cortex-m-rt
crate) initializes static
variables (the content of RAM is random on power on) and then calls the user
program entry point. As the user program is the only process running it is not
allowed to end / exit; this is enforced in the signature of the entry
function: fn() -> !
-- a divergent function can't return.
You can run this example on an x86 machine using QEMU. Make sure you have
qemu-system-arm
installed and run the following command
$ cargo run --example systick
(..)
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/debug/examples/systick`
.................
static
variables: what is safe and what's not
As interrupt handlers have their own (call) stack they can't refer to (access)
local variables in main
or in functions called by main
. The only way main
and an interrupt handler can share state is through static
variables, which
have statically known addresses.
To really drive this point I find it useful to visualize the call stack of the program in the presence of interrupts. Consider the following example:
#[entry] fn main() -> ! { loop { { let x = 42; foo(); } { let w = 66; bar(); } } } fn foo() { let y = 24; // .. } fn bar() { let z = 33; // .. foo(); // .. } #[exception] fn SysTick() { // can't access `x` or `y` because their addresses are not statically known }
If we take snapshots of the call stack every time the SysTick
interrupt
handler is called we'll observe something like this:
+---------+
| SysTick |
| |
+---------+ +---------+ +#########+
| SysTick | | SysTick | | foo |
| | | | | y = 24 |
+#########+ +#########+ +---------+
| foo | | bar | | bar |
| y = 24 | | z = 33 | | z = 33 |
+---------+ +---------+ +---------+
| main | | main | | main |
| x = 42 | | w = 66 | | w = 66 |
+---------+ +---------+ +---------+
t = 1ms t = 2ms t = 3ms
From the call stack SysTick
looks like a normal function since it's contiguous
in memory to main
and the functions called from it. However, that's not the
case: SysTick
is invoked asynchronously. At time t = 1ms
SysTick
could, in
theory, access y
since it's in the previous stack frame; however, at time t = 2ms
y
doesn't exist; and at time t = 3ms
y
exists but has a different
location in memory (address).
I hope that explains why SysTick
can't safely access the stack frames that
belong to main
.
Let's now go over all the unsafe
and safe ways in which main
and interrupt
handlers can share state (memory). We'll start assuming the program will run on
a single core device, then we'll revisit our safe patterns in the context of a
multi-core device.
static mut
Unsynchronized access to static mut
variables is undefined behavior (UB). The
compiler will mis-optimize all those accesses.
Consider the following unsound program:
//! THIS PROGRAM IS UNSOUND! // source: examples/static-mut.rs #![no_main] #![no_std] extern crate panic_halt; use cortex_m::asm; use cortex_m_rt::{entry, exception}; static mut X: u32 = 0; #[inline(never)] #[entry] fn main() -> ! { // omitted: configuring and enabling the `SysTick` interrupt let x: &mut u32 = unsafe { &mut X }; loop { *x = 0; // <~ preemption could occur here and change the value behind `x` if *x != 0 { // the compiler may optimize away this branch panic!(); } else { asm::nop(); } } } #[exception] fn SysTick() { unsafe { X = 1; asm::nop(); } }
This program compiles: both main
and SysTick
can refer to the static
variable X
, which has a known, fixed location in memory. However, the program
is mis-optimized to the following machine code:
00000400 <main>:
400: bf00 nop
402: e7fd b.n 400 <main>
00000404 <SysTick>:
404: bf00 nop
406: 4770 bx lr
As you can see all accesses to X
were optimized away changing the intended
semantics.
Volatile
Using volatile operations to access static mut
variables does not prevent
UB. Volatile operations will prevent the compiler from mis-optimizing accesses
to the variables but they don't help with torn reads and writes which lead to
UB.
//! THIS PROGRAM IS UNSOUND! // source: examples/volatile.rs #![no_main] #![no_std] extern crate panic_halt; use core::ptr; use cortex_m::asm; use cortex_m_rt::{entry, exception}; #[repr(u64)] enum Enum { A = 0x0000_0000_ffff_ffff, B = 0xffff_ffff_0000_0000, } static mut X: Enum = Enum::A; #[entry] fn main() -> ! { // omitted: configuring and enabling the `SysTick` interrupt loop { // this write operation is not atomic: it's performed in two moves unsafe { ptr::write_volatile(&mut X, Enum::A) } // <~ preemption unsafe { ptr::write_volatile(&mut X, Enum::B) } } } #[exception] fn SysTick() { unsafe { // here we may observe `X` having the value `0x0000_0000_0000_0000` // or `0xffff_ffff_ffff_ffff` which are not valid `Enum` variants match X { Enum::A => asm::nop(), Enum::B => asm::bkpt(), } } }
In this program the interrupt handler could preempt the 2-step write operation
that changes X
from variant A
to variant B
(or vice versa) mid way. If
that happens the handler could observe X
having the value
0x0000_0000_0000_0000
or 0xffff_ffff_ffff_ffff
, neither of which are valid
values for the enum.
Let me say that again: Relying only on volatile operations for memory safety is likely wrong. The only semantics that volatile operations provide are: "tell the compiler to not remove this operation, or merge it with another operation" and "tell the compiler to not reorder this operation with respect to other volatile operations"; neither is directly related to synchronized access to memory.
Atomics
Accessing atomics stored in static
variables is memory safe. If you are
building abstractions like channels on top of them (which likely will require
unsafe
code to access some shared buffer) make sure you use the right
Ordering
or your abstraction will be unsound.
Here's an example of using a static variable for synchronization (a delay in this case).
NOTE: not all embedded targets have atomic CAS instructions in their ISA. MSP430 and ARMv6-M are prime examples. API like
AtomicUsize.fetch_add
is not available incore
for those targets.
static X: AtomicBool = AtomicBool::new(false); #[entry] fn main() -> ! { // omitted: configuring and enabling the `SysTick` interrupt // wait until `SysTick` returns before starting the main logic while !X.load(Ordering::Relaxed) {} loop { // main logic } } #[exception] fn SysTick() { X.store(true, Ordering::Relaxed); }
State and re-entrancy
A common pattern in embedded C is to use a static
variable to preserve state
between invocations of an interrupt handler.
void handler() {
static int counter = 0;
counter += 1;
// ..
}
This makes the function non-reentrant, meaning that calling this function from
itself, from main
or an interrupt handler is UB (it breaks mutable aliasing
rules).
We can make this C pattern safe in Rust if we make the non-reentrant function
unsafe
to call or impossible to call. cortex-m-rt
v0.5.x supports this
pattern and uses the latter approach to prevent calling non-reentrant functions
from safe code.
Consider this example:
// source: examples/state.rs #![no_main] #![no_std] extern crate panic_halt; use cortex_m::asm; use cortex_m_rt::{entry, exception}; #[inline(never)] #[entry] fn main() -> ! { loop { // SysTick(); //~ ERROR: cannot find function `SysTick` in this scope asm::nop(); } } #[exception] fn SysTick() { static mut COUNTER: u64 = 0; // user code *COUNTER += 1; // SysTick(); //~ ERROR: cannot find function `SysTick` in this scope }
The #[exception]
attribute performs the following source-level transformation:
# #![allow(unused_variables)] #fn main() { #[link_name = "SysTick"] // places this function in the vector table fn randomly_generated_identifier() { let COUNTER: &mut u64 = unsafe { static mut COUNTER: u64 = 0; &mut COUNTER }; // user code *COUNTER += 1; // .. } #}
Placing the static mut
variable inside a block makes it impossible to create
more references to it from user code.
This transformation ensures that the software can't call the interrupt handler from safe code, but could the hardware invoke the interrupt handler in a way that breaks memory safety? The answer is: it depends, on the target architecture.
In the ARM Cortex-M architecture once an instance of an interrupt handler starts another one won't start until the first one ends (if the same interrupt signal arrives again it is withheld). On the other hand, in the ARM Cortex-R architecture there's a single handler for all interrupts; receiving two different interrupt signals can cause the handler (function) to be invoked twice and that would break the memory safety of the source level transformation we presented above.
Critical sections
When it's necessary to share state between main
and an interrupt handler a
critical section can be used to synchronize access. The simplest critical
section implementation consists of temporarily disabling all interrupts while
main
accesses the shared static
variable. Example below:
// source: examples/cs1.rs #![no_main] #![no_std] extern crate panic_halt; use cortex_m::interrupt; use cortex_m_rt::{entry, exception}; static mut COUNTER: u64 = 0; #[inline(never)] #[entry] fn main() -> ! { loop { // `SysTick` can preempt `main` at this point // start of critical section: disable interrupts interrupt::disable(); // = `asm!("CPSID I" : : : "memory" : "volatile")` // ^^^^^^^^ // `SysTick` can not preempt this block { let counter: &mut u64 = unsafe { &mut COUNTER }; *counter += 1; } // end of critical section: re-enable interrupts unsafe { interrupt::enable() } //^= `asm!("CPSIE I" : : : "memory" : "volatile")` // ^^^^^^^^ // `SysTick` can start at this point } } #[exception] fn SysTick() { // exclusive access to `COUNTER` let counter: &mut u64 = unsafe { &mut COUNTER }; *counter += 1; }
Note the use of the "memory"
clobber; this acts as a compiler barrier that
prevents the compiler from reordering the operation on COUNTER
to outside the
critical section. It's also important to not access COUNTER
in main
outside a critical section; thus references to COUNTER
should not escape the
critical section. With these two restrictions in place, the mutable reference to
COUNTER
created in SysTick
is guaranteed to be unique for the whole
execution of the handler.
Disabling all the interrupt is not the only way to create a critical section; other ways include masking interrupts (disabling one or a subset of all interrupts) and increasing the running priority (see next section).
Masking interrupts to create a critical section deserves an example because it
doesn't use inline asm!
and thus requires explicit compiler barriers
(atomic::compiler_fence
) for memory safety.
// source: examples/cs2.rs #![no_main] #![no_std] extern crate panic_halt; use core::sync::atomic::{self, Ordering}; use cortex_m_rt::{entry, exception}; static mut COUNTER: u64 = 0; #[inline(never)] #[entry] fn main() -> ! { let mut syst = cortex_m::Peripherals::take().unwrap().SYST; // omitted: configuring and enabling the `SysTick` interrupt loop { // `SysTick` can preempt `main` at this point // start of critical section: disable the `SysTick` interrupt syst.disable_interrupt(); // ^ this method is implemented as shown in the comment below // // ``` // let csr = ptr::read_volatile(0xE000_E010);` // ptr::write_volatile(0xE000_E010, csr & !(1 << 1)); // ``` // a compiler barrier equivalent to the "memory" clobber atomic::compiler_fence(Ordering::SeqCst); // `SysTick` can not preempt this block { let counter: &mut u64 = unsafe { &mut COUNTER }; *counter += 1; } atomic::compiler_fence(Ordering::SeqCst); // end of critical section: re-enable the `SysTick` interrupt syst.enable_interrupt(); // ^ this method is implemented as shown in the comment below // // ``` // let csr = ptr::read_volatile(0xE000_E010);` // ptr::write_volatile(0xE000_E010, csr | (1 << 1)); // ``` // `SysTick` can start at this point } } #[exception] fn SysTick() { // exclusive access to `COUNTER` let counter: &mut u64 = unsafe { &mut COUNTER }; *counter += 1; }
The code is very similar to the one that disabled all interrupts except for the
start and end of the critical section, which now include a compiler_fence
(compiler barrier).
Priorities
Architectures like ARM Cortex-M allow interrupt prioritization, meaning that an interrupt that's given high priority can preempt a lower priority interrupt handler. Priorities must be considered when sharing state between interrupt handlers.
When two interrupt handlers, say A
and B
, have the same priority no
preemption can occur. Meaning that when signals for both interrupts arrive
around the same time then the handlers will be executed sequentially: that is
first A
and then B
, or vice versa. In this scenario, both handlers can
access the same static mut
variable without using a critical section; each
handler will "take turns" at getting exclusive access (&mut-
) to the static
variable. Example below.
// source: examples/coop.rs #![no_main] #![no_std] extern crate panic_halt; use cortex_m::asm; use cortex_m_rt::{entry, exception}; // priority = 0 (lowest) #[inline(never)] #[entry] fn main() -> ! { // omitted: enabling interrupts and setting their priorities loop { asm::nop(); } } static mut COUNTER: u64 = 0; // priority = 1 #[exception] fn SysTick() { // exclusive access to `COUNTER` let counter: &mut u64 = unsafe { &mut COUNTER }; *counter += 1; } // priority = 1 #[exception] fn SVCall() { // exclusive access to `COUNTER` let counter: &mut u64 = unsafe { &mut COUNTER }; *counter *= 2; }
When two interrupt handlers have different priorities then one can preempt
the other. Safely sharing state between these two interrupts requires a critical
section in the lower priority handler -- just like in the case of main
and an
interrupt handler. However, one more constraint is required: the priority of the
interrupts must remain fixed at runtime; reversing the priorities at runtime,
for example, would result in a data race.
The following example showcases safe state sharing between two interrupt handlers using a priority-based critical section.
// source: examples/cs3.rs #![no_main] #![no_std] extern crate panic_halt; use cortex_m::{asm, register::basepri}; use cortex_m_rt::{entry, exception}; // priority = 0 (lowest) #[inline(never)] #[entry] fn main() -> ! { // omitted: enabling interrupts and setting up their priorities loop { asm::nop(); } } static mut COUNTER: u64 = 0; // priority = 2 #[exception] fn SysTick() { // exclusive access to `COUNTER` let counter: &mut u64 = unsafe { &mut COUNTER }; *counter += 1; } // priority = 1 #[exception] fn SVCall() { // `SysTick` can preempt `SVCall` at this point // start of critical section: raise the running priority to 2 raise(2); // `SysTick` can *not* preempt this block because it has a priority of 2 (equal) // `PendSV` *can* preempt this block because it has a priority of 3 (higher) { // exclusive access to `COUNTER` let counter: &mut u64 = unsafe { &mut COUNTER }; *counter *= 2; } // start of critical section: lower the running priority to its original value unsafe { lower() } // `SysTick` can preempt `SVCall` again } // priority = 3 #[exception] fn PendSV() { // .. does not access `COUNTER` .. } fn raise(priority: u8) { const PRIO_BITS: u8 = 3; // (priority is encoded in hardware in the higher order bits of a byte) // (also in this encoding a bigger number means lower priority) let p = ((1 << PRIO_BITS) - priority) << (8 - PRIO_BITS); unsafe { basepri::write(p) } //^= `asm!("MSR BASEPRI, $0" : "=r"(p) : : "memory" : "volatile")` // ^^^^^^^^ } unsafe fn lower() { basepri::write(0) }
Runtime initialization
A common need in embedded Rust programs is moving, at runtime, a value from
main
into an interrupt handler. This can be accomplished at zero cost by
enforcing sequential access to static mut
variables.
// source: examples/init.rs #![feature(maybe_uninit)] #![no_main] #![no_std] extern crate panic_halt; use core::mem::MaybeUninit; use cortex_m::{asm, interrupt}; use cortex_m_rt::{entry, exception}; struct Thing { _state: (), } impl Thing { // NOTE the constructor is not `const` fn new() -> Self { Thing { _state: () } } fn do_stuff(&mut self) { // .. } } // uninitialized static variable static mut THING: MaybeUninit<Thing> = MaybeUninit::uninitialized(); #[entry] fn main() -> ! { // # Initialization phase // done as soon as the device boots interrupt::disable(); // critical section that can't be preempted by any interrupt { // initialize the static variable at runtime unsafe { THING.set(Thing::new()) }; // omitted: configuring and enabling the `SysTick` interrupt } // reminder: this is a compiler barrier unsafe { interrupt::enable() } // # main loop // `SysTick` can preempt `main` at this point loop { asm::nop(); } } #[exception] fn SysTick() { // this handler always observes the variable as initialized let thing: &mut Thing = unsafe { &mut *THING.as_mut_ptr() }; thing.do_stuff(); }
In this pattern is important to disable interrupts before yielding control to the user program and enforcing that the end user initializes all the uninitialized static variables before interrupts are re-enabled. Failure to do so would result in interrupt handlers observing uninitialized static variables.
Redefining Send
and Sync
The core / standard library defines these two marker traits as:
Sync
: types for which it is safe to share references between threads.
Send
: types that can be transferred across thread boundaries
Threads are an OS abstraction so they don't exist "out of the box" in bare metal context, though they can be implemented on top of interrupts. We'll broaden the definition of these two marker traits to include bare metal code:
-
Sync
: types for which it is safe to share references between execution contexts. -
Send
: types that can be transferred between execution contexts.
An interrupt handler is an execution context independent of the main
function,
which can be seen as the "bottom" execution context. An OS thread is also an
execution context. Each execution context has its own (call) stack and operates
independently of other execution contexts though they can share state.
Broadening the definitions of these marker traits does not change the rules
around static
variables. They must still hold values that implement the Sync
trait. Atomics implement Sync
so they are valid to place in static
variables
in bare metal context.
Let's now revisit the safe patterns we described before and see where the Sync
and Send
bounds need to be enforced for safety.
State
# #![allow(unused_variables)] #fn main() { #[exception] fn SysTick() { static mut X: Type = Type::new(); } #}
Does Type
need to satisfy Sync
or Send
? X
is effectively owned by the
SysTick
interrupt and not shared with any other execution context so neither
bound is required for this pattern.
Critical section
We can abstract the "disable all interrupts" critical section pattern into a
Mutex
type.
// source: examples/mutex.rs #![no_main] #![no_std] extern crate panic_halt; use core::cell::{RefCell, UnsafeCell}; use bare_metal::CriticalSection; use cortex_m::interrupt; use cortex_m_rt::{entry, exception}; struct Mutex<T>(UnsafeCell<T>); // TODO does T require a Sync / Send bound? unsafe impl<T> Sync for Mutex<T> {} impl<T> Mutex<T> { const fn new(value: T) -> Mutex<T> { Mutex(UnsafeCell::new(value)) } // NOTE: the `'cs` constraint prevents the returned reference from outliving // the `CriticalSection` token fn borrow<'cs>(&self, _cs: &'cs CriticalSection) -> &'cs T { unsafe { &*self.0.get() } } } static COUNTER: Mutex<RefCell<u64>> = Mutex::new(RefCell::new(0)); #[inline(never)] #[entry] fn main() -> ! { loop { // `interrupt::free` runs the closure in a critical section (interrupts disabled) interrupt::free(|cs: &CriticalSection| { let counter: &RefCell<u64> = COUNTER.borrow(cs); *counter.borrow_mut() += 1; // &*counter.borrow() //~ ERROR: this reference cannot outlive the closure }); } } #[exception] fn SysTick() { interrupt::free(|cs| { let counter = COUNTER.borrow(cs); *counter.borrow_mut() *= 2; }); }
Here we use a CriticalSection
token to prevent references escaping the
critical section / closure (see the lifetime constraints in Mutex.borrow
).
It's important to note that a Mutex.borrow_mut
method with no additional
runtime checks would be unsound as it would let the end user break Rust aliasing
rules:
# #![allow(unused_variables)] #fn main() { #[exception] fn SysTick() { interrupt::free(|cs| { // both `counter` and `alias` refer to the same memory location let counter: &mut u64 = COUNTER.borrow_mut(cs); let alias: &mut u64 = COUNTER.borrow_mut(cs); }); } #}
Changing the signature of borrow_mut
to fn<'cs>(&self, &'cs mut CriticalSection) -> &'cs mut T
does not help because it's possible to nest
calls to interrupt::free
.
# #![allow(unused_variables)] #fn main() { #[exception] fn SysTick() { interrupt::free(|cs: &mut CriticalSection| { let counter: &mut u64 = COUNTER.borrow_mut(cs); // let alias: &mut u64 = COUNTER.borrow_mut(cs); //~^ ERROR: `cs` already mutably borrowed interrupt::free(|cs2: &mut CriticalSection| { // this breaks aliasing rules let alias: &mut u64 = COUNTER.borrow_mut(cs2); }); }); } #}
As for the bounds required on the value of type T
protected by the Mutex
:
T
must implement the Send
trait because a Mutex
can be used as a channel
to move values from main
to an interrupt handler. See below:
struct Thing { _state: (), } static CHANNEL: Mutex<RefCell<Option<Thing>>> = Mutex::new(RefCell::new(None)); #[entry] fn main() -> ! { interrupt::free(|cs| { let channel = CHANNEL.borrow(cs); *channel.borrow_mut() = Some(Thing::new()); }); loop { asm::nop(); } } #[exception] fn SysTick() { interrupt::free(|cs| { let channel = CHANNEL.borrow(cs); let maybe_thing = channel.borrow_mut().take(); if let Some(thing) = mabye_thing { // `thing` has been moved into the interrupt handler } }); }
So the Sync
implementation must look like this:
# #![allow(unused_variables)] #fn main() { unsafe impl<T> Sync for Mutex<T> where T: Send {} #}
This constraint applies to all types of critical sections.
Runtime initialization
For the pattern of moving values from main
to an interrupt handler this is
clearly a "send" operation so the moved value must implement the Send
trait.
We won't give an example of an abstraction for that pattern in this text but any
such abstraction must enforce at compile time that values to be moved implement
the Send
trait.
Multi-core
So far we have discussed single core devices. Let's see how having multiple cores affects the memory safety of the abstractions and patterns we have covered.
Mutex: !Sync
The Mutex
abstraction we created and that disables interrupts to create a
critical section is unsound in multi-core context. The reason is that the
critical section doesn't prevent other cores from making progress so if more
than one core gets a reference to the data behind the Mutex
all accesses
become data races.
Here an example where we assume a dual-core device and a framework that lets you write bare-metal multi-core in a single source file.
// THIS PROGRAM IS UNSOUND! // single memory location visible to both cores static COUNTER: Mutex<Cell<u64>> = Mutex::new(Cell::new(0)); // runs on the first core #[core(0)] #[entry] fn main() -> ! { loop { interrupt::free(|cs| { let counter = COUNTER.borrow(cs); counter.set(counter.get() + 1); }); } } // runs on the second core #[core(1)] #[entry] fn main() -> ! { loop { interrupt::free(|cs| { let counter = COUNTER.borrow(cs); counter.set(counter.get() * 2); }); } }
Here each core accesses the COUNTER
variable in their main
context in an
unsynchronized manner; this is undefined behavior.
The problem with Mutex
is not the critical section that uses; it's the fact
that it can be stored in a static
variable making accessible to all cores.
Thus in multi-core context the Mutex
abstraction should not implement the
Sync
trait.
Critical sections based on interrupt masking can be used safely on architectures / devices where it's possible to assign a single core to an interrupt and any core can mask that interrupt, provided that scoping is enforced somehow. Here's an example:
# #![allow(unused_variables)] #fn main() { static mut COUNTER: u64 = 0; // runs on the first core // priority = 2 #[core(0)] #[exception] fn SysTick() { // exclusive access to `COUNTER` let counter: &mut u64 = unsafe { &mut COUNTER }; *counte += 1; } // initialized in the second core's `main` function using the runtime // initialization pattern static mut SYST: MaybeUninit<SYST> = MaybeUninit::ununitialized(); // runs on the second core // priority = 1 #[core(1)] #[exception] fn SVCall() { // `SYST` is owned by this core / interrupt let syst = unsafe { &mut *SYST.as_mut_ptr() }; // start of critical section: disable the `SysTick` interrupt syst.disable_interrupt(); atomic::compiler_fence(Ordering::SeqCst); // `SysTick` can not preempt this block { let counter: &mut u64 = unsafe { &mut COUNTER }; *counter += 1; } atomic::compiler_fence(Ordering::SeqCst); // end of critical section: re-enable the `SysTick` interrupt syst.enable_interrupt(); } #}
Atomics
Atomics are safe to use in multi-core context provided that memory barrier
instructions are inserted where appropriate. If you are using the correct
Ordering
then the compiler will insert the required barriers for you. Critical
sections based on atomics, AKA spinlocks, are memory safe to use on multi-core
devices though they can deadlock.
// spin = "0.5.0" use spin::Mutex; static COUNTER: Mutex<u64> = Mutex::new(0); // runs on the first core #[core(0)] #[entry] fn main() -> ! { loop { *COUNTER.lock() += 1; } } // runs on the second core #[core(1)] #[entry] fn main() -> ! { loop { *COUNTER.lock() *= 2; } }
State
The stateful interrupt handler pattern remains safe if and only if the target architecture / device supports assigning a handler to a single core and the program has been configured to not share stateful interrupts between cores -- that is cores should not execute the exact same handler when the corresponding signal arrives.
Runtime initialization
As the runtime initialization pattern is used to initialize the "state" of interrupt handlers so all the additional constraints required for multi-core memory safety of the State pattern are also required here.