+7 (812) 670-9095
Обратная связьEnglish
Главная → Статьи → Системное ПО → Статья #10. Планировщик: дополнительные возможности и сохранение контекста
Версия для печати

Статья #10. Планировщик: дополнительные возможности и сохранение контекста

21 сентября 2018

Следующая статья Колина Уоллса о планировщиках задач Nucleus SE.



Статья #10. Планировщик: дополнительные возможности и сохранение контекста


В предыдущей статье мы рассматривали различные типы планирования, поддерживаемые ОСРВ, и соответствующие возможности в Nucleus SE. В этой статье рассмотрим дополнительные возможности планирования в Nucleus SE и процесс сохранения и восстановления контекста.


Опциональные функции

При разработке Nucleus SE я сделал максимальное число функций опциональными, что позволяет экономить на памяти и/или времени.


Приостановка задач

Как упоминалось ранее в статье «Планировщик: реализация», Nucleus SE поддерживает различные варианты приостановки задач, но эта функция является опциональной и включается символом NUSE_SUSPEND_ENABLE в nuse_config.h. Если установлено значение TRUE, то структура данных определена как NUSE_Task_Status[]. Такой тип приостановки применяется для всех задач. Массив имеет тип U8, где 2 полубайта используются отдельно. Младшие 4 бита содержат статус задачи: NUSE_READY, NUSE_PURE_SUSPEND, NUSE_SLEEP_SUSPEND, NUSE_MAILBOX_SUSPEND и т.д. Если задача приостановлена вызовом API (например, NUSE_MAILBOX_SUSPEND), старшие 4 бита содержат индекс объекта, на котором задача приостановлена. Эта информация используется, когда ресурс становится доступен и для вызова API необходимо выяснить, какую из приостановленных задач нужно возобновить.

Для выполнения приостановки задач используется пара функций планировщика: NUSE_Suspend_Task() и NUSE_Wake_Task().

Код NUSE_Suspend_Task имеет следующий вид:


Статья #10: Планировщик: дополнительные возможности и сохранение контекста


Функция сохраняет новое состояние задачи (все 8 бит), получаемое как параметр suspend_code. При включении блокировки (см. «API-вызовы блокировки» ниже) сохраняется код возврата NUSE_SUCCESS. Далее вызывается NUSE_Reschedule() для передачи управления следующей задаче.

Код NUSE_Wake_Task() достаточно прост:

Статья #10: Планировщик: дополнительные возможности и сохранение контекста

Состояние задачи устанавливается в NUSE_READY. Если планировщик Priority не используется, текущая задача продолжает занимать процессор, пока не придет время освободить ресурс. Если используется планировщик Priority, вызывается NUSE_Reschedule() с индексом задачи в качестве указания на выполнение, поскольку задача может иметь более высокий приоритет и должна быть незамедлительно поставлена на выполнение.


API-вызовы блокировки

В Nucleus RTOS поддерживается целый ряд вызовов API, с помощью которых разработчик может приостановить (заблокировать) задачу, если ресурсы недоступны. Задача возобновится, когда ресурсы снова будут доступны. Этот механизм реализуется и в Nucleus SE и применим к ряду объектов ядра: задача может быть заблокирована в разделе памяти, в группе событий, почтовом ящике, очереди, канале или семафоре. Но, как и большинство средств в Nucleus SE, он является опциональной и определяется символом NUSE_BLOCKING_ENABLE в nuse_config.h. Если установлено значение TRUE, то определяется массив NUSE_Task_Blocking_Return[], который содержит код возврата для каждой задачи; это может быть NUSE_SUCCESS или код NUSE_MAILBOX_WAS_RESET, показывающий, что объект был сброшен, когда задача была заблокирована. При включении блокировки соответствующий код включается в функции API с помощью условной компиляции.


Счетчик планировщика

Nucleus RTOS подсчитывает, сколько раз задача была запланирована с момента её создания и последнего сброса. Такая возможность реализована и в Nucleus SE, но является опциональной и определяется символом NUSE_SCHEDULE_COUNT_SUPPORT в nuse_config.h. Если установлено значение TRUE, то создается массив NUSE_Task_Schedule_Count[] типа U16, в котором хранится счетчик каждой задачи в приложении.


