RTOS ChibiOS/RT – операционная система для микроконтроллеров часть 4.

chibios/rt

RTOS ChibiOS/RT – операционная система для микроконтроллеров.

RTOS ChibiOS/RT:

CHIBIOS/RT – операционная система для микроконтроллеров

CHIBIOS/RT – операционная система для микроконтроллеров часть 2.

CHIBIOS/RT – операционная система для микроконтроллеров часть 3.

Мьютексы и условная переменная

В ChibiOS/RT представлена POSIX совместимая реализация мьютекса и элемента синхронизации условная переменная. POSIX (portable operating system interface for Unix — переносимый интерфейс операционных систем Unix) — набор стандартов, описывающих интерфейсы между операционной системой и прикладной программой (системный API), библиотеку языка C и набор приложений и их интерфейсов. Стандарт создан для обеспечения совместимости различных Unix подобных операционных систем и переносимости прикладных программ на уровне исходного кода, но может быть использован и для не Unix систем. Вместе, мьютекс и условная переменная, образуют высокого уровня конструкцию Монитор.

Мьютексы — это один из вариантов реализации семафора, для организации взаимного исключения. Они реализованы во многих ОС, их основное назначение — организация взаимного исключения для потоков из одного и того же или из разных процессов. Задача мьютекса — защита объекта от доступа к нему других потоков, отличных от того, который завладел мьютексом. В каждый конкретный момент только один поток может владеть объектом, защищенным мьютексом. Если другому потоку будет нужен доступ к переменной, защищенной мьютексом, то этот поток блокируется (засыпает) до тех пор, пока мьютекс не будет освобожден. Цель использования мьютексов — защита данных от повреждения в результате асинхронных изменений (состояние гонки), однако могут порождаться другие проблемы — такие, как взаимная блокировка (deadlock).

Глобальные настройки

На мьютекс и условную переменную влияют следующие глобальные настройки [таблица 1]:

Таблица 1. Глобальные настройки мьютекса .

CH_CFG_USE_MUTEXES Разрешение на использование API мьютекса ядра
CH_CFG_USE_MUTEXES_RECURSIVE Разрешение на использование рекурсивного мьютекса ядра
CH_CFG_USE_CONDVARS Разрешение на использование API элемента синхронизации условная переменная
CH_CFG_USE_CONDVARS_TIMEOUT Разрешение на использование тайм-аута для синхронизации условная переменная

Мьютексы

Мьютексы (mutex, от mutual exclusion— «взаимное исключение») — являются механизмом предназначены для реализации взаимного исключения в самом общем виде. Существует, часто путаница между мьютексом и бинарным семафорам, оба в состоянии решить ту же проблему, что и каждый из них по синхронизации, но есть существенные различия.

Мьютекс имеет атрибут владельца, семафоры не имеют владельцев. Из-за этого мьютекс может быть разблокирован только в том же потоке, поток который захватил мьютекс. Это не требуется для семафора, который может быть разблокирован любым потоком или даже ISR.

Для мьютекса можно реализовать протокол обработки инверсией приоритета зная владельца, требуется для того, чтобы иметь возможность реализовать приоритет наследования или максимальный приоритет для алгоритма. Для мьютекса СhibiOS/RT реализован алгоритм наследования приоритета без каких-либо ограничений на количество потоков или количество вложенных зон взаимного исключения.

Для мьютекса может быть реализован механизм рекурсивные взаимоисключения, это означает, что тот же поток может заблокировать тот же мьютекс несколько раз, а затем должен освободить столько же раз, сколько раз занял его, для дальнейшего выполнения потока. В ChibiOS/RT использование рекурсивного мьютекса разрешается путем активации опции конфигурации CH_CFG_USE_MUTEXES_RECURSIVE.

Поведение мьютекса характеризуются следующей диаграммой состояния [рисунок 1]:

Рисунок 1. Диаграмма состояния работы мьютекса.

RTOS ChibiOS/RT

Существует строгая взаимосвязь между мьютексом и потоком [Рисунок 2]:

RTOS ChibiOS/RT

Рисунок 2. Взаимосвязь работы мьютекса и потока.

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

Таблица 2. API мьютекса.

MUTEX_DECL() Статический инициализатор мьютекса
chMtxObjectInit() Инициализирует мьютекс объект типа mutex_t
chMtxLock() Захватить мьютекс
chMtxLockS() Захватить мьютекс вариант S – класса
chMtxTryLock() Попытка заблокировать мьютекс, если мьютекс уже занят другим потоком, то функция завершается без ожидания
chMtxTryLockS() Попытка заблокировать мьютекс, если мьютекс уже занят другим потоком, то функция завершается без ожидания вариант S – класса
chMtxUnlock() Освободить мьютекс
chMtxUnlockS() Освободить мьютекс вариант S – класса
chMtxUnlockAll() Освободить все мьютексы принадлежащие данному потоку

Пример

         Мьютекс используется единожды, механизм взаимоисключения.

