diff options
Diffstat (limited to 'book/ru/src/internals/tasks.md')
| -rw-r--r-- | book/ru/src/internals/tasks.md | 400 |
1 files changed, 398 insertions, 2 deletions
diff --git a/book/ru/src/internals/tasks.md b/book/ru/src/internals/tasks.md index 85f783f..6650325 100644 --- a/book/ru/src/internals/tasks.md +++ b/book/ru/src/internals/tasks.md @@ -1,3 +1,399 @@ -# Task dispatcher +# Программные задачи -**TODO** +RTIC поддерживает программные и аппаратные задачи. Каждая аппаратная задача +назначается на отдельный обработчик прерывания. С другой стороны, несколько +программных задач могут управляться одним обработчиком прерывания -- +это сделано, чтобы минимизировать количество обработчиков прерывания, +используемых фреймворком. + +Фреймворк группирует задачи, для которых вызывается `spawn` по уровню приоритета, +и генерирует один *диспетчер задачи* для каждого уровня приоритета. +Каждый диспетчер запускается на отдельном обработчике прерывания, +а приоритет этого обработчика прерывания устанавливается так, чтобы соответствовать +уровню приоритета задач, управляемых диспетчером. + +Каждый диспетчер задач хранит *очередь* задач, *готовых* к выполнению; +эта очередь называется *очередью готовности*. Вызов программной задачи состоит +из добавления записи в очередь и вызова прерывания, который запускает соответствующий +диспетчер задач. Каждая запись в эту очередь содержит метку (`enum`), +которая идентифицирует задачу, которую необходимо выполнить и *указатель* +на сообщение, передаваемое задаче. + +Очередь готовности - неблокируемая очередь типа SPSC (один производитель - один потребитель). +Диспетчер задач владеет конечным потребителем в очереди; конечным производителем +считается ресурс, за который соперничают задачи, которые могут вызывать (`spawn`) другие задачи. + +## Дисметчер задач + +Давайте сначала глянем на код, генерируемый фреймворком для диспетчеризации задач. +Рассмотрим пример: + +``` rust +#[rtic::app(device = ..)] +mod app { + // .. + + #[interrupt(binds = UART0, priority = 2, spawn = [bar, baz])] + fn foo(c: foo::Context) { + foo.spawn.bar().ok(); + + foo.spawn.baz(42).ok(); + } + + #[task(capacity = 2, priority = 1)] + fn bar(c: bar::Context) { + // .. + } + + #[task(capacity = 2, priority = 1, resources = [X])] + fn baz(c: baz::Context, input: i32) { + // .. + } + + extern "C" { + fn UART1(); + } +} +``` + +Фреймворк создает следующий диспетчер задач, состоящий из обработчика прерывания и очереди готовности: + +``` rust +fn bar(c: bar::Context) { + // .. пользовательский код .. +} + +mod app { + use heapless::spsc::Queue; + use cortex_m::register::basepri; + + struct Ready<T> { + task: T, + // .. + } + + /// вызываемые (`spawn`) задачи, выполняющиеся с уровнем приоритета `1` + enum T1 { + bar, + baz, + } + + // очередь готовности диспетчера задач + // `U4` - целое число, представляющее собой емкость этой очереди + static mut RQ1: Queue<Ready<T1>, U4> = Queue::new(); + + // обработчик прерывания, выбранный для диспетчеризации задач с приоритетом `1` + #[no_mangle] + unsafe UART1() { + // приоритет данного обработчика прерывания + const PRIORITY: u8 = 1; + + let snapshot = basepri::read(); + + while let Some(ready) = RQ1.split().1.dequeue() { + match ready.task { + T1::bar => { + // **ПРИМЕЧАНИЕ** упрощенная реализация + + // используется для отслеживания динамического приоритета + let priority = Cell::new(PRIORITY); + + // вызов пользовательского кода + bar(bar::Context::new(&priority)); + } + + T1::baz => { + // рассмотрим `baz` позднее + } + } + } + + // инвариант BASEPRI + basepri::write(snapshot); + } +} +``` + +## Вызов задачи + +Интерфейс `spawn` предоставлен пользователю как методы структурв `Spawn`. +Для каждой задачи существует своя структура `Spawn`. + +Код `Spawn`, генерируемый фреймворком для предыдущего примера выглядит так: + +``` rust +mod foo { + // .. + + pub struct Context<'a> { + pub spawn: Spawn<'a>, + // .. + } + + pub struct Spawn<'a> { + // отслеживает динамический приоритет задачи + priority: &'a Cell<u8>, + } + + impl<'a> Spawn<'a> { + // `unsafe` и спрятано, поскольку сы не хотит, чтобы пользователь вмешивался сюда + #[doc(hidden)] + pub unsafe fn priority(&self) -> &Cell<u8> { + self.priority + } + } +} + +mod app { + // .. + + // Поиск максимального приоритета для конечного производителя `RQ1` + const RQ1_CEILING: u8 = 2; + + // используется, чтобы отследить сколько еще сообщений для `bar` можно поставить в очередь + // `U2` - емкость задачи `bar`; максимум 2 экземпляра можно добавить в очередь + // эта очередь заполняется фреймворком до того, как запустится `init` + static mut bar_FQ: Queue<(), U2> = Queue::new(); + + // Поиск максимального приоритета для конечного потребителя `bar_FQ` + const bar_FQ_CEILING: u8 = 2; + + // приоритет-ориентированная критическая секция + // + // это запускае переданное замыкание `f` с динамическим приоритетом не ниже + // `ceiling` + fn lock(priority: &Cell<u8>, ceiling: u8, f: impl FnOnce()) { + // .. + } + + impl<'a> foo::Spawn<'a> { + /// Вызывает задачу `bar` + pub fn bar(&self) -> Result<(), ()> { + unsafe { + match lock(self.priority(), bar_FQ_CEILING, || { + bar_FQ.split().1.dequeue() + }) { + Some(()) => { + lock(self.priority(), RQ1_CEILING, || { + // помещаем задачу в очередь готовности + RQ1.split().1.enqueue_unchecked(Ready { + task: T1::bar, + // .. + }) + }); + + // вызываем прерывание, которое запускает диспетчер задач + rtic::pend(Interrupt::UART0); + } + + None => { + // достигнута максимальная вместительность; неудачный вызов + Err(()) + } + } + } + } + } +} +``` + +Использование `bar_FQ` для ограничения числа задач `bar`, которые могут бы вызваны, +может показаться искусственным, но это будет иметь больше смысла, когда мы поговорим +о вместительности задач. + +## Сообщения + +Мы пропустили, как на самом деле работает передача сообщений, поэтому давайте вернемся +к реализации `spawn`, но в этот раз для задачи `baz`, которая принимает сообщение типа `u64`. + +``` rust +fn baz(c: baz::Context, input: u64) { + // .. пользовательский код .. +} + +mod app { + // .. + + // Теперь мы покажем все содержимое структуры `Ready` + struct Ready { + task: Task, + // индекс сообщения; используется с буфером `INPUTS` + index: u8, + } + + // память, зарезервированная для хранения сообщений, переданных `baz` + static mut baz_INPUTS: [MaybeUninit<u64>; 2] = + [MaybeUninit::uninit(), MaybeUninit::uninit()]; + + // список свободной памяти: используется для отслеживания свободных ячеек в массиве `baz_INPUTS` + // эта очередь инициализируется значениями `0` и `1` перед запуском `init` + static mut baz_FQ: Queue<u8, U2> = Queue::new(); + + // Поиск максимального приоритета для конечного потребителя `baz_FQ` + const baz_FQ_CEILING: u8 = 2; + + impl<'a> foo::Spawn<'a> { + /// Spawns the `baz` task + pub fn baz(&self, message: u64) -> Result<(), u64> { + unsafe { + match lock(self.priority(), baz_FQ_CEILING, || { + baz_FQ.split().1.dequeue() + }) { + Some(index) => { + // ПРИМЕЧАНИЕ: `index` - владеющий указатель на ячейку буфера + baz_INPUTS[index as usize].write(message); + + lock(self.priority(), RQ1_CEILING, || { + // помещаем задачу в очередь готовности + RQ1.split().1.enqueue_unchecked(Ready { + task: T1::baz, + index, + }); + }); + + // вызываем прерывание, которое запускает диспетчер задач + rtic::pend(Interrupt::UART0); + } + + None => { + // достигнута максимальная вместительность; неудачный вызов + Err(message) + } + } + } + } + } +} +``` + +А теперь давайте взглянем на настоящую реализацию диспетчера задач: + +``` rust +mod app { + // .. + + #[no_mangle] + unsafe UART1() { + const PRIORITY: u8 = 1; + + let snapshot = basepri::read(); + + while let Some(ready) = RQ1.split().1.dequeue() { + match ready.task { + Task::baz => { + // ПРИМЕЧАНИЕ: `index` - владеющий указатель на ячейку буфера + let input = baz_INPUTS[ready.index as usize].read(); + + // сообщение было прочитано, поэтому можно вернуть ячейку обратно + // чтобы освободить очередь + // (диспетчер задач имеет эксклюзивный доступ к + // последнему элементу очереди) + baz_FQ.split().0.enqueue_unchecked(ready.index); + + let priority = Cell::new(PRIORITY); + baz(baz::Context::new(&priority), input) + } + + Task::bar => { + // выглядит также как ветка для `baz` + } + + } + } + + // инвариант BASEPRI + basepri::write(snapshot); + } +} +``` + +`INPUTS` плюс `FQ`, список свободной памяти равняется эффективному пулу памяти. +Однако, вместо того *список свободной памяти* (связный список), чтобы отслеживать +пустые ячейки в буфере `INPUTS`, мы используем SPSC очередь; это позволяет нам +уменьшить количество критических секций. +На самом деле благодаря этому выбору код диспетчера задач неблокируемый. + +## Вместительность очереди + +Фреймворк RTIC использует несколько очередей, такие как очереди готовности и +списки свободной памяти. Когда список свободной памяти пуст, попытка выызова +(`spawn`) задачи приводит к ошибке; это условие проверяется во время выполнения. +Не все операции, произвожимые фреймворком с этими очередями проверяют их +пустоту / наличие места. Например, возвращение ячейки списка свободной памяти +(см. диспетчер задач) не проверяется, поскольку есть фиксированное количество +таких ячеек циркулирующих в системе, равное вместительности списка свободной памяти. +Аналогично, добавление записи в очередь готовности (см. `Spawn`) не проверяется, +потому что вместительность очереди выбрана фреймворком. + +Пользователи могут задавать вместительность программных задач; +эта вместительность - максимальное количество сообщений, которые можно +послать указанной задаче от задачи более высоким приоритетом до того, +как `spawn` вернет ошибку. Эта определяемая пользователем иместительность - +размер списка свободной памяти задачи (например `foo_FQ`), а также размер массива, +содержащего входные данные для задачи (например `foo_INPUTS`). + +Вместительность очереди готовности (например `RQ1`) вычисляется как *сумма* +вместительностей всех задач, управляемх диспетчером; эта сумма является также +количеством сообщений, которые очередь может хранить в худшем сценарии, когда +все возможные сообщения были посланы до того, как диспетчер задач получает шанс +на запуск. По этой причине получение ячейки списка свободной памяти при любой +операции `spawn` приводит к тому, что очередь готовности еще не заполнена, +поэтому вставка записи в список готовности может пропустить проверку "полна ли очередь?". + +В нашем запущенном примере задача `bar` не принимает входных данных, поэтому +мы можем пропустить проверку как `bar_INPUTS`, так и `bar_FQ` и позволить +пользователю посылать неограниченное число сообщений задаче, но если бы мы сделали это, +было бы невозможно превысить вместительность для `RQ1`, что позволяет нам +пропустить проверку "полна ли очередь?" при вызове задачи `baz`. +В разделе о [очереди таймера](timer-queue.html) мы увидим как +список свободной памяти используется для задач без входных данных. + +## Анализ приоритетов + +Очереди, использемые внутри интерфейса `spawn`, рассматриваются как обычные ресурсы +и для них тоже работает анализ приоритетов. Важно заметить, что это SPSC очереди, +и только один из конечных элементов становится ресурсом; другим конечным элементом +владеет диспетчер задач. + +Рассмотрим следующий пример: + +``` rust +#[rtic::app(device = ..)] +mod app { + #[idle(spawn = [foo, bar])] + fn idle(c: idle::Context) -> ! { + // .. + } + + #[task] + fn foo(c: foo::Context) { + // .. + } + + #[task] + fn bar(c: bar::Context) { + // .. + } + + #[task(priority = 2, spawn = [foo])] + fn baz(c: baz::Context) { + // .. + } + + #[task(priority = 3, spawn = [bar])] + fn quux(c: quux::Context) { + // .. + } +} +``` + +Вот как будет проходить анализ приоритетов: + +- `idle` (prio = 0) и `baz` (prio = 2) соревнуются за конечный потребитель + `foo_FQ`; это приводит к максимальному приоритету `2`. + +- `idle` (prio = 0) и `quux` (prio = 3) соревнуются за конечный потребитель + `bar_FQ`; это приводит к максимальному приоритету `3`. + +- `idle` (prio = 0), `baz` (prio = 2) и `quux` (prio = 3) соревнуются за + конечный производитель `RQ1`; это приводит к максимальному приоритету `3` |
