CoroOS: концепт операционной системы для микроконтролеров на корутинах С++20

от автора

Здравствуйте! Меня зовут Александр, и я работаю программистом микроконтроллеров.

Наверное, любой разработчик встраиваемых систем время от времени подумывает написать свою собственную ось. Да такую, чтобы другим неповадно было!

И ваш автор не исключение.

Как по мне — дело не то чтобы запредельно сложное, сколько кропотливое. Если у вас, как и у меня, увлечение или карьера крутится вокруг Arm Cortex-M серии, то вооружаемся стволами (раз, два и три) и выдвигаемся за Джеффом.

Но, написав и запустив ядро своей «best of the best» оси около года назад, я вскоре забросил разработку. Ибо как я ни креативил, вместо Сокола Тысячелетия у меня получался крепенький, но банальный и скучный велосипед.

А ведь хотелось оригинальности и бесстыдного выпендрёжа.

И тут в 20-й стандарт завезли корутины.

Вот это вот всё:

#include <coroutine>  Coro task(){      foo();      co_await awaitable_1();      bar();      auto res = co_await awaitable_2();      func(res);  }

Тут ваша чуйка эмбеддера должна триггернуть: «А если то же самое, но с перламутровыми пуговицами?» :

#include <coroutine>  Coro task1() {    while (true) {     // ожидаем некое событие     co_await event.get();     // по его наступлению блинкаем     toggle_led();     // запускаем таймер на 250 мс/с/мин и ждем     co_await timer.get(250);     // по истечению времени задержки снова блинкаем     toggle_led();     // и все по новой    } }

Действительно, получилась сигнатура типичной задачи в РТОС. Причем в случае с корутинами компилятор возьмет на себя расчет требуемой памяти под задачу. Вероятно, эти данные будет несложно получить и учесть. Нам останется только контролировать объем памяти, выделенной суммарно под все задачи. Уже неплохо.

Фантазируем дальше. Будет удобно, если оператор co_await сможет выступить единым окном обмена данными между корутиной, диспетчером и примитивами синхронизации (эвенты, мьютексы, таймеры, очереди etc.). Тогда мы сможем выиграть в композиции и читабельности кода.

Хорошо, а что можно сделать с приоритетами задач? А можно дерзнуть, вывернуть все наизнанку и внезапно получить задачу с динамическим приоритетом времени выполнения в зависимости от ожидаемого события :

#include <coroutine>  Coro task2(){      while(true){     // ждем сигнала от очереди с нормальным приоритетом     co_await queue.get<CoPrio::normal>();     // выгружаем значение в режиме нормального приоритета     auto payload = queue.unload();     // пробуем захватить мютекс. Если успешно, то продолжаем     // выполнение сразу. Если нет - ждем его освобождения      co_await mutex.get<CoPrio::low>();     // работаем с неким общим ресурсом в режиме низкого приоритета     shared_bus_send(res);     // свобождаем мьютекс     mutex.give();     // ждем событие с высоким приоритетом     co_await event.get<CoPrio::high>();     // выполняем срочную работу в режиме высокого приоритета     very_urgent_func();   } }

Выглядит заманчиво.

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

Для заинтересовавшихся этой темой читателей дам несколько вводных, которых буду придерживаться далее по ходу статьи:

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

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

  • важным элементом статьи являются комментарии в примерах кода.

Далее — вдумчивый лонгрид. Все-таки ось пишем, а не моргалку ардуиновскую.

Перво-наперво нам понадобится некий синхро-объект, с помощью которого мы будем обмениваться данными между корутиной и внешним миром. Определим его:

#include "co_types.hpp"  struct CoSync{   co_mutex_t mutex;  // объект с параметрами мьютекса      // рассмотрим его подробнее в разделе о CoMutex   void* co_addr;     // адрес coroutine_handle   CoState state;     // состояние корутины (выполняется, приостановлена etc.)   CoPrio prio;       // приоритет   base_t id;         // уникальный идентификатор   base_t size;       // размер выделенной для корутины памяти   co_act_t expected; // ожидаемое корутиной событие };  // также зададим алиас на указатель объекта CoSync using co_sync_t = CoSync*;

Объект кастомизируемый; мы вольны при развитии проекта расширить его новыми полями данных.

