+7 (812) 494-9090
Обратная связьEnglish
Главная → Статьи → Системное ПО → Книга знаний ОСРВ МАКС. Статья 2. Ядро системы и приоритет задач
Версия для печати

Книга знаний ОСРВ МАКС. Статья 2. Ядро системы и приоритет задач

30 августа 2017

Один из разработчиков нашей операционной системы реального времени МАКС написал серию статей о ее создании и особенностях. Получилась своеобразная «Книга знаний», неформальное руководство программиста.

В первой статье мы рассказали об отличии ОС от ОСРВ, представили архитектуру ОСРВ МАКС и ее специфику.

Представляем вторую статью, в которой речь пойдет о ядре системы и приоритете задач. Также статья размещена на Хабре.




Я продолжаю выкладывать главы «Книги знаний» ОСРВ МАКС. Первая часть была общей. Сегодня вторая часть, посвященная ядру и приоритету задач.


Задача

Как уже упоминалось , задача в ОСРВ МАКС является аналогом потока в ОС общего назначения. Одновременно в системе может исполняться произвольное число задач (в рамках доступных ресурсов, разумеется). В ОС общего назначения на этом можно было бы прекратить теоретизировать и переходить к практике, но в случае с ОС реального времени программист должен быть уверен, что он сделал всё верно и задачи будут гарантированно получать столько процессорного времени, сколько им требуется. А чтобы всё сделать верно, необходимо знать кое-какую теорию. Поэтому рассмотрим работу задач более подробно.


Виды многозадачности

В ОСРВ МАКС (как и в большинстве других ОС реального времени) возможны три различных вида многозадачности: вытесняющая, кооперативная, смешанная. Зачем так много? Дело в том, что в разных системах удобнее использовать разные виды. Поэтому выбор наиболее удобного вида остаётся за разработчиком конкретной прикладной программы.


Вытесняющая многозадачность

Данный вид — самый привычный для многих программ, более того, вытесняющая многозадачность похожа на ту, что применяется в ОС общего назначения. Планировщик даёт каждой задаче фиксированный квант времени (задаётся при помощи константы MAKS_TICK_RATE_HZ, по умолчанию равной 1000 Гц, то есть, квант по умолчанию равен одной миллисекунде), после чего вытесняет эту задачу, ставя на исполнение следующую. Принцип перебора задач рассмотрим чуть ниже, здесь же просто отметим, что задачи исполняются по очереди, согласно системному таймеру. В целом, данная схема выглядит достаточно симпатичной (не зря же она применяется в ОС общего назначения), пока не начинаются битвы за аппаратуру.


Проблемы вытесняющей многозадачности при работе с аппаратурой

Одна из основных задач микроконтроллеров — работа с аппаратурой. И эта особенность накладывает на программиста ряд обязанностей. Может так оказаться, что одна и та же шина SPI обслуживает несколько устройств, разделённых разными линиями CS. Пока не закончена работа с одним устройством, приступать к работе с другим нельзя.


Рис. 1. Пример использования единой шины SPI с несколькими устройствами


Рис. 1. Пример использования единой шины SPI с несколькими устройствами

Шина же I2C исходно спроектирована для подключения многих устройств.

Иногда требуется выдерживать какие-либо временные диаграммы с достаточно высокой точностью. Если квант времени уйдёт — диаграмма будет искажена.

Но при вытесняющей многозадачности планировщик беспощаден. Никто не знает, когда сработает таймер на переключение задач, поэтому в описанных выше случаях приходится применять синхронизирующие механизмы. При определённой схеме достаточно внедрить несколько синхронизирующих примитивов. Но может оказаться так, что программист будет вынужден думать не столько о программе, сколько о синхронизации. И в этом случае лучше отказаться от вытесняющей многозадачности в пользу кооперативной.


Кооперативная многозадачность