Начальное состояние задачи

Когда в Nucleus RTOS создается задача, можно выбрать её состояние: готова или приостановлена. В Nucleus SE, по умолчанию, при запуске все задачи готовы. Опция, выбранная с помощью символа NUSE_INITIAL_TASK_STATE_SUPPORT в nuse_config.h, позволяет выбрать состояние запуска. Массив NUSE_Task_Initial_State[] определяется в nuse_config.c и требует инициализации NUSE_READY или NUSE_PURE_SUSPEND для каждой задачи в приложении.


Сохранение контекста

Идея сохранения контекста задачи с любым типом планировщика, кроме RTC (Run to Completion), была представлена в статье #3 «Задачи и планирование». Как уже упоминалось, есть несколько способов сохранять контекст. Учитывая, что Nucleus SE не предназначен для 32-разрядных процессоров, я предпочёл для сохранения контекста использовать не стек, а таблицы.

Двумерный массив типа ADDR NUSE_Task_Context[][] используется для сохранения контекста для всех задач в приложении. Строки – NUSE_TASK_NUMBER (количество задач в приложении), столбцы – NUSE_REGISTERS (количество регистров, которые необходимо сохранить; зависит от процессора и устанавливается в nuse_types.h).

Само собой, сохранение контекста и восстановление кода, зависят от процессора. И это единственный код Nucleus SE, привязанный к конкретному устройству (и среде разработки). Приведу пример кода сохранения/восстановления для процессора ColdFire. Хотя такой выбор может показаться странным из-за устаревшего процессора, но его ассемблер читается легче, чем ассемблеры большинства современных процессоров. Код достаточно прост для использования в качестве основы для создания переключателя контекста и для других процессоров:


Статья #10: Планировщик: дополнительные возможности и сохранение контекста

Когда требуется переключение контекста, этот код вызывается в NUSE_Context_Swap. Используется две переменные: NUSE_Task_Active, индекс текущей задачи, контекст которой необходимо сохранить; NUSE_Task_Next, индекс задачи, контекст которой необходимо загрузить (см. раздел «Глобальные данные»).

Процесс сохранения контекста работает следующим образом:

  • Регистры A0 и D0 временно сохраняются в стеке;
  • A0 настраивается для указания на массив блоков контекста NUSE_Task_Context[][];
  • D0 загружается с помощью NUSE_Task_Active и умножается на 72 (ColdFire имеет 18 регистров, требующих 72 байта для хранения);
  • затем D0 добавляется к A0, который теперь указывает на блок контекста для текущей задачи;
  • далее регистры сохраняются в блок контекста; сначала A0 и D0 (из стека), затем D1-D7 и A1-A6, затем SR и PC (из стека, мы рассмотрим быстро инициируемое переключение контекста), и в конце сохраняется указатель стека.

Процесс загрузки контекста – та же последовательность действий в обратном порядке:

  • A0 настраивается для указания на массив блоков контекста NUSE_Task_Context[][];
  • D0 загружается с помощью NUSE_Task_Active, инкрементируется и перемножается на 72;
  • затем D0 добавляется к A0, который теперь указывает на блок контекста для новой задачи (поскольку загрузка контекста должна осуществляться в обратной процессу сохранения последовательности, первым требуется указатель стека);
  • далее регистры восстанавливаются из блока контекста; сначала указатель стека, далее PC и SR помещаются в стек, затем загружаются D1-D7 и A1-A6, и в конце D0 и A0.

Сложность при реализации переключения контекста заключается в непростом для многих процессоров доступе к регистру состояния (для ColdFire это SR). Общепринятым решением является прерывание, т. е. программное прерывание или прерывание условным переходом, что приводит к загрузке SR в стек вместе с PC. Так работает Nucleus SE на ColdFire. Макрос NUSE_CONTEXT_SWAP() задается в nuse_types.h, который расширяется до: asm(" trap #0");

Ниже представлен код инициализации (NUSE_Init_Task() в nuse_init.c) для блоков контекста:


Статья #10: Планировщик: дополнительные возможности и сохранение контекста