Тогда promise_type корутины может быть определен следующим образом. Опять же, для упрощения чтения я приведу только методы, содержащие логику нашей программы. Минимальный требуемый стандартом набор методов объекта Promise всегда можно посмотреть здесь.

#include <coroutine> #include <limits>  #include "co_proxy.hpp" #include "co_alloc.hpp"  struct Coro {      using promise_type = Coro;      CoSync sync {     .mutex{.ptr = nullptr, .is_taken = false},     .co_addr{nullptr},     .state{CoState::stopped},     .prio{CoPrio::lowest},     .id{ indexer_t{}() }, // присваиваем уникальный id     // в момент создания корутины     .size{0},     .expected{std::numeric_limits<co_act_t>::max()},   };      auto get_return_object() { return Coro{}; }      std::suspend_never initial_suspend() {     // корутина создана,     // меняем состояние на "готова"     sync.state = CoState::ready;     // сохраняем размер выделенной корутине памяти     sync.size = CoAlloc::get_current_size();     return {};   }      template<co_act_t ID, CoPrio P>   auto yield_value ( co_proxy_t<ID, P> p) {          struct Awaitable{ /* см. определение ниже */ };          return Awaitable{p}; }      template<co_act_t ID, CoPrio P>   auto await_transform(co_proxy_t<ID, P> p) {     return yield_value<ID, P>(p);   }      void* operator new(std::size_t sz){     // переопределяем для корутины стандартный     // оператор new, стобы аллоцировать память     // кастомным аллокатором. Как и где мы хотим.     return CoAlloc::allocate(sz); }  void operator delete( void* p){     CoAlloc::deallocate(p);   } }; 

Пробежимся сверху вниз и разберем новые типы и функции, встретившиеся в promise_type.

В момент создания корутины мы присваиваем ей идентификатор. Это просто число от 0 до суммарного количества задач, запущенных в программе. Его основное назначение — служить индексом массива, в котором будут храниться указатели на синхро-объекты CoSync. Индексируем мы корутины объектом типа indexer_t :

using indexer_t = decltype( []{ static base_t i; return ++i - 1; } );

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

Структура Awaitable:

#include <coroutine>  struct Awaitable{   // объект proxy при инстанцировании Awaitable в   // методе yield_value сохранит значение аргумента p,   // переданного ему примитивом синхронизации (эвент, очередь etc.).   // proxy - легковесный объект шаблонного типа co_proxy_t(см. ниже),    // параметризованный индексом ожидаемого события и приоритетом   // он служит каналом передачи инфо между объектом синхронизации   // и корутиной.   co_proxy_t<ID, P> proxy;      // проверяем в объекте proxy параметры мьютекса.   // по результатам приостанавливаем корутину или продолжаем   // выполнение текущей задачи   bool await_ready () {     // если мьютекс вообще не захватывался - по умолчанию     // приостанавливаемся.     if (not proxy.mutex.ptr) return false;          // иначе возвращаем флаг захвата мьютекса и действуем по     // его значению     return proxy.mutex.is_taken;   }      void await_suspend (std::coroutine_handle<promise_type> coro) {     // получаем адрес поля sync из объекта promise корутины      co_sync_t sync = &coro.promise().sync;          // при первом вызове co_await сохраняем указатель     // на sync в диспетчере. co_proxy_t знает о типе     // диспетчера, поэтому имеет доступ к его статическим     // методам     if (CoState::ready == sync->state)                     decltype(proxy)::store_sync(sync);          // последовательно сохраняем в sync: адрес cooutine_handle,     // новые параметры мьютекса, новые приоритет и ожидаемое событие.     // также меняем состояние корутины на приостановленное      sync->co_addr = coro.address();     sync->mutex = proxy.mutex;     sync->state = CoState::suspended;     sync->prio = P;     sync->expected = ID;   }      // в данной версии оси я пока не решил, что можно и нужно   // возвращать через оператор co_yield, поэтому возвращаем пока 0   auto await_resume () { return 0; } }; 

Как известно, корутины динамически аллоцируют память в куче. Для встроенных решений слово «куча» почти ругательство. Хороший разработчик встроенного ПО, как правило, сам планирует кому, где и сколько выделить памяти. Мы хотим быть хорошими, поэтому реализуем собственный аллокатор. Воспользуемся готовым инструментом из стандарта и заюзаем std::pmr::monotonic_buffer_resource. Он быстрый, принимает в конструкторе указатель на определенный нами фрагмент памяти и имеет необходимые методы (de)allocate() :

