Real-Time Interrupt-driven Concurrency — страница 18 из 23

fn main() {

fn foo(c: foo::Context) {

// ПРИМЕЧАНИЕ: BASEPRI содержит значение `0` (значение сброса) в этот момент


// увеличить динамический приоритет до `3`

unsafe { basepri::write(160) }


// две операции над `y` объединены в одну

y += 2;


// BASEPRI не изменяется для доступа к `x`, потому что динамический приоритет достаточно высок

x += 1;


// уменьшить (восстановить) динамический приоритет до `1`

unsafe { basepri::write(224) }


// средина


// увеличить динамический приоритет до `2`

unsafe { basepri::write(192) }


x += 1;


// увеличить динамический приоритет до `3`

unsafe { basepri::write(160) }


y += 1;


// уменьшить (восстановить) динамический приоритет до `2`

unsafe { basepri::write(192) }


// ПРИМЕЧАНИЕ: было вы правильно объединить эту операцию над  `x` с предыдущей, но

// compiler fences грубые и предотвращают оптимизацию

x += 1;


// уменьшить (восстановить) динамический приоритет до `1`

unsafe { basepri::write(224) }


// ПРИМЕЧАНИЕ: BASEPRI содержит значение `224` в этот момент

// обработчик UART0 восстановит значение `0` перед завершением

}

}

Инвариант BASEPRI

Инвариант, который фреймворк RTIC должен сохранять в том, что значение BASEPRI в начале обработчика прерывания должно быть таким же, как и при выходе из него. BASEPRI может изменяться в процессе выполнения обработчика прерывания, но но выполнения обработчика прерывания в начале и конце не должно вызвать наблюдаемого изменения BASEPRI.

Этот инвариант нужен, чтобы избежать уеличения динамического приоритета до значений, при которых обработчик не сможет быть вытеснен. Лучше всего это видно на следующем примере:


#![allow(unused)]

fn main() {

#[rtic::app(device = ..)]

mod app {

struct Resources {

#[init(0)]

x: u64,

}


#[init]

fn init() {

// `foo` запустится сразу после завершения `init`

rtic::pend(Interrupt::UART0);

}


#[task(binds = UART0, priority = 1)]

fn foo() {

// BASEPRI равен `0` в этот момент; динамический приоритет равен `1`


// `bar` вытеснит `foo` в этот момент

rtic::pend(Interrupt::UART1);


// BASEPRI равен `192` в этот момент (из-за бага); динамический приоритет равен `2`

// эта функция возвращается в `idle`

}


#[task(binds = UART1, priority = 2, resources = [x])]

fn bar() {

// BASEPRI равен `0` (динамический приоритет = 2)


x.lock(|x| {

// BASEPRI увеличен до `160` (динамический приоритет = 3)


// ..

});


// BASEPRI восстановлен до `192` (динамический приоритет = 2)

}


#[idle]

fn idle() -> ! {

// BASEPRI равен `192` (из-за бага); динамический приоритет = 2


// это не оказывает эффекта, из-за значени BASEPRI

// задача `foo` не будет выполнена снова никогда

rtic::pend(Interrupt::UART0);


loop {

// ..

}

}


#[task(binds = UART2, priority = 3, resources = [x])]

fn baz() {

// ..

}


}

}

ВАЖНО: давайте например мы забудем восстановить BASEPRI в UART1 -- из-за какого нибудь бага в генераторе кода RTIC.


#![allow(unused)]

fn main() {

// код, сгенерированный RTIC


mod app {

// ..


#[no_mangle]

unsafe fn UART1() {

// статический приоритет этого прерывания (определен пользователем)

const PRIORITY: u8 = 2;


// сделать снимок BASEPRI

let initial = basepri::read();


let priority = Cell::new(PRIORITY);

bar(bar::Context {

resources: bar::Resources::new(&priority),

// ..

});


// БАГ: ЗАБЫЛИ восстановить BASEPRI на значение из снимка

basepri::write(initial);

}

}

}

В результате, idle запустится на динамическом приоритете 2 и на самом деле система больше никогда не перейдет на динамический приоритет ниже 2. Это не компромис для безопасности памяти программы, а влияет на диспетчеризацию задач: в этом конкретном случае задачи с приоритетом 1 никогда не получат шанс на запуск.

Анализ приоритетов

Поиск максимального приоритета ресурса (ceiling) - поиск динамического приоритета, который любая задача должна иметь, чтобы безопасно работать с памятью ресурсов. Анализ приоритетов - относительно прост, но критичен для безопасности памяти RTIC программ.

Для расчета максимального приоритета ресурса мы должны сначала составить список задач, имеющих доступ к ресурсу -- так как фреймворк RTIC форсирует контроль доступа к ресурсам на этапе компиляции, он также имеет доступ к этой информации на этапе компиляции. Максимальный приоритет ресурса - просто наивысший логический приоритет среди этих задач.

init и idle не настоящие задачи, но у них есть доступ к ресурсам, поэтому они должны учитываться при анализе приоритетов. idle учитывается как задача, имеющая логический приоритет 0, в то время как init полностью исключается из анализа -- причина этому в том, что init никогда не использует (не нуждается) критические секции для доступа к статическим переменным.

В предыдущем разделе мы показывали, что разделяемые ресусы могут быть представлены уникальными ссылками (&mut-) или скрываться за прокси в зависимости от того, имеет ли задача к ним доступ. Какой из вариантов представляется задаче зависит от приоритета задачи и максимального приоритета ресурса. Если приоритет задачи такой же, как максимальный приоритет ресурса, тогда задача получает уникальную ссылку (