static mutex_t mtx1;
 
static THD_WORKING_AREA(waThread1, 128);
static THD_FUNCTION(Thread1, arg) {
 
  while (true) {
 
    /* Начала работы потока. */
    ...;
 
    /* Запрашиваем доступ к защищенному ресурсу. */
    chMtxLock(&mtx1);
 
    /* Доступ к общему ресурсу. */
    ...;
 
    /* Освобождение ресурса. */
    chMtxUnlock(&mtx1);
 
    /* Продолжение работы потока. */
    ...;
  }
}
 
static THD_WORKING_AREA(waThread2, 128);
static THD_FUNCTION(Thread2, arg) {
 
  while (true) {
 
    /* Начала работы потока. */
    ...;
 
    /* Запрашиваем доступ к защищенному ресурсу. */
    chMtxLock(&mtx1);
 
    /* Доступ к общему ресурсу. */
    ...;
 
    /* Освобождение ресурса. */
    chMtxUnlock(&mtx1);
 
    /* Продолжение работы потока. */
    ...;
  }
}
 
/*
 * Инициализация.
 */
void main(void) {
 
  /*
   * Системная инициализация.
   * Инициализация ядра, потоков,
   * активация RTOS и запуск планировщика.
   */
  chSysInit();
 
  /* Инициализация мьютекса обмена ресурсом. */
  chMtxObjectInit(&mtx1);
 
  /* Начало работы потоков.*/
  chThdCreateStatic(waThread1, sizeof(waThread1), NORMALPRIO + 1, Thread1, NULL);
  chThdCreateStatic(waThread2, sizeof(waThread2), NORMALPRIO + 1, Thread2, NULL);
 
  /* Выполнение. */
  ...;
}

Заметки

Во всех этих ситуациях, когда доступ к совместно используемым ресурсам может быть отсрочен, то рекомендуется использовать chMtxTryLock() вместо chMtxLock(), chMtxTryLock() гораздо более эффективный, не заняв мьютекс, не может войти в состояние ожидания.

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

Условная переменная и Монитор

Переменные условия являются дополнительной конструкцией работы с мьютексами для образования конструкции монитор, которая имеет схожее по поведению с Java synchronized для конструктов.

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

Поведение условной переменной и монитора характеризуются следующей диаграммой состояния [рисунок 3]:

RTOS ChibiOS/RT

Рисунок 3. Диаграмма состояния монитора и условной переменной.

Обратите внимание на то, что функция chCondWait() неявно работает на последнем принятом мьютексе.

Мониторы

Формальный способ понять, как работаю мониторы, как элемент синхронизации, вообразить его как дом с 3 комнатами [рисунок 4].

В доме имеется:

Главная дверь вход в атриум.

Атриум — это очередь мьютекса.

Если мьютекс взят, то открывается дверь в главную комнату.

В зале может находится и оставаться человек в любой момент времени, зона взаимной блокировки.

Дверь выхода, когда освобождается мьютекс.

Дверь в комнату ожидания, ожидание на условной переменной.

Зал ожидания для людей, которые ожидают события, например, звонка, выполнения условия, расположение в очереди условных переменных.

Если произошло событие, тогда мы снова попадаем в атриум.

Запасная дверь есть в зале ожидания, выход из нее по времени, является запасным вариантом.

RTOS ChibiOS/RT

Рисунок 4. Схема «Дом», формальное представление работы Монитора.

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

Обратите внимание, что мониторы могут иметь более одной очереди условной переменной, тогда в этом случае было бы несколько коридоров, приносящие обратно в атриумом.

Шаблон кода

Мониторы должны всегда записывается следуя этому шаблону:

void synchronized(void) {
 
  /* Войти в монитор. */
  chMtxLock(&mtx);
 
  /* Защищенный код до проверки состояния, необязательно. */
  ...;
 
  /* Проверка состояния и удержание в ожидании выполнения условия, пока не будет удовлетворено условию, часть связана с тайм-аут может быть опущена при использовании chCondWait().*/
  while (!condition) {
    msg_t msg = chCondWaitTimeout(&cond1, MS2ST(500));
    if (msg == MSG_TIMEOUT)
      return;
  }
 
  /* Защищенный код, проверка условия - выполнено успешно. */
  ...;
 
  /* Выйти из монитора. */
  chMtxUnlock();
}

Объект переменные условия является просто потоком очереди, очередь назовём “состоянием переменной”, потому что ввод в очередь обычно делается после проверки состояния, которое не является частью самого объекта. Над циклом while строиться работа с переменной состояния [таблица 3].

 Таблица 3. API переменной условия.