// in co_alloc.cpp  #include <memory_resource> #include "co_alloc.hpp"  byte_t raw_buf[CoParam::CORO_STORAGE_SIZE];  std::pmr::monotonic_buffer_resource mbr{raw_buf, CoParam::CORO_STORAGE_SIZE};  // переменная current_size содержит кэшированное значение // кол-ва байт последней аллокации. // max_size - суммарный размер памяти, аллоцированной всеми // корутинами в программе std::size_t current_size, max_size;

Реализация CoAlloc тривиальна(второстепенные детали опущены):

// in co_alloc.hpp  struct CoAlloc{   static void* allocate (std::size_t size);   static void deallocate (void *p);   static std::size_t get_current_size();   static std::size_t check_memory(); };  // in co_alloc.cpp  void* CoAlloc::allocate(std::size_t size){   current_size = size;   max_size += size;   return mbr.allocate(size); }  void CoAlloc::deallocate([[maybe_unused]] void *p){   // метод release() класса monotonic_buffer_resource   // высвобождает сразу всю аллоцированную объектом mbr   // память. Но так как наши задачи крутятся   // в infinite loop, мы вообще не должны сюда попасть.   // но если попали, то значит сушим весла.   mbr.release();   std::abort(); }  std::size_t CoAlloc::get_current_size(){   return current_size; }  std::size_t CoAlloc::check_memory(){   return max_size; }

Теперь рассмотрим подробнее механизм взаимодействия корутины с объектами синхронизации.

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

// in co_proxy.hpp  template<co_act_t ID, CoPrio P> struct CoProxyData final : public CoManager {   co_mutex_t mutex{     .ptr = nullptr,     .is_taken = false     }; };  template<co_act_t ID, CoPrio P> using co_proxy_t = CoProxyData<ID, P>;

Как и указывалось ранее, тип co_proxy_t аккумулирует знание о типе диспетчера (структура CoManager, о ней чуть позже), параметрах мьютекса, а также идентификаторе события (шаблонный параметр А) и приоритете (шаблонный параметр P). Но как может выглядить единый интерфейс для всех примитивов синхронизации?

Реализуем базовый класс CoProxy. От него в дальнейшем, используя паттерн CRTP, в компайл тайме унаследуем классы конкретных примитивов.

// in co_proxy.hpp  #include "co_util.hpp" #include "co_manager.hpp"  template<typename T> class CoProxy : public CoManager {      public:   using derived_ptr = T*; // алиас указателя на наследуемый класс      // метод give() будет передавать в диспетчер идентификатор   // наступившего события. метод обеспечивает двусторонний    // канал связи - при необходимости мы передадим через pack   // аргументов необходимые данные источнику события. Пример увидим   // в реализации таймера   template<typename ...Args> void give(Args&& ...args) {          // получаем от класса-наследника id события     co_act_t action =        derived()->give_impl(co_detail::forward<Args>(args) ...);     // обрабатываем его в методе диспетчера       set_action(action); }      // метод get() формирует из данных контекста и сведений    // объекта синхронизации объект типа co_proxy_t,   // передаваемый корутине при каждом вызове оператора co_await.   template<CoPrio P, typename ...Args>   auto get (Args&& ...args) {     return derived()->template get_impl<P>(co_detail::forward<Args>(args) ...); }   // метод ready() сигнализирует о готовности объекта   // синхронизации к определенному действию.   // Пример увидим в реализации очереди.    bool ready() {     return derived()->ready_impl(); }      // если объект синхронизации несет полезную нагрузку,   // выгружаем ее методом unload(). Смотрим в примере очереди ниже.   auto unload() {     return derived()->unload_impl(); }  private:    derived_ptr derived() {     return static_cast<derived_ptr>(this); } };

Здесь вы наверняка обратили на конструкцию co_detail::forward<Args>(args). Действительно, пока наша ОС в зачаточном состянии, мы не знаем всех направлений ее развития. Поэтому разумно на этом этапе заложить в ключевом интерфейсе максимум вариативности. Исполним это через инструментарий шаблонов и perfect forwarding. Ну а чтобы не инклюдить сквозь весь проект нехилый такой хедер <utility>, я определил move(), forward() в компактном заголовочнике»co_util.hpp», в нэймспейсе co_detail, благо их реализации рассмотрены во многих источниках(пример).

