概要
セマフォ(Semaphore)は、複数のタスク(スレッド)間で共有資源へのアクセスを制御したり、タスクの実行順序を同期するために使うカウンタ型の同期プリミティブです。「信号機(semaphore)」の名の通り、資源へのアクセスを許可・禁止するシグナリング機構として機能します。
1965年にEdsger W. Dijkstraが提案した古典的な概念で、P(取得、Wait)とV(解放、Signal)の2つの操作で定義されます。RTOSではこれがtake(pend)とgive(post)として実装されています。
カウンタの初期値と最大値によって用途が変わります。
- バイナリセマフォ(Binary Semaphore): カウンタが0または1。タスク間の同期通知に使用
- カウンティングセマフォ(Counting Semaphore): カウンタが0〜N。複数の同一資源の管理に使用
ミューテックスとの違いは、セマフォは「所有権」の概念がない点です。あるタスクがtakeしたセマフォを別のタスクがgiveすることができます。この特性から、セマフォはタスク間の「通知・同期」に使い、資源の排他制御(ロック)には所有権のあるミューテックスを使うのが設計の基本です。
歴史・背景
セマフォはEdsger W. Dijkstraが1962〜65年にオランダの技術高校(Eindhoven University of Technology)でEDGEOSを開発した際に考案しました。1965年の論文「Cooperating Sequential Processes」で公開されたこの概念は、並行プログラミングの基礎理論を形成し、その後のOS・RTOSのすべてに影響を与えています。
Dijkstraはオランダ語のProberen(試みる)の頭文字を取ってP操作、Verhogen(増やす)の頭文字を取ってV操作と名付けました。POSIX規格ではsem_wait()(P操作)とsem_post()(V操作)として標準化されています。
組み込みRTOSへの応用は1970〜80年代の商用RTOS(VRTX、AMX等)から始まり、現在のFreeRTOS・Zephyr・TOPPERS-ITRONにも引き継がれています。
技術仕様
FreeRTOSのセマフォ種別と用途
| 種別 | 作成関数 | 初期値 | 主な用途 |
|---|---|---|---|
| バイナリセマフォ | xSemaphoreCreateBinary() | 0 | ISR→タスクの通知・同期 |
| カウンティングセマフォ | xSemaphoreCreateCounting(max, init) | 指定値 | 複数資源の管理・イベントカウント |
| ミューテックス | xSemaphoreCreateMutex() | 1 | 資源の排他制御(優先度継承付き) |
| 再帰ミューテックス | xSemaphoreCreateRecursiveMutex() | 1 | 同タスクから複数回取得可能な排他制御 |
POSIX セマフォAPI
Linux(組み込みLinux含む)での標準的なセマフォAPI:
#include <semaphore.h>
sem_t sem;
sem_init(&sem, 0, 1); /* 初期値1のセマフォ(0=プロセス内共有) */
sem_wait(&sem); /* P操作(取得、カウンタを-1) */
sem_post(&sem); /* V操作(解放、カウンタを+1) */
sem_trywait(&sem); /* ノンブロッキングP操作 */
sem_timedwait(&sem, &ts); /* タイムアウト付きP操作 */
sem_destroy(&sem); /* セマフォの破棄 */
セマフォ操作の時間計算量
FreeRTOSのセマフォ操作はO(1)で実行されます。ただしブロッキング操作では、ブロックされていたタスクの起床処理がスケジューラに依存します。
動作原理
バイナリセマフォによるISR→タスク同期
/* バイナリセマフォ(ISRからタスクへの通知) */
SemaphoreHandle_t dataSemaphore;
void setup(void) {
dataSemaphore = xSemaphoreCreateBinary();
/* 初期値0(誰も与えていない状態) */
xTaskCreate(processTask, "Process", 256, NULL, 3, NULL);
}
/* ISR:データ準備完了を通知(セマフォをGive) */
void ADC_IRQHandler(void) {
adc_raw_value = ADC1->DR; /* データ読み取り */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* タスクへセマフォを給与(通知) */
xSemaphoreGiveFromISR(dataSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/* タスク:セマフォを待機してデータ処理 */
void processTask(void *pvParam) {
for (;;) {
/* セマフォが与えられるまで待機(ブロック) */
if (xSemaphoreTake(dataSemaphore, pdMS_TO_TICKS(100)) == pdTRUE) {
processADCValue(adc_raw_value);
} else {
/* タイムアウト:センサ異常の可能性 */
handleSensorError();
}
}
}
この例で注目すべき点:ISRがGiveし、タスクがTakeするという「違うコンテキスト」での操作がセマフォの典型的な使い方です。所有権がないためこれが可能です。
カウンティングセマフォによる資源管理
/* 3つのDMAチャネルを管理するカウンティングセマフォ */
#define NUM_DMA_CHANNELS 3
SemaphoreHandle_t dmaChannelSem;
void initDMA(void) {
/* 初期値3(3チャネル利用可能) */
dmaChannelSem = xSemaphoreCreateCounting(NUM_DMA_CHANNELS, NUM_DMA_CHANNELS);
}
void taskA(void *pvParam) {
for (;;) {
/* DMAチャネルが空くまで待機(カウンタ-1) */
xSemaphoreTake(dmaChannelSem, portMAX_DELAY);
/* DMAを使った転送 */
startDMATransfer_A();
waitDMAComplete_A();
/* DMAチャネルを解放(カウンタ+1) */
xSemaphoreGive(dmaChannelSem);
vTaskDelay(10);
}
}
/* taskB, taskCも同様。3タスクが同時にDMAを使おうとすると、
カウンタが0になり4番目以降はブロックされる */
セマフォとミューテックスの使い分け
/* 適切な使い分け */
/* ケース1:タスク間の同期通知 → バイナリセマフォ */
/* 生産者タスクが作成完了を消費者タスクへ通知 */
SemaphoreHandle_t notifySem = xSemaphoreCreateBinary();
/* 生産者 */ xSemaphoreGive(notifySem);
/* 消費者 */ xSemaphoreTake(notifySem, portMAX_DELAY);
/* ケース2:共有資源の排他制御 → ミューテックス(所有権あり) */
/* 同じタスクがTakeしてGiveする必要がある場合 */
MutexHandle_t sharedMutex = xSemaphoreCreateMutex();
/* タスク内 */ xSemaphoreTake(sharedMutex, portMAX_DELAY);
/* ... 共有資源を使用 ... */
/* 同じタスク内 */ xSemaphoreGive(sharedMutex); /* Takeしたタスクと同じ */
用途・ユースケース
ISRとタスク間の同期(最も一般的な用途)
セマフォの最も典型的な組み込み用途は、ISRからタスクへのイベント通知です。
シナリオ:UARTでコマンド受信
1. UART ISR がデータを受信 → バイナリセマフォをGive
2. 受信処理タスクがセマフォをTake → データ解析
3. セマフォは0に戻る(次のデータ待ち)
複数タスクからの共有バッファ書き込み制限
/* 書き込みバッファへの同時アクセス数を1に制限 */
SemaphoreHandle_t writeSem = xSemaphoreCreateBinary();
xSemaphoreGive(writeSem); /* 初期値1(許可状態)にする */
void writeTask(void *pvParam) {
xSemaphoreTake(writeSem, portMAX_DELAY); /* バイナリセマフォで1個だけ許可 */
writeToBuffer(); /* クリティカルな書き込み処理 */
xSemaphoreGive(writeSem);
}
/* ただし所有権がないため、別タスクがGiveすることが可能 → 排他目的にはMutex推奨 */
イベントカウント(バッファオーバーフロー監視)
/* 処理待ちイベント数をカウント */
SemaphoreHandle_t eventCount = xSemaphoreCreateCounting(100, 0);
void sensor_isr(void) {
BaseType_t xHPTW = pdFALSE;
xSemaphoreGiveFromISR(eventCount, &xHPTW); /* イベント++(カウンタ+1) */
portYIELD_FROM_ISR(xHPTW);
}
void processingTask(void *pvParam) {
for (;;) {
xSemaphoreTake(eventCount, portMAX_DELAY); /* イベント待機(カウンタ-1) */
processOneEvent(); /* 1イベントを処理 */
}
}
/* カウンティングセマフォにより、ISRが連続して発火しても
イベントが失われずキューイングされる */
実装・開発のポイント
セマフォに関するアンチパターン
-
デッドロックの回避 複数のセマフォを決まった順序で取得します。順序が逆になるとデッドロックが発生します。
-
資源の排他制御にはセマフォを使わない バイナリセマフォには所有権がないため、誤って別タスクがGiveしてしまうリスクがあります。排他制御には必ずミューテックスを使います。
-
ISR内でのブロッキング操作禁止 ISR内では
xSemaphoreTake()(ブロッキング)を呼んではいけません。xSemaphoreGiveFromISR()を使います。
タイムアウトの設定
/* タイムアウトの使い方 */
TickType_t timeout = pdMS_TO_TICKS(100); /* 100msのタイムアウト */
if (xSemaphoreTake(sem, timeout) == pdTRUE) {
/* セマフォを取得 */
} else {
/* タイムアウト → エラー処理 */
handle_timeout_error();
}
/* 永久待機(注意:デッドロック時に検出不能) */
xSemaphoreTake(sem, portMAX_DELAY);
/* ノンブロッキング(即時失敗) */
if (xSemaphoreTake(sem, 0) == pdTRUE) { /* タイムアウト0 */ }
セマフォとタスク通知の比較
FreeRTOS 8.2以降では、タスク通知(xTaskNotifyGive/ulTaskNotifyTake)がバイナリセマフォの代替として使えます。セマフォより高速(オブジェクト生成不要)で、タスクごとに1つの通知カウンタを持ちます。
/* タスク通知(バイナリセマフォの代替・高速) */
/* ISR側 */
vTaskNotifyGiveFromISR(targetTaskHandle, &xHPTW);
/* タスク側 */
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); /* pdTRUE: 通知後カウンタをクリア */
他技術との比較
| 観点 | バイナリセマフォ | カウンティングセマフォ | ミューテックス | タスク通知 |
|---|---|---|---|---|
| 主な用途 | ISR→タスク通知・同期 | 複数資源管理 | 資源の排他制御 | タスク間の高速通知 |
| 所有権 | なし | なし | あり | なし |
| 優先度継承 | なし | なし | あり(FreeRTOS) | なし |
| ISRから操作 | 可(FromISR API) | 可 | 不可 | 可(FromISR API) |
| 速度 | 中 | 中 | 中 | 速い(オブジェクト不要) |
| カウント機能 | なし(0/1のみ) | あり(0〜N) | なし | あり(1カウンタ) |
セマフォはリアルタイムシステムの同期・通知の根幹を担う仕組みです。用途に応じてバイナリセマフォ・カウンティングセマフォ・ミューテックスを使い分けることで、安全かつ効率的なマルチタスクシステムを構築できます。