При кооперативной многозадачности переключение осуществляется не по таймеру, а по команде от самой задачи. Когда она выполнила всё, что положено выполнить в данном кванте времени, она сама вызывает функцию переключения задач Yield(). Вызов этой функции служит сигналом планировщику, что пора передать управление следующей задаче. Как следствие, у задачи есть гарантия, что её исполнение не будет прервано, пока она сама не запросит систему об этом. Но само собой разумеется, обратная сторона медали — большой груз ответственности, лежащий на программисте. Именно прикладной программист должен гарантировать, что задача не займёт процессор слишком надолго. Также принято писать, что зависание задачи завесит всю систему, но по-моему, это даже лучше. Аппаратура должна или работать, или нет. Если отказала одна из подсистем — аппаратура становится небезопасной, последствия могут быть ужасны. Так что пусть лучше аппарат откажет полностью, это послужит сигналом для поиска ошибки. Тем не менее, в случае вытесняющей многозадачности зависание одной задачи не прервёт работу в целом, а в случае кооперативной — задача просто не отдаст управление другим.

Также кооперативная многозадачность может быть использована для быстрого портирования приложений из однозадачных систем. Рассмотрим фрагмент «прошивки» Marlin для 3D-принтера (по сути, станка с ЧПУ):


 void loop() { if(buflen < (BUFSIZE-1)) get_command(); #ifdef SDSUPPORT
	card.checkautostart(false); #endif if(buflen) { ... process_commands(); //SDSUPPORT
	buflen = (buflen-1); bufindr = (bufindr + 1)%BUFSIZE; } //check heater every n milliseconds
	manage_heater(); manage_inactivity(); checkHitEndstops(); lcd_update(); } 

Функция loop () вызывается исполняющей средой Arduino в бесконечном цикле. Подобный цикл — аналог планировщика, который переключает задачи. Если переделывать всё на настоящий планировщик, то вполне можно выкинуть функцию loop(), а её составляющие оформить в виде задач, которые крутятся в бесконечном цикле, вызывая Yield() перед очередной итерацией. Это будет гарантировать, что задачи не станут конфликтовать между собой за оборудование и создавать друг другу наведённые таймауты. А уже в дальнейшем, по мере адаптации системы, можно попытаться перейти и на вытесняющую многозадачность.


Смешанная многозадачность

Последний возможный вариант, когда система работает в режиме вытесняющей многозадачности, но какая-то (или какие-то) из задач вызывает функцию Yield(). В частности, если задача получила управление, но выяснила, что данные для неё ещё не готовы. Тогда, чтобы не занимать ресурсы, она может вызвать функцию Yield(), чтобы отдать управление другой задаче. Однако в текущей версии ОСРВ МАКС такое хоть и допустимо, но следует использовать с осторожностью. Причину я лучше выделю красным цветом, для усиления воздействия.

Обращаю внимание, что смешанную многозадачность в текущей ОСРВ МАКС следует использовать с осторожностью. Дело в том, что вытесняющее переключение задач происходит по таймеру, который работает с фиксированной частотой. Принудительное переключение задачи не влияет на этот таймер. Допустим, задача А всегда исполняется в течение 700 мкс, после чего передаёт управление планировщику при помощи функции Yield(). Планировщик поставит на исполнение задачу Б. Но через 300 мкс придёт прерывание от таймера, и планировщик передаст управление следующей задаче В. Так как передача управления происходит последовательно, задача Б окажется вечно ущемлённой. Ей всегда будет отдаваться неполноценный квант времени, а остаток от кванта задачи А.


Состояние задачи

Теперь, когда мы познакомились с видами многозадачности, можно рассмотреть задачу и логику передачи управления ею поближе. Граф состояний задачи и переходов между ними показан на Рис. 2.


Граф состояний задачи и переходов между ними

Рис. 2. Состояния задачи и граф переходов между ними

running

Самое приятное состояние. Задача исполняется. Каждый момент времени, в этом состоянии может быть всего одна задача.

ready

Когда у задачи истёк квант времени, либо она самостоятельно вызвала функцию Yield(), она переходит в это состояние и будет находиться в нём до тех пор, пока планировщик снова не поставит её на исполнение.

