aboutsummaryrefslogtreecommitdiff
path: root/book/ru/src/internals/critical-sections.md
diff options
context:
space:
mode:
Diffstat (limited to 'book/ru/src/internals/critical-sections.md')
-rw-r--r--book/ru/src/internals/critical-sections.md521
1 files changed, 521 insertions, 0 deletions
diff --git a/book/ru/src/internals/critical-sections.md b/book/ru/src/internals/critical-sections.md
new file mode 100644
index 0000000..e4c3d0a
--- /dev/null
+++ b/book/ru/src/internals/critical-sections.md
@@ -0,0 +1,521 @@
+# Критические секции
+
+Когда ресурсы (статические переменные) разделяются между двумя или более задачами,
+которые выполняются с разными приоритетами, некая форма запрета изменений
+необходима, чтобы изменять память без гонки данных. В RTIC мы используем
+основанные на приоритетах критические секции, чтобы гарантировать запрет изменений
+(см. [Протокол немедленного максимального приоритета][icpp]).
+
+[icpp]: https://en.wikipedia.org/wiki/Priority_ceiling_protocol
+
+Критическия секция состоит во временном увеличении *динамического* приоритета задачи.
+Пока задача находится в критической секции, все другие задачи, которые могут
+послать запрос переменной *не могут запуститься*.
+
+Насколько большим должен быть динамический приориткт, чтобы гарантировать запрет изменений
+определенного ресурса? [Анализ приоритетов](ceilings.html) отвечает на этот вопрос
+и будет обсужден в следующем разделе. В этом разделе мы сфокусируемся
+на реализации критической секции.
+
+## Прокси-ресурсы
+
+Для упрощения, давайте взглянем на ресурс, разделяемый двумя задачами,
+запускаемыми с разными приоритетами. Очевидно, что одна задача может вытеснить
+другую; чтобы предотвратить гонку данных задача с *низким приоритетом* должна
+использовать критическую секцию, когда необходимо изменять разделяемую память.
+С другой стороны, высокоприоритетная задача может напрямую изменять
+разделяемую память, поскольку не может быть вытеснена низкоприоритетной задачей.
+Чтобы заставить использовать критическую секцию на задаче с низким приоритетом,
+мы предоставляем *прокси-ресурсы*, в которых мы отдаем уникальную ссылку
+(`&mut-`) высокоприоритетной задаче.
+
+Пример ниже показывает разные типы, передаваемые каждой задаче:
+
+``` rust
+#[rtic::app(device = ..)]
+mut app {
+ struct Resources {
+ #[init(0)]
+ x: u64,
+ }
+
+ #[interrupt(binds = UART0, priority = 1, resources = [x])]
+ fn foo(c: foo::Context) {
+ // прокси-ресурс
+ let mut x: resources::x = c.resources.x;
+
+ x.lock(|x: &mut u64| {
+ // критическая секция
+ *x += 1
+ });
+ }
+
+ #[interrupt(binds = UART1, priority = 2, resources = [x])]
+ fn bar(c: bar::Context) {
+ let mut x: &mut u64 = c.resources.x;
+
+ *x += 1;
+ }
+
+ // ..
+}
+```
+
+Теперь давайте посмотрим. как эти типы создаются фреймворком.
+
+``` rust
+fn foo(c: foo::Context) {
+ // .. пользовательский код ..
+}
+
+fn bar(c: bar::Context) {
+ // .. пользовательский код ..
+}
+
+pub mod resources {
+ pub struct x {
+ // ..
+ }
+}
+
+pub mod foo {
+ pub struct Resources {
+ pub x: resources::x,
+ }
+
+ pub struct Context {
+ pub resources: Resources,
+ // ..
+ }
+}
+
+pub mod bar {
+ pub struct Resources<'a> {
+ pub x: &'a mut u64,
+ }
+
+ pub struct Context {
+ pub resources: Resources,
+ // ..
+ }
+}
+
+mod app {
+ static mut x: u64 = 0;
+
+ impl rtic::Mutex for resources::x {
+ type T = u64;
+
+ fn lock<R>(&mut self, f: impl FnOnce(&mut u64) -> R) -> R {
+ // мы рассмотрим это детально позднее
+ }
+ }
+
+ #[no_mangle]
+ unsafe fn UART0() {
+ foo(foo::Context {
+ resources: foo::Resources {
+ x: resources::x::new(/* .. */),
+ },
+ // ..
+ })
+ }
+
+ #[no_mangle]
+ unsafe fn UART1() {
+ bar(bar::Context {
+ resources: bar::Resources {
+ x: &mut x,
+ },
+ // ..
+ })
+ }
+}
+```
+
+## `lock`
+
+Теперь давайте рассмотрим непосредственно критическую секцию. В этом примере мы должны
+увеличить динамический приоритет минимум до `2`, чтобы избежать гонки данных.
+В архитектуре Cortex-M динамический приоритет можно изменить записью в регистр `BASEPRI`.
+
+Семантика регистра `BASEPRI` такова:
+
+- Запись `0` в `BASEPRI` отключает его функциональность.
+- Запись ненулевого значения в `BASEPRI` изменяет уровень приоритета, требуемого для
+ вытеснения прерывания. Однако, это имеет эффект, только когда записываемое значение
+ *меньше*, чем уровень приоритета текущего контекста выполнения, но обращаем внимание, что
+ более низкий уровень аппаратного приоритета означает более высокий логический приоритет
+
+Таким образом, динамический приоритет в любой момент времени может быть рассчитан как
+
+``` rust
+dynamic_priority = max(hw2logical(BASEPRI), hw2logical(static_priority))
+```
+
+Где `static_priority` - приоритет, запрограммированный в NVIC для текущего прерывания,
+или логический `0`, когда текущий контекств - это `idle`.
+
+В этом конкретном примере мы можем реализовать критическую секцию так:
+
+> **ПРИМЕЧАНИЕ:** это упрощенная реализация
+
+``` rust
+impl rtic::Mutex for resources::x {
+ type T = u64;
+
+ fn lock<R, F>(&mut self, f: F) -> R
+ where
+ F: FnOnce(&mut u64) -> R,
+ {
+ unsafe {
+ // начать критическую секцию: увеличить динамический приоритет до `2`
+ asm!("msr BASEPRI, 192" : : : "memory" : "volatile");
+
+ // запустить пользовательский код в критической секции
+ let r = f(&mut x);
+
+ // окончить критическую секцию: восстановить динамический приоритет до статического значения (`1`)
+ asm!("msr BASEPRI, 0" : : : "memory" : "volatile");
+
+ r
+ }
+ }
+}
+```
+
+В данном случае важно указать `"memory"` в блоке `asm!`.
+Это не даст компилятору менять местами операции вокруг него.
+Это важно, поскольку доступ к переменной `x` вне критической секции привело бы
+к гонке данных.
+
+Важно отметить, что сигнатура метода `lock` препятствет его вложенным вызовам.
+Это необходимо для безопасности памяти, так как вложенные вызовы привели бы
+к созданию множественных уникальных ссылок (`&mut-`) на `x`, ломая правила заимствования Rust.
+Смотреть ниже:
+
+``` rust
+#[interrupt(binds = UART0, priority = 1, resources = [x])]
+fn foo(c: foo::Context) {
+ // resource proxy
+ let mut res: resources::x = c.resources.x;
+
+ res.lock(|x: &mut u64| {
+ res.lock(|alias: &mut u64| {
+ //~^ ошибка: `res` уже был заимствован уникально (`&mut-`)
+ // ..
+ });
+ });
+}
+```
+
+## Вложенность
+
+Вложенные вызовы `lock` на *том же* ресурсе должны отклоняться компилятором
+для безопасности памяти, однако вложенные вызовы `lock` на *разных* ресурсах -
+нормальная операция. В этом случае мы хотим убедиться, что вложенные критические секции
+никогда не приведут к понижению динамического приоритета, так как это плохо,
+и мы хотим оптимизировать несколько записей в регистр `BASEPRI` и compiler fences.
+Чтобы справиться с этим, мы проследим динамический приоритет задачи, с помощью стековой
+переменной и используем ее, чтобы решить, записывать `BASEPRI` или нет.
+На практике, стековая переменная будет соптимизирована компилятором, но все еще
+будет предоставлять информацию компилятору.
+
+Рассмотрим такую программу:
+
+``` rust
+#[rtic::app(device = ..)]
+mod app {
+ struct Resources {
+ #[init(0)]
+ x: u64,
+ #[init(0)]
+ y: u64,
+ }
+
+ #[init]
+ fn init() {
+ rtic::pend(Interrupt::UART0);
+ }
+
+ #[interrupt(binds = UART0, priority = 1, resources = [x, y])]
+ fn foo(c: foo::Context) {
+ let mut x = c.resources.x;
+ let mut y = c.resources.y;
+
+ y.lock(|y| {
+ *y += 1;
+
+ *x.lock(|x| {
+ x += 1;
+ });
+
+ *y += 1;
+ });
+
+ // середина
+
+ x.lock(|x| {
+ *x += 1;
+
+ y.lock(|y| {
+ *y += 1;
+ });
+
+ *x += 1;
+ })
+ }
+
+ #[interrupt(binds = UART1, priority = 2, resources = [x])]
+ fn bar(c: foo::Context) {
+ // ..
+ }
+
+ #[interrupt(binds = UART2, priority = 3, resources = [y])]
+ fn baz(c: foo::Context) {
+ // ..
+ }
+
+ // ..
+}
+```
+
+Код, сгенерированный фреймворком, выглядит так:
+
+``` rust
+// опущено: пользовательский код
+
+pub mod resources {
+ pub struct x<'a> {
+ priority: &'a Cell<u8>,
+ }
+
+ impl<'a> x<'a> {
+ pub unsafe fn new(priority: &'a Cell<u8>) -> Self {
+ x { priority }
+ }
+
+ pub unsafe fn priority(&self) -> &Cell<u8> {
+ self.priority
+ }
+ }
+
+ // repeat for `y`
+}
+
+pub mod foo {
+ pub struct Context {
+ pub resources: Resources,
+ // ..
+ }
+
+ pub struct Resources<'a> {
+ pub x: resources::x<'a>,
+ pub y: resources::y<'a>,
+ }
+}
+
+mod app {
+ use cortex_m::register::basepri;
+
+ #[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); // то же, что и `asm!` блок, виденный ранее
+ }
+
+ // так же для `UART0` / `foo` и `UART2` / `baz`
+
+ impl<'a> rtic::Mutex for resources::x<'a> {
+ type T = u64;
+
+ fn lock<R>(&mut self, f: impl FnOnce(&mut u64) -> R) -> R {
+ unsafe {
+ // определение максимального приоритет ресурса
+ const CEILING: u8 = 2;
+
+ let current = self.priority().get();
+ if current < CEILING {
+ // увеличить динамический приоритет
+ self.priority().set(CEILING);
+ basepri::write(logical2hw(CEILING));
+
+ let r = f(&mut y);
+
+ // восстановить динамический приоритет
+ basepri::write(logical2hw(current));
+ self.priority().set(current);
+
+ r
+ } else {
+ // динамический приоритет достаточно высок
+ f(&mut y)
+ }
+ }
+ }
+ }
+
+ // повторить для ресурса `y`
+}
+```
+
+Наконец, компилятор оптимизирует функцию `foo` во что-то наподобие такого:
+
+``` rust
+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.
+
+Этот инвариант нужен, чтобы избежать уеличения динамического приоритета до значений,
+при которых обработчик не сможет быть вытеснен. Лучше всего это видно на следующем примере:
+
+``` rust
+#[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.
+
+``` rust
+// код, сгенерированный 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` никогда не получат шанс на запуск.