CONDVAR_DECL() Статическая инициализация переменной условия
chCondObjectInit() Инициализация объекта переменная условия типа condition_variable_t
chCondSignal() Сигнал переменной условия
chCondSignalI() Сигнал переменной условия (вариант I-класса)
chCondBroadcast() Широковещательный канал для переменной условия
chCondBroadcastI() Широковещательный канал для переменной условия (вариант I-класса)
chCondWait() Заставляет вызывающий поток освободить последний занятый мьютекс и войти в условие переменной очереди ожидания
chCondWaitS() Заставляет вызывающий поток освободить последний занятый мьютекс и войти в условие переменной очереди ожидания (вариант S-класса)
chCondWaitTimeout() Заставляет вызывающий поток освободить последний занятый мьютекс и войти в условие переменной очереди ожидания со спецификацией таймаута
chCondWaitTimeoutS() Заставляет вызывающий поток освободить последний занятый мьютекс и войти в условие переменной очереди ожидания со спецификацией таймаута (вариант S-класса)

 Пример

В этом примере у нас производится синхронизация функций, работающую по кольцевой очереди сообщений.

#define QUEUE_SIZE 128
 
static msg_t queue[QUEUE_SIZE], *rdp, *wrp;
static size_t qsize;
static mutex_t qmtx;
static condition_variable_t qempty;
static condition_variable_t qfull;
 
/*
 * Синхронная инициализация очереди.
 */
void qInit(void) {
 
  chMtxObjectInit(&qmtx);
  chCondObjectInit(&qempty);
  chCondObjectInit(&qfull);
 
  rdp = wrp = &queue[0];
  qsize = 0;
}
 
/*
 * Записываем сообщение в очередь, ожидаем пока очередь заполнена. 
 * Ждем свободного слота.
 */
void qProduce(msg_t msg) {
 
  /* Вхождение в монитор.*/
  chMtxLock(&qmtx);
 
  /* Ожидаем сообщения в очереди. */
  while (qsize >= QUEUE_SIZE)
    chCondWait(&qfull);
 
  /* Записываем сообщение в очередь. */  
  *wr = msg;
  if (++wr >= &queue[QUEUE_SIZE])
    wr = &queue[0];
  qsize++;
 
  /* Передача сигнала о поступлении сообщения. */
  chCondSignal(&qempty);
 
  /* Выход из монитора. */
  chMtxUnlock(&qmtx);
}
 
/*
 * Читаем сообщение из очереди, ждем пока очередь пустая.
 * Процедура ожидания для сообщения.
 */
msg_t qConsume(void) {
  msg_t msg;
 
  /* Входим в монитор. */
  chMtxLock(&qmtx);
 
  /* Ожидаем появления сообщения в очереди. */
  while (qsize == 0)
    chCondWait(&qempty);
 
  /* Читаем сообщение из очереди. */  
  msg = *rd
  if (++rd >= &queue[QUEUE_SIZE])
    rd = &queue[0];
  qsize--;
 
  /* Передача сообщения, что есть свободный слот. */
  chCondSignal(&qfull);
 
  /* Выход из монитора. */
  chMtxUnlock(&qmtx);
 
  return msg;
}

Пример с использованием прерывания

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

#define QUEUE_SIZE 128
 
static msg_t queue[QUEUE_SIZE], *rdp, *wrp;
static size_t qsize;
static mutex_t qmtx;
static condition_variable_t qempty;
 
/*
 * Синхронная инициализация очереди.
 */
void qInit(void) {
 
  chMtxObjectInit(&qmtx);
  chCondObjectInit(&qempty);
 
  rdp = wrp = &queue[0];
  qsize = 0;
}
 
/*
 * Записываем сообщение в очередь, ожидаем пока очередь заполнена.
 * Ждем свободного слота.
 * Обратите внимание, что функция I-класса используется внутри критической секции.
 */
void qProduceFromISR(msg_t msg) {
 
  /* Входим в монитор. */
  chSysLockFromISR();
 
  /* Проверка на место в очереди. */
  if (qsize < QUEUE_SIZE) {
 
    /* Пишем сообщение в очередь. */  
    *wr = msg;
    if (++wr >= &queue[QUEUE_SIZE];
      wr = &queue[0];
    qsize++;
 
    /* Сигнализируем, что есть сообщение в очереди. */
    chCondSignalI(&qempty);
  }
 
  /* Выходим из монитора. */
  chSysUnlockFromISR();
}
 
/*
 * Читаем сообщение из очереди, если очередь пустая, ждем сообщения. 
 * Следует отметить, что функции S-класса используют критическую секцию. 
*/
msg_t qConsume(void) {
  msg_t msg;
 
  /* Входим в монитор, испробуем оба элемента синхронизации критическая секция и мьютекс. */
  chSysLock();
  chMtxLockS(&qmtx);
 
  /* В ожидании сообщений в очереди. */
  while (qsize == 0)
    chCondWaitS(&qempty);
 
  /* Читаем сообщение из очереди. */  
  msg = *rd
  if (++rd >= &queue[QUEUE_SIZE];
    rd = &queue[0];
  qsize--;
 
  /* Выходим из монитора. */
  chMtxUnlockS(&qmtx);
  chSysUnlock();
 
  return msg;
}