blocked

В это состояние задача перейдёт, если она запросила какой-либо занятый ресурс. Это могут быть примитивы синхронизации (мьютекс или семафор), это может быть ожидание события, это может быть ожидание сообщения из очереди. Пока ресурс не освободится, задача будет находиться в заблокированном состоянии, не расходуя процессорных тактов. Это чрезвычайно важное состояние для задачи, ведь чем меньше задач исполняется, тем больше процессорного времени остаётся для остальных. Крайне рекомендуется не организовывать ожидание чего-либо в цикле, а блокировать задачи, чтобы планировщик знал, что их не следует и вызывать.

Когда ресурс освободится, в зависимости от ситуации, задача может либо немедленно перейти к исполнению, либо уйти в состояние готовности к планированию. Последнее произойдёт, если освободился ресурс, которого ждало сразу несколько задач (ведь на исполнение поставить можно только одну), либо если в настоящий момент исполняются задачи с более высоким приоритетом.

inactive

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


Приоритет задачи

Каждой задаче сопоставляется свой приоритет.

Приоритет — это неотъемлемое и чрезвычайно важное свойство задачи. В целом, можно было бы присваивать приоритетам просто числовые значения. Но, во-первых, в ОС общего назначения приоритеты принято именовать (другое дело, что там это имеет смысл, а здесь, в принципе, можно было бы обойтись), а во-вторых, объектно-ориентированный подход тесно связан с типизацией. Некие абстрактные числа легко перепутать, а при именовании можно связать имена с конкретным типом «приоритет».

Так что и для целей соблюдения традиций, и для обеспечения строгой типизации, в ОСРВ МАКС выделяются следующие приоритеты задач:


  enum Priority { PriorityIdle, ///< Низший приоритет (только для задачи IDLE)
	PriorityLow, ///< Низкий приоритет
	PriorityBelowNormal, ///< Приоритет ниже обычного
	PriorityNormal, ///< Обычный приоритет (используется по умолчанию)
	PriorityAboveNormal, ///< Приоритет выше обычного
	PriorityHigh, ///< Высокий приоритет
	PriorityRealtime, ///< Наивысший приоритет (реального времени)
	PriorityMax = MAKS_MAX_TASK_PRIORITY }; 

Порядок переключения задач

При описании ОС общего назначения обычно данный раздел имеет огромный размер, при этом авторы добавляют, что описывают механизм кратко, так как от версии к версии механизм меняется. Это связано с тем, что ОС общего назначения должна постараться обеспечить работу всех потоков всех процессов. В случае с ОС реального времени всё просто. Задачи переключаются по алгоритму Round Robin, схема представлена Рис. 3. Суть этого алгоритма заключается в том, что задачи списка выполняется по порядку, а при достижении конца, ОС переключается на начало списка. Внутри списка при выборе задачи на исполнение берётся задача, следующая далее в списке. Если у неё состояние ready, она ставится на исполнение. Если bloсked — переходим к следующей задаче.


Переключение задач по алгоритму Round Robin

Рис. 3. Переключение задач по алгоритму Round Robin

Стоит отметить, что исполняются задачи с максимальным приоритетом, то есть в реальной системе может быть несколько списков. Например, список задач с приоритетом PriorityNormal и список с приоритетом PriorityHigh. При этом, если все задачи не находятся в состоянии ожидания, планировщик будет всегда ставить на исполнение только задачи из списка с более высоким приоритетом.


Пример исполнения задач 1-6


Рис. 4. Пример исполнения задач 1-6 (с более высоким приоритетом) и полного простоя задач 7-12 (их приоритет низок для работы)

Однако это не значит, что какие-то задачи игнорируются: при правильно спроектированной программе кванты времени достанутся всем задачам. Просто задачи с более высокими приоритетами должны быть спроектированы так, чтобы как можно чаще находиться в состоянии blocked — ожидания каких-либо ресурсов (семафоров, данных из очереди и т.п.).