В принципе я и далее по возможности буду избегать включения в свои заголовочники «тяжелых» хедеров стандартной библиотеки (буду подключать их только в .cpp файлах или использовать в качестве альтернативы свою легковесную имплементацию требуемых инструментов). Цель проста и благородна — сэкономить время себе и потенциальному пользователю на сборку проекта. Понятно, что речь в данном случае идет о секундах, но все-таки…

Настало время разработать примитивы синхронизации. Начнем с очереди. Класс CoQueue может быть определен так:

// in "co_queue.hpp"  #include "co_variant.hpp" #include "co_queue_impl.hpp" #include "co_proxy.hpp"  template<co_act_t A> class CoQueue final: public CoProxy<CoQueue<A>>{      public:   // шаблонный класс CoQueueImpl реализует собственно логику   // очереди. Параметризуем его типом полезной нагрузки   // размером и типом отвечающим за атомарность операций   using co_queue_t =      CoQueueImpl<co_payload_t, CoParam::CORO_QUEUE_SIZE, co_critical_t>;      // в методе give_impl() помещаем данные в очередь и   // возвращаем id данной конкретной очереди.    // как помните, в методе give() базового класса этот   // id передается диспетчеру для сигнала возобновления   // целевой корутины   co_act_t give_impl(const co_payload_t& payload) {          instance().push(payload);     return A;   }      // конструируем и передаем корутине сведения о   // событии, приоритете, параметрах мьютекса (по умолчанию - пустые)   // и типе диспетчера   template<CoPrio P>   co_proxy_t<A, P> get_impl() { return {}; }      // проверяем, содержит ли очередь данные   bool ready_impl() {     return !instance().is_empty();   }      // выгружаем очередь   co_payload_t unload_impl() {     return instance().pop();   }      private:   // приватным методом instance() при первом конструировании   // объекта co_queue создаем статический объект queue_impl   // и возвращаем ссылку на него при всех последующих операциях.   [[gnu::always_inline]] co_queue_t& instance() {     static  co_queue_t queue_impl;     return queue_impl;   } };  // дефайн упрощащющий задание пользовательских типов очередей #define CO_QUEUE(q)   using q = CoProxy<CoQueue<__COUNTER__>>  /*  USER SECTION START  */ // в пользовательской секции задаем типы очередей и  // и далее инстанцируем и пользуем где необходимо. При этом  // каждый вновь созданный объект этого же типа будет // помнить историю операций с ним. CO_QUEUE(spi_queue_t); CO_QUEUE(uart_queue_t); /*  USER SECTION end  */

С классом CoQueueImpl я вас ничем не удивлю, его реализация на данном этапе разработки ОС элементарна:

// in "co_queue_impl.hpp"  #include "critical_section.hpp" #include "co_types.hpp"  template<typename P, CoParam D, typename CS> class CoQueueImpl{  public:    void push (const P& payload) {   CS critical_section;      queue[head] = payload;      ++head;      if (D == head) head = 0; }  P pop () {   CS critical_section;          base_t current = tail;          ++tail;          if (D == tail) tail = 0;          return queue[current]; }      P back() {     return queue[tail];   }  bool is_empty() { return head == tail; }  auto& get_instance() { return queue; }  private: P queue[D];   base_t head{0}, tail{0}; };

В рассмотренном выше классе CoQueue в качестве элемента очереди задан некий тип co_payload_t. Это алиас на облегченный (отсылка к моему бзику об экономии времени компиляции) аналог std::variant — класс CoVariant. В его основе использован т.н. tagged union. Если вы не знакомы с этой конструкцией, то продемонстрирую основную идею урезанной имплементацей CoVariant ниже. Полную реализацию сможете найти в примере в конце статьи. Пока наш вариант готов принимать только типы uint32_t и void*. Расширение его новыми типами — вопрос аккуратного копипаста. Ну а если вас не тревожит время сборки проекта, его легко можно заменить на std::variant.

// in "co_variant.hpp"  class CoVariant{      public:   CoVariant(const CoVariant& other) : tag(other.tag){          switch(tag){                case Tag::NONE:         val = 0;         break;              case Tag::BASE_T:         val = other.val;         break;              case Tag::VOID_PTR:         ptr = other.ptr;         break;     }   }      private:      enum class Tag{NONE, VOID_PTR, BASE_T};   Tag tag{Tag::NONE};      union{         void* ptr;         base_t val;     }; }; 

