FreeRTOS: セマフォとミューテックス

マルチタスクにおいて避けられない、タスク間の同期・排他制御プリミティブAPIであるセマフォ(Semaphore)とミューテックス(Mutex)について調べる。

セマフォ(Semaphore)と遅延割込み処理

セマフォとは「手旗信号」のことであり、旗を上げる(up)と通ってよし、降ろす(down)と止まれという、タスクの流れを制御する仕組みのことである。これを使うことで複数タスクが同時に動作するとまずいクリティカルセクションの排他制御や、限りある資源へのアクセス制御(条件同期。いわゆる生産者・消費者問題)が可能になる。up/down操作をgive/takeやsignal/waitと呼ぶこともある。FreeRTOSのAPIはxSemaphoreGive/xSemaphoreTakeである。

セマフォで1プロセスだけブロックできるもの(キュー長が1)をバイナリセマフォと呼ぶことがある。この場合、2つ以上のタスクをブロックできるセマフォをカウンティングセマフォと呼び区別する。

ここではセマフォのユースケースとして、遅延割込み処理(deferred interrupt processing)を取り上げる。割込み禁止区間を短くして応答性を高めるためにも、ISRは極力小さく保ち、タスクで遅延処理するのが一般的なベストプラクティスである。UNIXだとソフト割込みと呼ばれカーネルスレッドで遅延割込み処理が実行される。FreeRTOSの場合は、遅延割込み処理自体は他のアプリケーションタスクと変わらない。ISRと遅延割込み処理タスクをハンドオーバーするために、バイナリセマフォを使うこともできる。タスク側ではセマフォをtakeしてブロッキングして、ISR側は割込みをハンドリングするとセマフォをgiveすることでタスクを起床させる。これは一種の条件同期である。なお、ISR内でセマフォをgiveするときはxSemaphoreGiveFromISR関数という割込み安全なAPIを使わねばならない。

上記の例をサンプルコードで示す。一部関数はWindows版シミュレータにしか実装されていないのでちゃんと動かないが、周期タスク内で直接セマフォをgiveして誤魔化している。

/* Kernel includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "semphr.h"

/* Local includes. */
#include "console.h"

#define WINDOWS_PORT            0
#define mainINTERRUPT_NUMBER    3

SemaphoreHandle_t xBinarySemaphore;

static void vPeriodicTask(void *pvParameters)
{
   const TickType_t xDelay500ms = pdMS_TO_TICKS(500UL);

   for (;;) {
       vTaskDelay(xDelay500ms);

       console_print("Periodic task - About to generate an interrupt.\r\n");
#if WINDOWS_PORT
       vPortGenerateSimulatedInterrupt(mainINTERRUPT_NUMBER);
#else
       xSemaphoreGive(xBinarySemaphore);
#endif
       console_print("Periodic task - Interrupt generated.\r\n");
   }
}

#if WINDOWS_PORT
/* ISR */
static uint32_t ulExampleInterruptHandler(void)
{
   BaseType_t xHigherPriorityTaskWoken;

   xHigherPriorityTaskWoken = pdFALSE;
   xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
   portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
#endif

/* Software interrupt handler */
static void vHandlerTask(void *pvParameters)
{
   for (;;) {
       xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
       console_print("Handle task - Processing event.\r\n");
   }
}

int main_semphr(void)
{
   xBinarySemaphore = xSemaphoreCreateBinary();
   if (xBinarySemaphore != NULL) {
       xTaskCreate(vHandlerTask, "Handler", 1000, NULL, 3, NULL);
       xTaskCreate(vPeriodicTask, "Periodic", 1000, NULL, 1, NULL);
#if WINDOWS_PORT
       vPortSetInterruptHandler(mainINTERRUPT_NUMBER, ulExampleInterruptHandler);
#endif

       vTaskStartScheduler();
   }


   for (;;);
}

なお、FreeRTOSにはタスク通知(Task notification)APIってのがあって、セマフォを使うより高速で省メモリフットプリントにタスク間でイベント通知を送ることができるようだ。

ミューテックス(Mutex)と優先度継承

ミューテックスはバイナリセマフォと基本的には同じだが、優先度逆転(Priority Inversion)問題に対応するための優先度継承(Priority Inheritance)プロトコルをサポートしている点が異なる。一方で制限もあって、セマフォのコード例で示したようにISR内で使ってはいけない。

優先度逆転問題はこれまたOSの教科書では典型的な話題である。優先度が異なる3つのタスクがあり、高優先度タスクと低優先度タスクが資源を共有しているとする。低優先度タスクが共有資源を保持し(ミューテックスをtakeする)、その操作が終わるまで(giveするまで)高優先度タスクがブロックされた場合、中優先度タスクが優先実行され、結果的に高優先度タスクの実行が遅れてしまう。このように高優先度タスクと中優先度タスクには直接依存関係がないにも関わらず、中優先度タスクが優先されて実行されてしまう現象を優先度逆転と呼ぶ。

これを解決する手法はいくつかあるが、優先度継承は実装が簡単なものの一つ。簡単に言うと、高優先度タスクがミューテックスをtakeしてブロックするときに、ミューテックスを所有するタスクの優先度を高優先度タスクのものに一時的に上げる。次にその実装を見てみよう。

まずFreeRTOSでは、セマフォもミューテックスもキューのバリエーションである。include/semphr.hを抜粋したのが以下の定義になる。

typedef QueueHandle_t SemaphoreHandle_t;

#define xSemaphoreCreateBinary()   xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount )    xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
#define xSemaphoreCreateMutex()    xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )

#define xSemaphoreGive( xSemaphore )    xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
#define xSemaphoreTake( xSemaphore, xBlockTime )    xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

他にもXXXCreate関数はxQueueGenericCreate関数に行き着く。

優先度継承の実装はざっくり以下の通り。
ミューテック構造体(struct QueueDefinition内のu.xSemaphore変数)には、takeしているタスクのハンドラ(TCB)が格納されている。また、タスクハンドラには、現在の優先度uxPriorityの他に、優先度継承前の優先度を覚えておくためのuxBasePriorityがある。xQueueSemaphoreTake関数で、ミューテックスをtakeする場合はxTaskPriorityInherit関数を呼ぶ。また、ミューテックスをtakeしているタスクがなくなった場合は、xTaskPriorityDisinherit関数を読んで、優先度を元に戻す。

この記事が気に入ったらサポートをしてみませんか?