Рис. 5. Пример исполнения задач 7-12


Рис. 5. Пример исполнения задач 7-12 (с низким приоритетом), так как все высокоприоритетные задачи заблокированы

Задачи с нормальным приоритетом — это «рабочие лошадки». Они постоянно исполняют повторяющиеся действия: следят за медленной аппаратурой, обеспечивают ввод управляющих воздействий, отображение результатов и т. д.

Задачи с приоритетом ниже нормального могут вводиться в программу, если есть гарантированный шанс того, что даже задачи с нормальным приоритетом когда-то смогут все перейти в состояние blocked (то есть, простой вызов функции Yield не поможет, надо либо ждать ресурсов, либо вызывать функцию Delay (правда, сделать так, чтобы уснули все задачи с текущим приоритетом)).


Практические примеры применения задач с высоким приоритетом будут приводиться далее. Они предназначены для обработки «быстрой» аппаратуры. Пока же достаточно запомнить несколько основополагающих правил:

  • На исполнение ставятся незаблокированные задачи с наивысшим приоритетом.
  • Задачи выбираются по кругу, одна за другой (алгоритм Round Robin).
  • Программист должен позаботиться о том, чтобы высокоприоритетные задачи, в основном, находились в заблокированном состоянии. Иначе задачи с более низкими приоритетами никогда не будут поставлены на исполнение.

Возможности по экономии энергопотребления
Когда все задачи находятся в заблокированном состоянии, процессор должен что-то делать, либо его стоит усыпить. В системе имеется специальная константа препроцессора, которая позволяет выбирать один из вариантов поведения.

MAKS_SLEEP_ON_IDLE

Если данная константа определена со значением 1, то планировщик, не найдя ни одной активной задачи ни на одном из уровней приоритета, усыпит процессор, что приведёт к снижению энергопотребления. Пробуждение наступит при приходе любого запроса на прерывание (включая системный таймер). При нулевом значении определения в таких случаях будет исполняться холостая задача, принудительно запускаемая самой операционной системой и имеющая минимально возможный приоритет:


  for(;;) { #if MAKS_DEBUG 
	++ IdleTaskCnt; #endif 
	} 

Обычный и привилегированный режимы работы задачи

Текущее оборудование, на котором может работать ОСРВ МАКС, не поддерживает виртуализации памяти, но поддерживает некоторые элементы её защиты. Благодаря этому можно ловить некоторые ошибки в работе программы. Классической является защита участка памяти в районе нулевого адреса для того, чтобы фиксировать попытки обращения по нулевым указателям (например, менеджер памяти не выделил память, вернул нулевой указатель, а программа, не проверив, начала им пользоваться).

Кроме того, можно запретить программе пользователя менять приоритеты прерываний, заблокировав доступ к аппаратуре NVIC.

Обычно в литературе пишут, что защита памяти позволяет сохранить работоспособность системы при сбое в приложении, но я не согласен с тем, что работу оборудования следует поддерживать. Наоборот, при фиксации подобных сбоев необходимо как можно быстрее перевести оборудование в выключенное состояние и сигнализировать о проблеме. Просто все советы по поддержанию работоспособности касаются ОС общего назначения. Вылетел пасьянс — запустили минёра, большой разницы не будет. В случае же с оборудованием, в системе работает одно приложение. Запускать другое — невозможно. И сбой любой из подсистем может привести к тому, что механическая или электрическая часть системы пойдут вразнос. Именно поэтому следует как можно быстрее всё перевести в безопасное состояние и проинформировать разработчиков о проблеме.

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

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

Во-первых, можно запустить отдельные задачи с привилегированным доступом, указав это в аргументах функции добавления задач Task::Add(). Кроме того, можно установить опцию условной компиляции MAKS_PROFILING_ENABLED в 1, после чего все задачи будут работать в привилегированном режиме.

В следующей статье я представлю структуру простейшей программы, работающей под управлением ОСРВ МАКС.


Теги: ОСРВ, rtos, микроконтроллеры