Интерфейс класса CoMutex следует той же логике, что и рассмотренный ранее класс CoQueue. Существенные детали реализации, связанные именно с функционалом мьютекса, прокомментированы в примере кода:

// in "co_types.hpp"  // структура параметров мьютекса struct CoMutexData{     bool* ptr; // указатель на мьютекс     bool is_taken; // флаг успешности взятия мьютекса };  using co_mutex_t = CoMutexData;  // in "co_mutex.hpp"  #include "critical_section.hpp" #include "co_proxy.hpp"  template<typename CS> class CoMutexImpl{  public:    co_mutex_t get_mutex() {   CS critical_section;     // если мьютекс свободен, флаг is_taken = true     bool is_taken = !mutex;     // забираем мьтекс     if (is_taken) mutex = true;          return {&mutex, is_taken};   }    void give_mutex() { mutex = false; }  private: bool mutex{false}; };  template<co_act_t A> class CoMutex final : public CoProxy<CoMutex<A>>{      public:      co_act_t give_impl() {     instance().give_mutex();     return A; };      template<CoPrio P>   co_proxy_t<A, P> get_impl() {     // передаем корутине параметры мьютекса     return {.mutex = instance().get_mutex(),};   }      private:   using mutex_impl_t = CoMutexImpl<co_critical_t>;      [[gnu::always_inline]] mutex_impl_t& instance() {     static mutex_impl_t mutex_impl;     return mutex_impl;   } };  #define CO_MUTEX(n)   using n = CoProxy<CoMutex<__COUNTER__>>  /*  USER SECTION START  */ CO_MUTEX(dma_mutex_t); /*  USER SECTION END  */

Для имплементации таймера мы будем использовать стандартный инструментарий из std::chrono. Но сначала определим наш ресурс локального времени:

// in "co_chrono.сpp"  #include <chrono> #include <tuple>  struct PlatformClock{   using duration = std::chrono::duration<base_t, std::milli>;   using rep = duration::rep;   using period =  duration::period;   using time_point = std::chrono::time_point<PlatformClock, duration>;      static constexpr bool is_steady = false;      static time_point now() {     // пример к статье будет реализован на stm-ке,     // поэтому, не мудрствуя лукаво, воспользуемся халовской функцией     auto millisecond_tick = HAL_GetTick();          return time_point(duration(millisecond_tick));   } };

Далее определим вспомогательный класс CoChrono:

// in "co_chrono.hpp"  struct CoChrono{   // заводим и регистрируем таймер   static void set_timer (co_act_t A, base_t delay);   // проверяем зарегистрированные таймеры   static void check_if_expired(); };  // in "co_chrono.cpp"  #include <chrono> #include <tuple> #include "co_proxy.hpp" #include "co_queue_impl.hpp" #include "co_chrono.hpp"  using namespace std::chrono;  // наследуемся от CoProxy, чтобы иметь возможность // сигналить о наступлении заданного времени class CoChronoImpl final: public CoProxy<CoChronoImpl>{   public:   co_act_t give_impl(co_act_t A) { return A; } };  using co_chrono_t = CoProxy<CoChronoImpl>;  // задаем тип и удобоваримый алиас регистрационной записи таймера. // она будет содержать id таймера, стартовое время и величину задержки using chrono_entry_t = std::tuple<co_act_t, PlatformClock::time_point, base_t>;  // хранить записи будем в очереди; задаем ее тип и алиас using chrono_queue_t =    CoQueueImpl<chrono_entry_t, CoParam::CORO_TIMER_NUM, co_critical_t>;  // инстанцируем очередь chrono_queue_t chrono_queue;   void CoChrono::set_timer (co_act_t A, base_t delay) {   // сохраняем запись с установкой времени момента регистрации   chrono_queue.push( {A, PlatformClock::now(), delay} ); }  // этот метод вызываем в обработчике прерывания // таймера, назначенного в микроконтроллере void CoChrono::check_if_expired() {   auto& q = chrono_queue.get_instance();   co_chrono_t chrono;      // пробегаемся по очереди   for (auto& [act, start_point, delay] : q){          // если задержка не установлена, пропускаем итерацию     if (not delay) continue;          // считаем пройденное время с момента регистрации таймера     auto res =        duration_cast<milliseconds>(PlatformClock::now() - start_point).count();          // если время вышло, сигналим диспетчеру и зачищаем поле delay,     // чтобы избежать повторного срабатывания     if (res > delay) {              chrono.give(act);              delay = 0;     }   } }

