Обзор одной российской RTOS, часть 2. Ядро ОСРВ МАКС

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

    Содержание (опубликованные и неопубликованные статьи):

    Часть 1. Общие сведения
    Часть 2. Ядро ОСРВ МАКС (настоящая статья)
    Часть 3. Структура простейшей программы
    Часть 4. Полезная теория
    Часть 5. Первое приложение
    Часть 6. Средства синхронизации потоков
    Часть 7. Средства обмена данными между задачами
    Часть 8. Работа с прерываниями

    Задача


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

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


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

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


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

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


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

    Пример использования единой шины 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. Состояния задачи и граф переходов между ними

    Рис. 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 — переходим к следующей задаче.

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

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

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

    Рис. 4. Пример исполнения задач 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, после чего все задачи будут работать в привилегированном режиме.

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

    Comments 11

      0
      задаётся при помощи константы MAKS_TICK_RATE_HZ

      Именно MAKS, не MAX?
        +1
        «ОСРВ МАКС» — Операционная Система Реального Времени для Мультиагентных Когерентных Систем.

        Так что именно MAKS. Такая политика руководства применительно к данному конкретному продукту. Там даже комментарии все на русском. Тоже не от незнания английского.
          +1
          Прошу прощения, не сразу переключился со своего контекста и не сообразил, что MAKS — не от слова МАКСИМУМ.
        +1
        Это вторая статья из цикла, а я так и не понял, зачем я это читал, смогу ли я (как инженер-программист) использовать вашу ОС в своих проектах?
        Как вы планируете распространять свою ОС, какая будет лицензия?
        Извините, но пока это выглядит как пиар ход для привлечения внимания к какому-то коммерческому продукту, который вы, возможно, выкатите в следующих статьях.
          0
          Начну издалека, но это важно. Давным-давно, в прошлом тысячелетии, сдавал я курсовик преподавательнице, которая осуществляла на нашей кафедре нормоконтроль дипломов. И поэтому в её требованиях к отчёту было, что всё должно соответствовать ЕСПД. А я, работая на заводе, имел личный томик ЕСПДшных ГОСТов и, так получилось, что выучил его почти наизусть.

          Во время защиты, она пыталась доказать, что у меня ничего в отчёте не понятно, и что надо добваить то-то и то-то. А я – тут же открывал томик и показывал, что именно это — не допускается. Апофеозом был следующий диалог:

          — Давайте выйдем в коридор, покажем десяти первым встречным Ваш отчёт и спросим, понятен ли он им?
          — Хорошо, но ещё покажем ворох документов на ДВК и спросим, понятен ли им и этот ворох
          — Думаете, это хорошо?
          — Зато по ЕСПД

          Тогда всё кончилось хорошо, мы сошлись на написании небольшого приложения в свободной форме…

          Переходим к нашим дням. Я участвовал в проектировании идеологии ОС, я делал прототип. Затем – с ядром работала группа разработчиков, а я был отвлечён другими делами, и только иногда помогал идеологически. Шло время. Ядро обрело полную функциональность и массу детских болезней. Затем эти болезни оттуда изъяли, встал вопрос об опытной эксплуатации…

          Но там такое дело. Для сертификации ОС, документация на неё должна соответствовать ЕСПД. И для большинства серьёзных Заказчиков – тоже. Поэтому… В общем, смотрите начало комментария. Итак. Передо мной лежат ЕСПД-шные документы, передо мной лежит ядро… И я задаю один простой вопрос: «Как это работает?». При том, что я знаю всю идеологию. И при том, что я – лицо заинтересованное в начале работы. При этом я ничего не понимаю. А если это будет сторонний человек, который просто обязан быть настроен скептически?

          Собственно, тогда и родилась идея в дополнение к официальной документации, от которой не отвертеться, сделать художественное руководство. Эта задача легла на меня. Я изучил текущий вариант ОС, и попутно – описал её так, чтобы было понятно простому программисту, сделав акцент именно на отличия работы под нашей ОС от стандартных общепринятых подходов программирования под ту же Windows. Это руководство долго пылилось в запасниках, ни разу не попадая в открытый доступ… И вот, наконец, было решено его опубликовать здесь.

          Правда, есть в команде и противоположное мнение, что ЕСПДшная документация вполне красива, и некоторые сторонние разработчики (именно программисты, а не бюрократы) её хвалили. И даже по ней начинали программировать под ОС. Но это никак не противоречит моей идеологии «Больше документов, хороших и разных». Кто лучше читает ЕСПД – прочтёт ту документацию, кто там ничего не поймёт (вроде меня) – тому лучше художественный текст. И при чтении художественного текста, всё равно точные прототипы следует смотреть в ЕСПДшном документе (или в заголовочных файлах, кому как нравится).

          Теперь про доступность ОС. Скачать демо версию ядра можно хоть сейчас с нашего сайта. Там же, кстати, лежит документация ЕСПДшная. Но лицензия там – чисто для ознакомления. В коммерческих целях – надо покупать. Можно было бы написать, что программисты могут убедить руководство сделать это. Но понятно, что никто этим никогда заниматься не будет, чудес не бывает.

          Тем не менее, среди наших Заказчиков есть производители отечественных микроконтроллеров. Так что может оказаться, что на стол простому программисту ляжет плата на базе микроконтроллера и предписание использовать именно нашу ОС. И та самая документация, полностью соответствующая ЕСПД… Надо бы противоикотное средство заранее закупить, так как боюсь, вспоминать нас будут многие непривычные к тому стилю, и делать это будут вдумчиво. А вот те, кто сейчас прочёл этот цикл статей – хмыкнут, скажут: «Где-то что-то я такое видел», отыщут его и найдут сначала сухое теоретическое описание (эти два поста), а также более практический следующий, и уже совсем практический, который будет через один. Ну, и более, более следующие. И поймут, что не так страшен чёрт, как его малюют.

          P.S. про лицензию возможны обновления. Сейчас идёт согласование с высшим руководством, чтобы ничего лишнего не наобещать. Если всё будет утверждено — будет ещё один комментарий.
            +2
            Согласование дополнения про лицензии «на верхах» прошло. Давайте я вставлю комментарий руководителя по поводу лицензий в виде прямой речи. Пересказывая, могу что-то утерять или исказить. Поэтому лучше прямую речь.

            Текущая лицензия – лежит на сайте вместе с демкой. Бесплатно – только изучение.

            Думаю можно озвучить, что мы планируем сделать ядро полностью бесплатным. Для начала – для некоммерческого использования, и это случится, по всей вероятности, уже до конца года. Также всерьез рассматриваем возможность сделать бесплатным и коммерческое применение. Не потому, что нет спроса – спрос уже сейчас гораздо выше ожидаемого. Но для более быстрого распространения. Платными же останутся отдельные модули (например, Mesh), сертифицированные дистрибутивы и т.п. То есть массовый пользователь не будет в чем-то ограничен, оплата потребуется только в случае применения в особо крупных и специализированных промышленных проектах.

            Почему не сделали бесплатной сразу – чтобы быть в состоянии поддерживать наших клиентов. В случае взрывного роста популярности наша служба поддержки моментально бы захлебнулась. А так – постепенно наращиваем свои возможности, аккуратно регулируя величину спроса лицензионной политикой.
            0
            в общем мне не понятно, как у молодого автора процесс/поток (в общем — единица планирования) может переходить из состояния «заблокирован» сразу в состояние «выполняется»
              0
              Допустим, все потоки почему-то заснули с целью ожидания тех или иных ресурсов. Например, взводимых по прерыванию. И вот прерывание пришло, один из ресурсов освободился. Работающих потоков — нет, все ждут. Само собой, один из них начнёт выполняться.
                0
                Кстати, более реальный случай. Освободился ресурс, которого ждал поток с более высоким приоритетом. Тоже начнётся его исполнение.
                +1
                переключение задач происходит по таймеру, который работает с фиксированной частотой.…. Допустим, задача А исполняется за 700 мкс, после чего передаёт управление функцией Yield(). Планировщик поставит на исполнение задачу Б. Но через 300 мкс придёт прерывание от таймера, и планировщик передаст управление дальше.

                Почему бы не давать задаче Б 1000 + 300 мкс? Тогда задача Б не ущемляется, а, наоборот, поощрается. Остальным задачам от этого, как минимум, не хуже, а если и задача Б завершится раньше, то и следующей задаче перепадёт больше машинного времени.
                На первый взгляд, такой подход выглядит адекватнее и воплощаться должен просто.
                  0
                  В целом — согласен. Там так было — до написания этого документа, никто и не обращал внимания на данный факт. Потом разработчики ядра прочли и сказали: «А точно!». И задумались о том, как переделать. Но рутина засосала. Ведь на самом деле — этот третий режим используется настолько редко… А разработчиков — настолько мало (сколько бы ни было — всегда не хватает)… А более срочных задач — горы. Поэтому пока будет красной рамкой в документе.

                  Тем не менее, я разработчикам ссылку на этот комментарий сейчас перешлю.

                Only users with full accounts can post comments. Log in, please.