Так происходит инициализация указателя стека, PC и SR. Первые два имеют значения, установленные пользователем в nuse_config.c. Значение SR определяется как символ NUSE_STATUS_REGISTER в nuse_types.h. Для ColdFire это значение – 0x40002000.


Глобальные данные

Планировщик Nucleus SE требует очень мало памяти для хранения данных, но, конечно, использует структуры данных, связанные с задачами, которые будут подробно рассмотрены в следующих статьях.


Данные ОЗУ

Планировщик не использует данные, расположенные в ПЗУ, а в ОЗУ размещено от 2 до 5 глобальных переменных (все задаются в nuse_globals.c), зависящих от того, какой планировщик используется:

  • NUSE_Task_Active – переменная типа U8, содержащая индекс текущей задачи;
  • NUSE_Task_State – переменная типа U8, содержащая значение, показывающее состояние выполняемого в данный момент кода, который может быть задачей, обработчиком прерывания или кодом запуска; возможные значения: NUSE_TASK_CONTEXT, NUSE_STARTUP_CONTEXT, NUSE_NISR_CONTEXT и NUSE_MISR_CONTEXT;
  • NUSE_Task_Saved_State – переменная типа U8, используемая для защиты значения NUSE_Task_State в управляемом прерывании;
  • NUSE_Task_Next – переменная типа U8, содержащая индекс следующей задачи, которая должна быть запланирована для всех планировщиков, кроме RTC;
  • NUSE_Time_Slice_Ticks – переменная типа U16, содержащая счетчик квантов времени; используется только с планировщиком TS.

Объем данных (Data Footprint) планировщика

Планировщик Nucleus SE не использует данные ПЗУ. Точный объем данных ОЗУ варьируется в зависимости от используемого планировщика:

  • для RTC – 2 байта (NUSE_Task_Active и NUSE_Task_State);
  • для RR и Priority – 4 байта (NUSE_Task_Active, NUSE_Task_State, NUSE_Task_Saved_State и NUSE_Task_Next);
  • для TS – 6 байтов (NUSE_Task_Active, NUSE_Task_State, NUSE_Task_Saved_State, NUSE_Task_Next и NUSE_Time_Slice_Ticks).

Реализация других механизмов планирования

Несмотря на то, что Nucleus SE предлагает на выбор 4 планировщика, охватывающих большинство случаев, открытая архитектура позволяет реализовывать возможности и для других случаев.


Квантование времени с фоновой задачей

Как уже рассказывалось в статье #3 «Задачи и планирование», простой планировщик квантования времени (Time slice) имеет ограничения, поскольку он ограничивает максимальное время, в течение которого задача может занимать процессор. Более сложной опцией было бы добавление поддержки фоновой задачи. Такую задачу можно было бы запланировать на любом слоте, выделенном для приостановленных задач, и запустить при частичном освобождении слота. Этот подход позволяет запланировать задачи с регулярными интервалами и прогнозируемой долей времени процессорного ядра на выполнение.


Priority and Round Robin (RR)

В большинстве ядер реального времени планировщик приоритетов поддерживает несколько задач на каждом уровне приоритета, в отличие от Nucleus SE, где каждая задача имеет уникальный уровень. Я отдал предпочтение последнему варианту, поскольку это значительно упрощает структуры данных и, следовательно, код планировщика. Для поддержки же более сложных архитектур потребовались бы многочисленные таблицы ПЗУ и ОЗУ.


Об авторе: Колин Уоллс уже более тридцати лет работает в сфере электронной промышленности, значительную часть времени уделяя встроенному ПО. Сейчас он – инженер в области встроенного ПО в Mentor Embedded ( подразделение Mentor Graphics). Колин Уоллс часто выступает на конференциях и семинарах, автор многочисленных технических статей и двух книг по встроенному ПО. Живет в Великобритании. Профессиональный блог Колина: https://blogs.mentor.com/colinwalls/, e-mail: colin_walls@mentor.com


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


Источник: https://www.embedded.com/design/operating-systems/4458662/1/The-scheduler---options-and-context-save

Теги: ОСРВ, RTOS, планировщик