Теперь мы готовы дать определение класса CoTimer:

// in "co_timer.hpp"  #include "co_proxy.hpp" #include "co_chrono.hpp"  template<co_act_t A> class CoTimer final : public CoChrono, public CoProxy<CoTimer<A>>{   public:      template<CoPrio P>   co_proxy_t<A, P> get_impl(base_t delay) {     // регистрируем и запускаем таймер     set_timer(A, delay);     return {};   } };  #define CO_TIMER(n)   using n = CoProxy<CoTimer<__COUNTER__>>  /*  USER SECTION START  */ CO_TIMER(app_timer_t); /*  USER SECTION END  */

Класс CoEvent здесь приводить не буду, он не несет ничего нового к рассмотренному. Его реализацию вы сможете посмотреть по ссылке на пример в конце статьи.

Теперь рассмотрим подробнее диспетчер нашей ОС — класс CoManager и его интерфейс:

// in "co_manager.hpp"  struct CoManager{     static void set_action(co_act_t act);     static void store_sync(co_sync_t s);     static void run(); };  // in "co_manager.cpp"  #include <coroutine>  #include "critical_section.hpp" #include "co_queue_impl.hpp" #include "co_manager.hpp"  // если событие, возобновляющее корутину, это сигнал от мьютекса, // то обрабатываем параметры мьютекса, сохраненные в объекте CoSync корутины // локальной функцией mutex_take() static bool mutex_take (co_sync_t sync); //объявляем локальную функцию, ответственную за возобновление корутины static void co_resume (co_sync_t sync);  // на базе разработанного ранее класса создаем очередь // в которую будем складывать указатели на синхрообъекты // готовых к возобновлению корутин using sync_queue_t =    CoQueueImpl<co_sync_t, CoParam::CORO_TASK_NUM, co_critical_t>;  // создаем массив указателей на синхрообъекты всех корутин программы co_sync_t co_repo[CoParam::CORO_TASK_NUM];  // создаем массив очередей указателей синхрообъектов корутин // получивших сигнал к возобновлению. // наименьший индекс массива соотвтетствует наивысшему приоритету sync_queue_t co_queue_repo[CoPrio::num];  // указатель на синхрообъект корутины, выполняемой в данный момент времени co_sync_t current; // кэшированное значение памяти, выделенной аллокатором для корутин base_t current_memory;   void CoManager::set_action(co_act_t act) {      // пробегаемся по массиву указателей на синхрообъекты   for (auto sync : co_repo){     // если корутина с таким индексом не создана -      // пропускаем итерацию     if (not sync) continue;     // если id наступившего события совпадает с id ожидаемого     // события, то помещаем в очередь готовых к возобновлению     // в соответствии с назначенным событию приоритетом     if(act == sync->expected)       co_queue_repo[sync->prio].push(sync);   } };  // в методе co_yield() сохраняем указатель // на синхрообъект корутины void CoManager::store_sync(co_sync_t s) {   co_repo[s->id] = s; }  void CoManager::run(){      // пробегаемся по массиву очередей готовых к выполнению   // корутин   for (auto& queue : co_queue_repo){          // обрабатываем очередь пока она не опустеет      while ( not queue.is_empty() ) {              co_sync_t sync = queue.back();              // если в данный момент нет выполняемых корутин       // сохраняем указатель из очереди в переменную current       // и возобновляем корутину       if ( not current ||            CoState::suspended == current->state ){                 current = sync;                  co_resume(current);               // если в данный момент выполняется какая-то корутина       // и ее приоритет ниже, чем у данной, то вытесняем ее,       // сохранив ее указатель в переменную preemted.       // по завершению более срочной корутины, продолжаем выполнение       // вытесненной       } else if (CoState::running == current->state  &&                  sync->prio < current->prio          ){                  current->state = CoState::blocked;         co_sync_t preemted = current;         current = sync;                  co_resume(current);                  preemted->state = CoState::running;         current = preemted;                } else {         return;       }     }   } }  static void co_resume (co_sync_t sync){   // если корутина ждала сигнала от корутины,   // но он пока захвачен, то не возобновляемся   if( not mutex_take(sync) ) return;       // меняем состояние на "выполняется"   sync->state = CoState::running;   // сбрасываем id ожидаемого события   sync->expected = std::numeric_limits<co_act_t>::max();      // в период выполнения метода CoManager::run() прерывания   // запрещены, поэтому при возобновлении корутины мы их разрешаем   // (чтобы корутина могла быть вытеснена более приоритетной)...   co_detail::enable_irq();      std::coroutine_handle<>::from_address(sync->co_addr).resume();       // ... а по завершении - вновь запрещаем   co_detail::disable_irq(); }  static bool mutex_take (co_sync_t sync){      bool *mutex_ptr = sync->mutex.ptr;   // если mutex_ptr != nullptr и мьютекс захвачен   // корутину не возобновляем   if ( mutex_ptr && (*mutex_ptr) ) return false;   // иначе захватываем мьютекс и возобновляем   if (mutex_ptr) *mutex_ptr = true;      return true; } 

