aboutsummaryrefslogtreecommitdiff
path: root/book/ru/src/internals/timer-queue.md
diff options
context:
space:
mode:
authorAndrey Zgarbul <zgarbul.andrey@gmail.com>2021-04-04 08:15:13 +0300
committerAndrey Zgarbul <zgarbul.andrey@gmail.com>2021-04-08 12:22:43 +0300
commit05bda2b1bd2e15f5a20cda1444992eb9b6c8887e (patch)
tree25330724d4e6d9ea3a62b592c07bfc5799c7da57 /book/ru/src/internals/timer-queue.md
parent83cdf00eecb0f14857b5e0f28e884b2120eabb18 (diff)
update russian translation of the book
Diffstat (limited to 'book/ru/src/internals/timer-queue.md')
-rw-r--r--book/ru/src/internals/timer-queue.md373
1 files changed, 371 insertions, 2 deletions
diff --git a/book/ru/src/internals/timer-queue.md b/book/ru/src/internals/timer-queue.md
index 7059285..9f2dc37 100644
--- a/book/ru/src/internals/timer-queue.md
+++ b/book/ru/src/internals/timer-queue.md
@@ -1,3 +1,372 @@
-# Timer queue
+# Очередь таймера
-**TODO**
+Функциональность очередь таймера позволяет пользователю планировать задачи на запуск
+в опреленное время в будущем. Неудивительно, что эта функция также реализуется с помощью очереди:
+очередь приоритетов, где запланированные задачи сортируются в порядке аозрастания времени.
+Эта функция требует таймер, способный устанавливать прерывания истечения времени.
+Таймер используется для пуска прерывания, когда настает запланированное время задачи;
+в этот момент задача удаляется из очереди таймера и помещается в очередь готовности.
+
+Давайте посмотрим, как это реализовано в коде. Рассмотрим следующую программу:
+
+``` rust
+#[rtic::app(device = ..)]
+mod app {
+ // ..
+
+ #[task(capacity = 2, schedule = [foo])]
+ fn foo(c: foo::Context, x: u32) {
+ // запланировать задачу на повторный запуск через 1 млн. тактов
+ c.schedule.foo(c.scheduled + Duration::cycles(1_000_000), x + 1).ok();
+ }
+
+ extern "C" {
+ fn UART0();
+ }
+}
+```
+
+## `schedule`
+
+Давайте сначала взглянем на интерфейс `schedule`.
+
+``` rust
+mod foo {
+ pub struct Schedule<'a> {
+ priority: &'a Cell<u8>,
+ }
+
+ impl<'a> Schedule<'a> {
+ // `unsafe` и спрятано, потому что мы не хотим, чтобы пользовать сюда вмешивался
+ #[doc(hidden)]
+ pub unsafe fn priority(&self) -> &Cell<u8> {
+ self.priority
+ }
+ }
+}
+
+mod app {
+ type Instant = <path::to::user::monotonic::timer as rtic::Monotonic>::Instant;
+
+ // все задачи, которые могут быть запланированы (`schedule`)
+ enum T {
+ foo,
+ }
+
+ struct NotReady {
+ index: u8,
+ instant: Instant,
+ task: T,
+ }
+
+ // Очередь таймера - двоичная куча (min-heap) задач `NotReady`
+ static mut TQ: TimerQueue<U2> = ..;
+ const TQ_CEILING: u8 = 1;
+
+ static mut foo_FQ: Queue<u8, U2> = Queue::new();
+ const foo_FQ_CEILING: u8 = 1;
+
+ static mut foo_INPUTS: [MaybeUninit<u32>; 2] =
+ [MaybeUninit::uninit(), MaybeUninit::uninit()];
+
+ static mut foo_INSTANTS: [MaybeUninit<Instant>; 2] =
+ [MaybeUninit::uninit(), MaybeUninit::uninit()];
+
+ impl<'a> foo::Schedule<'a> {
+ fn foo(&self, instant: Instant, input: u32) -> Result<(), u32> {
+ unsafe {
+ let priority = self.priority();
+ if let Some(index) = lock(priority, foo_FQ_CEILING, || {
+ foo_FQ.split().1.dequeue()
+ }) {
+ // `index` - владеющий укачатель на ячейки в этих буферах
+ foo_INSTANTS[index as usize].write(instant);
+ foo_INPUTS[index as usize].write(input);
+
+ let nr = NotReady {
+ index,
+ instant,
+ task: T::foo,
+ };
+
+ lock(priority, TQ_CEILING, || {
+ TQ.enqueue_unchecked(nr);
+ });
+ } else {
+ // Не осталось места, чтобы разместить входные данные / instant
+ Err(input)
+ }
+ }
+ }
+ }
+}
+```
+
+Это очень похоже на реализацию `Spawn`. На самом деле одни и те же буфер
+`INPUTS` и список сободной памяти (`FQ`) используются совместно интерфейсами
+`spawn` и `schedule`. Главное отличие между ними в том, что `schedule` также
+размещает `Instant`, момент на который задача запланирована на запуск,
+в отдельном буфере (`foo_INSTANTS` в нашем случае).
+
+`TimerQueue::enqueue_unchecked` делает немного больше работы, чем
+просто добавление записи в min-heap: он также вызывает прерывание
+системного таймера (`SysTick`), если новая запись оказывается первой в очереди.
+
+## Системный таймер
+
+Прерывание системного таймера (`SysTick`) заботится о двух вещах:
+передаче задач, которых становятся готовыми из очереди таймера в очередь готовности
+и установке прерывания истечения времени, когда наступит запланированное
+время следующей задачи.
+
+Давайте посмотрим на соответствующий код.
+
+``` rust
+mod app {
+ #[no_mangle]
+ fn SysTick() {
+ const PRIORITY: u8 = 1;
+
+ let priority = &Cell::new(PRIORITY);
+ while let Some(ready) = lock(priority, TQ_CEILING, || TQ.dequeue()) {
+ match ready.task {
+ T::foo => {
+ // переместить эту задачу в очередь готовности `RQ1`
+ lock(priority, RQ1_CEILING, || {
+ RQ1.split().0.enqueue_unchecked(Ready {
+ task: T1::foo,
+ index: ready.index,
+ })
+ });
+
+ // вызвать диспетчер задач
+ rtic::pend(Interrupt::UART0);
+ }
+ }
+ }
+ }
+}
+```
+
+Выглядит похоже на диспетчер задач, за исключением того, что
+вместо запуска готовой задачи, она лишь переносится в очередь готовности,
+что ведет к ее запуску с нужным приоритетом.
+
+`TimerQueue::dequeue` установит новое прерывание истечения времени, если вернет
+`None`. Он сязан с `TimerQueue::enqueue_unchecked`, который вызывает это
+прерывание; на самом деле, `enqueue_unchecked` передает задачу установки
+нового прерывание истечения времени обработчику `SysTick`.
+
+## Точность и диапазон `cyccnt::Instant` и `cyccnt::Duration`
+
+RTIC предоставляет реализацию `Monotonic`, основанную на счетчике тактов `DWT` (Data Watchpoint and Trace). `Instant::now` возвращает снимок таймера; эти снимки
+DWT (`Instant`ы) используются для сортировки записей в очереди таймера.
+Счетчик тактов - 32-битный счетчик, работающий на частоте ядра.
+Этот счетчик обнуляется каждые `(1 << 32)` тактов; у нас нет прерывания,
+ассоциированног с этим счетчиком, поэтому ничего ужасного не случится,
+когда он пройдет оборот.
+
+Чтобы упорядочить `Instant`ы в очереди, нам нужно сравнить 32-битные целые.
+Чтобы учесть обороты, мы используем разницу между двумя `Instant`ами, `a - b`,
+и рассматриваем результат как 32-битное знаковое целое.
+Если результат меньше нуля, значит `b` более поздний `Instant`;
+если результат больше нуля, значит `b` более ранний `Instant`.
+Это значит, что планирование задачи на `Instant`, который на `(1 << 31) - 1` тактов
+больше, чем запланированное время (`Instant`) первой (самой ранней) записи
+в очереди приведет к тому, что задача будет помещена в неправильное
+место в очереди. У нас есть несколько debug assertions в коде, чтобы
+предотвратить эту пользовательскую ошибку, но этого нельзя избежать,
+поскольку пользователь может написать
+`(instant + duration_a) + duration_b` и переполнить `Instant`.
+
+Системный таймер, `SysTick` - 24-битный счетчик также работающий
+на частоте процессора. Когда следующая планируемая задача более, чем в
+`1 << 24` тактов в будущем, прерывание устанавливается на время в пределах
+`1 << 24` тактов. Этот процесс может происходить несколько раз, пока
+следующая запланированная задача не будет в диапазоне счетчика `SysTick`.
+
+Подведем итог, оба `Instant` и `Duration` имеют разрешение 1 такт ядра, и `Duration` эффективно имеет (полуоткрытый) диапазон `0..(1 << 31)` (не включая максимум) тактов ядра.
+
+## Вместительность очереди
+
+Вместительность очереди таймера рассчитывается как сумма вместительностей
+всех планируемых (`schedule`) задач. Как и в случае очередей готовности,
+это значит, что как только мы затребовали пустую ячейку в буфере `INPUTS`,
+мы гарантируем, что способны передать задачу в очередь таймера;
+это позволяет нам опустить проверки времени выполнения.
+
+## Приоритет системного таймера
+
+Приориет системного таймера не может быть установлен пользователем;
+он выбирается фреймворком.
+Чтобы убедиться, что низкоприоритетные задачи не препятствуют
+запуску высокоприоритетных, мы выбираем приоритет системного таймера
+максимальным из всех планируемых задач.
+
+Чтобы понять, почему это нужно, рассмотрим вариант, когда две ранее
+запланированные задачи с приоритетами `2` и `3` становятся готовыми в
+примерно одинаковое время, но низкоприоритетная задача перемещается
+в очередь готовности первой.
+Если бы приоритет системного таймера был, например, равен `1`,
+тогда после перемещения низкоприоритетной (`2`) задачи, это бы привело
+к завершению (из-за того, что приоритет выше приоритета системного таймера)
+ожидания выполнения высокоприоритетной задачи (`3`).
+Чтобы избежать такого сценария, системный таймер должен работать на
+приоритете, равном наивысшему из приоритетов планируемых задач;
+в этом примере это `3`.
+
+## Анализ приоритетов
+
+Очередь таймера - это ресурс, разделяемый всеми задачами, которые могут
+планировать (`schedule`) задачи и обработчиком `SysTick`.
+Также интерфейс `schedule` соперничает с интерфейсом `spawn`
+за списки свободной памяти. Все это должно уситываться в анализе приоритетов.
+
+Чтобы проиллюстрировать, рассмотрим следующий пример:
+
+``` rust
+#[rtic::app(device = ..)]
+mod app {
+ #[task(priority = 3, spawn = [baz])]
+ fn foo(c: foo::Context) {
+ // ..
+ }
+
+ #[task(priority = 2, schedule = [foo, baz])]
+ fn bar(c: bar::Context) {
+ // ..
+ }
+
+ #[task(priority = 1)]
+ fn baz(c: baz::Context) {
+ // ..
+ }
+}
+```
+
+Анализ приоритетов происходил бы вот так:
+
+- `foo` (prio = 3) и `baz` (prio = 1) планируемые задачи, поэтому
+ `SysTick` должен работать на максимальном из этих двух приоритетов, т.е. `3`.
+
+- `foo::Spawn` (prio = 3) и `bar::Schedule` (prio = 2) соперничают за
+ конечный потребитель `baz_FQ`; это приводит к максимальному приоритету `3`.
+
+- `bar::Schedule` (prio = 2) имеет экслюзивный доступ к
+ конечному потребителю `foo_FQ`; поэтому максимальный приоритет `foo_FQ` фактически `2`.
+
+- `SysTick` (prio = 3) и `bar::Schedule` (prio = 2) соперничают за
+ очередь таймера `TQ`; это приводит к максимальному приоритету `3`.
+
+- `SysTick` (prio = 3) и `foo::Spawn` (prio = 3) оба имеют неблокируемый
+ доступ к очереди готовности `RQ3`, что хранит записи `foo`;
+ поэтому максимальный приоритет `RQ3` фактически `3`.
+
+- `SysTick` имеет эксклюзивный доступ к очереди готовности `RQ1`,
+ которая хранит записи `baz`; поэтому максимальный приоритет `RQ1` фактически `3`.
+
+## Изменения в реализации `spawn`
+
+Когда интерфейс `schedule` используется, реализация `spawn` немного
+изменяется, чтобы отслеживать baseline задач. Как можете видеть в
+реализации `schedule` есть буферы `INSTANTS`, используемые, чтобы
+хранить время, в которое задача была запланирована навыполнение;
+этот `Instant` читается диспетчером задач и передается в пользовательский
+код, как часть контекста задачи.
+
+``` 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 => {
+ let input = baz_INPUTS[ready.index as usize].read();
+ // ADDED
+ let instant = baz_INSTANTS[ready.index as usize].read();
+
+ baz_FQ.split().0.enqueue_unchecked(ready.index);
+
+ let priority = Cell::new(PRIORITY);
+ // ИЗМЕНЕНО instant передан как часть контекста задачи
+ baz(baz::Context::new(&priority, instant), input)
+ }
+
+ Task::bar => {
+ // выглядит также как ветка для `baz`
+ }
+
+ }
+ }
+
+ // инвариант BASEPRI
+ basepri::write(snapshot);
+ }
+}
+```
+
+И наоборот, реализации `spawn` нужно писать значение в буфер `INSTANTS`.
+Записанное значение располагается в структуре `Spawn` и это либо
+время `start` аппаратной задачи, либо время `scheduled` программной задачи.
+
+``` rust
+mod foo {
+ // ..
+
+ pub struct Spawn<'a> {
+ priority: &'a Cell<u8>,
+ // ADDED
+ instant: Instant,
+ }
+
+ impl<'a> Spawn<'a> {
+ pub unsafe fn priority(&self) -> &Cell<u8> {
+ &self.priority
+ }
+
+ // ADDED
+ pub unsafe fn instant(&self) -> Instant {
+ self.instant
+ }
+ }
+}
+
+mod app {
+ 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) => {
+ baz_INPUTS[index as usize].write(message);
+ // ADDED
+ baz_INSTANTS[index as usize].write(self.instant());
+
+ lock(self.priority(), RQ1_CEILING, || {
+ RQ1.split().1.enqueue_unchecked(Ready {
+ task: Task::foo,
+ index,
+ });
+ });
+
+ rtic::pend(Interrupt::UART0);
+ }
+
+ None => {
+ // достигнута максимальная вместительность; неудачный вызов
+ Err(message)
+ }
+ }
+ }
+ }
+ }
+}
+```