Настало время поговорить о переключении контекста в arm cortex-m. На мой взгляд эту тему практически полностью закрыл замечательный материал уважаемого @lamerok. Я сам по ней закрывал белые пятна в своем понимании темы

Если вы не очень разбираетесь в этом вопросе, настоятельно рекомендую проштудировать сначала указанную статью.

Здесь же я ограничусь схематичным описанием процедуры пререключения контекста через призму взаимодействия с ОС:

  1. в МК назначаем таймер — источник тиков для ОС (обычно 1 или 10 мс)

  2. в обработчике прерываний этого таймера генерим PendSV request

  3. из обработчика PendSV IRQ, предварительно запретив прерывания и сохранив на стеке «снимок» значений системных регистров вытесненного контекста, вызываем в thread mode метод CoManager::run()

  4. из метода CoManager::run() последовательно, в соответствии с заданным приоритетом, возобновляем корутины. Они могут быть вновь прерваны системным таймером и тогда мы по методу матрешки вновь пробегаемся по п.1 — 4.

  5. из метода CoManager::run() возвращаемся в промежуточную функцию ManagerReturn() в которой генерируем NMI request.

  6. в обработчике NMI IRQ восстанавливаем кадр вытесненного контекста

  7. возвращаемся в вытесненный контекст в thread mode

Ну что ж, вся концепция и теория позади, переходим к примерам. Онлайн можно посмотреть здесь. На трех задачах потестированы все рассмотренные примитивы синхронизации. В демонстрационно-образовательных целях сделал отладочный вывод из корутин; можно понаблюдать порядок вызова методов при их приостановке/возобновлении. Как именно работает пример можно уточнить из комментариев в коде.

Рабочий пример на STM32F412 Discovery можно забрать отсюда. Там сделан акцент на вытеснение задач более приоритетными. task_1 стартует и ожидает event из обработчика прерывания TIM14, запущенного в режиме one pulse mode. Получив event, task_1 возобновляет работу и через 1 секунду загружает данные в очередь task_2. Получив сигнал от очереди, task_2 запускается и вытесняет task_1, так как имеет более высокий приоритет. Также отработав одну секунду, task_2 выгружает значение из очереди, инкрементирует его и загружает в очередь task_3. Последняя по схожему сценарию вытесняет task_2. По завершению работы task_3, возобновляется task_2, а следом и task_1. В конце task_1 рестартует TIM14 и описанный цикл повторяется. Работа задач демонстрируется через светодиоды и отладочный вывод через SWO.

И несколько слов в заключение. В статье описан именно концепт ОС на корутинах. Он работает, но пока опробован на самых простых задачах. Требуется обкатка на разных сценариях, наверняка я что-то зевнул и потребуется существенная доработка и модификация кода. К примеру, в некоторых ситуациях будет полезен механизм наследования приоритетов. Также логика диспетчера сейчас самая примитивная, точно по ходу тестов она будет оттачиваться и усложняться. Буду потихоньку допиливать в свободное от работы время.

Тем не менее, буду рад, если идеи и подходы, изложенные в этой статье вам показались небезынтересными.

Как и всегда, очень рассчитываю на конструктивную критику и встречные идеи.

Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/587000/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *