FreeRTOS 開発者ガイド

リソース管理

このセクションでは、以下について説明します。

  • リソースの管理および制御が必要な状況と理由。
  • クリティカルセクションの概要。
  • 相互排他の意味。
  • スケジューラの停止の意味。
  • ミューテックスを使用する方法。
  • ゲートキーパータスクを作成して使用する方法。
  • 優先度の逆転の意味と、その影響を優先度の継承で低減 (除去ではなく) する方法。

マルチタスクシステムでは、あるタスクがリソースへのアクセスを開始し、そのアクセスが完了する前にタスクが実行中状態から移行されると、エラーが発生する可能性があります。タスクが一貫しない状態でリソースを離れると、他のタスクや割り込みから同じリソースにアクセスしたときに、データの破損などの問題が発生する場合があります。

例は以下のとおりです。

  1. 周辺機器へのアクセス次のシナリオで、2 つのタスクが液晶ディスプレイ (LCD) に書き込む場合を考えます。タスク A が実行され、LCD への文字列「Hello world」の書き込みを開始します。文字列の最初の部分「Hello w」を出力した時点で、タスク A はタスク B に取って代わられます。タスク B は LCD に「Abort, Retry, Fail?」と書き込んだ後で、ブロック状態に入ります。タスク A は取って代わられたところから続行し、文字列の残りの部分「orld」を出力します。LCD には、破損した文字列「Hello wAbort, Retry, Fail?orld」が表示されます。
  2. 読み取り、変更、書き込みオペレーション

以下に C コードの行と、この行をアセンブリコードに変換する方法の一般的な例を示します。PORTA の値が、最初にメモリからレジスタに読み取られ、レジスタで変更された後で、メモリに書き戻されているのが確認できます。これは、読み取り、変更、書き込みオペレーションと呼ばれます。

/* The C code being compiled. */

PORTA |= 0x01;

/* The assembly code produced when the C code is compiled. */

LOAD R1,[#PORTA] ; Read a value from PORTA into R1

MOVE R2,#0x01 ; Move the absolute constant 1 into R2

OR R1,R2 ; Bitwise OR R1 (PORTA) with R2 (constant 1)

STORE R1,[#PORTA] ; Store the new value back to PORTA

これは、オペレーションを完了するために複数の命令を使用し、他のオペレーションが割り込むことができるため、非アトミックオペレーションです。次のシナリオで、2 つのタスクが PORTA というメモリにマップされたレジスタを更新する場合を考えます。

  1. タスク A が PORTA の値 (オペレーションの読み取り部分) をレジスタ内にロードします。
  2. タスク A は、同じオペレーションの変更部分と書き込み部分を完了する前に、タスク B に取って代わられます。
  3. タスク B が PORTA の値を更新し、ブロック状態に入ります。
  4. タスク A が、取って代わられたところから続行します。タスク A は、レジスタにすでに保持している PORTA 値のコピーを変更し、その更新した値を PORTA に書き戻します。

このシナリオで、タスク A は PORTA の古い値を更新して書き戻します。タスク A が PORTA 値のコピーを作成した時点から、これを変更して PORTA レジスタに書き戻す時点までの間に、タスク B が PORTA を変更します。タスク A が PORTA に書き込むと、タスク B がすでに実行した変更が上書きされ、PORTA レジスタの値が破損します。

この例では周辺機器レジスタを対象にしていますが、変数を対象にして読み取り、変更、書き込みオペレーションを実行した場合にも同じことが該当します。

  1. 変数に対する非アトミックアクセス非アトミックオペレーションの例としては、構造体の複数のメンバーを更新する場合や、アーキテクチャの自然語サイズより大きい変数を更新する (16 ビットマシンで 32 ビット変数を更新するなど) 場合が該当します。これらの更新が停止されると、データの損失や破損が発生する場合があります。
  2. 関数のリエントラント性関数は、複数のタスクから、またはタスクと割り込みの両方から呼び出すことができる場合、リエントラントです。リエントラント関数は、データや論理オペレーションの破損のリスクなしに複数の実行スレッドからアクセスできるため、スレッドセーフであると言われます。各タスクは、独自のスタックと独自のプロセッサ (ハードウェア) レジスタ値のセットを保持します。関数は、スタックに格納されているデータや、レジスタに保持されているデータにのみアクセスし、その他のデータにアクセスしない場合、リエントラントでスレッドセーフです。リエントラント関数の例は次のとおりです。
  3. /* A parameter is passed into the function. This will either be passed on the stack, or in a processor register. Either way is safe because each task or interrupt that calls the function maintains its own stack and its own set of register values, so each task or interrupt that calls the function will have its own copy of lVar1. */long lAddOneHundred( long lVar1 ){/* This function scope variable will also be allocated to the stack or a register, depending on the compiler and optimization level. Each task or interrupt that calls this function will have its own copy of lVar2. */long lVar2;lVar2 = lVar1 + 100;return lVar2;}

リエントラントでない関数の例は次のとおりです。

/* In this case lVar1 is a global variable, so every task that calls lNonsenseFunction will access the same single copy of the variable. */

long lVar1;

long lNonsenseFunction( void )

{

/* lState is static, so is not allocated on the stack. Each task that calls this function will access the same single copy of the variable. */

static long lState = 0;

long lReturn;

switch( lState )

{

case 0 : lReturn = lVar1 + 10;

lState = 1;

break;

case 1 : lReturn = lVar1 + 20;

lState = 0;

break;

}

}

相互排他

データの整合性を常に維持するには、相互排他手法を使用してタスク間またはタスクと割り込み間で共有されるリソースへのアクセスを管理します。これにより、タスクがアクセスを開始した共有リソースがリエントラントおよびスレッドセーフでない場合、同じタスクが当該リソースへの排他的なアクセスを保持することで、このリソースを一貫した状態に戻します。

相互排他を実装するには、FreeRTOS が提供するいくつかの機能を使用できます。ただし、相互排他を実現する最善の方法は、現実的な限り、リソースを共有しないことです。そのためには、各リソースにアクセスするタスクが 1 つのみであるようにアプリケーションを設計します。

クリティカルセクションとスケジューラの停止

基本的なクリティカルセクション

基本的なクリティカルセクションは、taskENTER_CRITICAL() マクロと taskEXIT_CRITICAL() マクロへのそれぞれの呼び出しで囲まれたコード領域です。クリティカルセクションはクリティカル領域とも呼ばれます。

taskENTER_CRITICAL() および taskEXIT_CRITICAL() はパラメータや戻り値を使用しません(関数に似たマクロは実際の関数と同じように値を返すことはありません。ただし、マクロを関数と同じものと理解するのが最も簡単です)。

クリティカルセクションの使い方を以下に示します。この例では、レジスタへのアクセスを保護するためにクリティカルセクションを使用しています。

/* Ensure access to the PORTA register cannot be interrupted by placing it within a critical section. Enter the critical section. */

taskENTER_CRITICAL();

/* A switch to another task cannot occur between the call to taskENTER_CRITICAL() and the call to taskEXIT_CRITICAL(). Interrupts might still execute on FreeRTOS ports that allow interrupt nesting, but only interrupts whose logical priority is above the value assigned to the configMAX_SYSCALL_INTERRUPT_PRIORITY constant. Those interrupts are not permitted to call FreeRTOS API functions. */

PORTA |= 0x01;

/* Access to PORTA has finished, so it is safe to exit the critical section. */

taskEXIT_CRITICAL();

いくつかの例では vPrintString() という関数を使用して文字列を標準出力に書き込みます。標準出力は、FreeRTOS Windows を使用している場合はターミナルウィンドウです。vPrintString() はさまざまなタスクから呼び出されるため、理論的には、その実装でクリティカルセクションを使用することで、標準出力へのアクセスを保護できます。以下に例を示します。

void vPrintString( const char *pcString )

{

/* Write the string to stdout, using a critical section as a crude method of mutual exclusion. */

taskENTER_CRITICAL();

{

printf( “%s”, pcString );

fflush( stdout );

}

taskEXIT_CRITICAL();

}

この方法でクリティカルセクションを実装することは、相互排他を提供する初歩的な手段です。具体的には、割り込みを完全に無効にするか、使用する FreeRTOS ポートに応じて configMAX_SYSCALL_INTERRUPT_PRIORITY で設定した優先度までの割り込みを無効にします。プリエンプティブなコンテキストの切り替えが発生するのは割り込み内に限られるため、割り込みが無効のままであれば、taskENTER_CRITICAL() を呼び出したタスクはクリティカルセクションが終了するまで実行中状態に留まることが保証されます。

基本的なクリティカルセクションは非常に短くする必要があります。そうしないと、割り込みの応答時間に悪影響を及ぼす場合があります。taskENTER_CRITICAL() への各呼び出しは、taskEXIT_CRITICAL() への呼び出しと確実に対応させる必要があります。このため、標準出力 (stdout またはコンピュータが出力データを書き込むストリーム) の保護には、クリティカルセクションを使用しません (上のコードを参照)。ターミナルへの書き込みオペレーションは長びく可能性があるためです。このセクションの例では他のソリューションを試しています。

カーネルはネストの深さのカウントを保持するため、クリティカルセクションを安全にネストできます。クリティカルセクションを終了するのは、ネストの深さがゼロに戻ったときに限ります。これは、先行する taskENTER_CRITICAL() への呼び出しごとに taskEXIT_CRITICAL() への 1 つの呼び出しが実行されたときです。

taskENTER_CRITICAL() および taskEXIT_CRITICAL() は、FreeRTOS が実行されているプロセッサの割り込み有効化状態をタスクで変更する唯一の正式な方法です。その他の方法で割り込み有効化状態を変更すると、マクロのネストカウントが無効になります。

taskENTER_CRITICAL() および taskEXIT_CRITICAL() は「FromISR」で終わっていないため、割り込みサービスルーチンから呼び出さないでください。taskENTER_CRITICAL() の割り込みセーフバージョンは taskENTER_CRITICAL_FROM_ISR() です。taskEXIT_CRITICAL() の割り込みセーフバージョンは taskEXIT_CRITICAL_FROM_ISR() です。割り込みセーフバージョンは、割り込みのネストを許可する FreeRTOS ポート専用です。割り込みのネストを許可しないポートでは無効です。

taskENTER_CRITICAL_FROM_ISR() から返される値は、次に示すように、対応する taskEXIT_CRITICAL_FROM_ISR() への呼び出しに渡す必要があります。

void vAnInterruptServiceRoutine( void )

{

/* Declare a variable in which the return value from taskENTER_CRITICAL_FROM_ISR() will be saved. */

UBaseType_t uxSavedInterruptStatus;

/* This part of the ISR can be interrupted by any higher priority interrupt. */

/* Use taskENTER_CRITICAL_FROM_ISR() to protect a region of this ISR. Save the value returned from taskENTER_CRITICAL_FROM_ISR() so it can be passed into the matching call to taskEXIT_CRITICAL_FROM_ISR(). */

uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();

/* This part of the ISR is between the call to taskENTER_CRITICAL_FROM_ISR() and taskEXIT_CRITICAL_FROM_ISR(), so can only be interrupted by interrupts that have a priority above that set by the configMAX_SYSCALL_INTERRUPT_PRIORITY constant. */

/* Exit the critical section again by calling taskEXIT_CRITICAL_FROM_ISR(), passing in the value returned by the matching call to taskENTER_CRITICAL_FROM_ISR(). */

taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );

/* This part of the ISR can be interrupted by any higher priority interrupt. */

}

クリティカルセクションで保護するコードに費やす処理時間よりも、クリティカルセクションに出入りするコードの実行に多くの処理時間を費やすのは無駄です。基本的なクリティカルセクションは入って出るまでが非常に高速で、常に決定論的であるため、保護するコード領域が非常に短い場合に使用するのが理想的です。

スケジューラの停止 (またはロック)

クリティカルセクションは、スケジューラを停止して作成することもできます。スケジューラの停止は、スケジューラのロックとも呼ばれます

基本的なクリティカルセクションは、他のタスクや割り込みによるアクセスからコード領域を保護します。スケジューラの停止で実装されるクリティカルセクションは、他のタスクによるアクセスからのみコード領域を保護します。割り込みは有効なまま残ります。

割り込みを無効にするだけではクリティカルセクションが長すぎて実装できない場合は、代わりにスケジューラを停止して実装できます。ただし、スケジューラの停止中に割り込みアクティビティがあると、スケジューラの再開 (または停止解除) のオペレーションが長びく可能性があります。状況に応じて最適な方法を使い分けてください。

vTaskSuspendAll() API 関数

vTaskSuspendAll() API 関数のプロトタイプは以下のとおりです。
void vTaskSuspendAll( void );
スケジューラを停止するには vTaskSuspendAll() を呼び出します。スケジューラを停止すると、コンテキストの切り替えは発生しなくなりますが、割り込みは有効のまま残ります。スケジューラの停止中に割り込みからコンテキスト切り替えのリクエストがあると、リクエストは保留され、スケジューラが再開 (停止解除) された場合にのみ実行されます。
スケジューラの停止中は FreeRTOS API 関数を呼び出すことができません。

xTaskResumeAll() API 関数

xTaskResumeAll() API 関数のプロトタイプは以下のとおりです。

BaseType_t xTaskResumeAll( void );

スケジューラを再開 (停止解除) するには xTaskResumeAll() を呼び出します。

次の表は、xTaskResumeAll() の戻り値を示しています。

戻り値説明
戻り値スケジューラの停止中にリクエストされたコンテキスト切り替えは保留され、スケジューラの再開後にのみ実行されます。xTaskResumeAll() が戻る前に、保留中のコンテキスト切り替えが実行されると、pdTRUE が返されます。それ以外の場合は pdFALSE が返されます。
カーネルはネストの深さのカウントを保持するため、vTaskSuspendAll() および xTaskResumeAll() への呼び出しを安全にネストできます。スケジューラが再開されるのは、ネストの深さがゼロに戻ったときに限られます。これは、先行する vTaskSuspendAll() への呼び出しごとに xTaskResumeAll() への 1 つの呼び出しが実行されたときです。


次のコードは、vPrintString() の実装を示しています。これにより、スケジューラが停止されて、ターミナル出力へのアクセスが保護されます。


void vPrintString( const char *pcString )


{


/* Write the string to stdout, suspending the scheduler as a method of mutual exclusion. */


vTaskSuspendScheduler();


{


printf( “%s”, pcString );


fflush( stdout );


}


xTaskResumeScheduler();


}

ミューテックス (およびバイナリセマフォ)

ミューテックス (MUTual EXclusion) は、複数のタスク間でリソースを共有する場合のアクセス制御に使用する特殊なタイプのバイナリセマフォです。ミューテックスを使用可能にするには、FreeRTOSConfig.h で configUSE_MUTEXES を 1 に設定する必要があります。

相互排他シナリオで使用する場合、ミューテックスは共有リソースに関連付けられたトークンと考えることができます。タスクは、リソースに正当にアクセスするために、まずトークンを正常に取得する (トークンホルダーになる) 必要があります。トークンホルダーは、リソースを使い終わったら、そのトークンを解放する必要があります。トークンが解放された場合にのみ、別のタスクはそのトークンを正常に取得し、同じ共有リソースに安全にアクセスできます。タスクは、トークンを保持している場合にのみ、共有リソースへのアクセスを許可されます。

ミューテックスとバイナリセマフォは多くの特徴を共有していますが、ミューテックスを相互排他に使用するシナリオは、バイナリセマフォを同期化に使用するシナリオとはまったく異なります。特に、取得後のセマフォがどうなるかが大きく異なります。

  • 相互排他に使用するセマフォは常に解放する必要があります。
  • 同期化に使用するセマフォは通常破棄され、解放されません。

このメカニズムは、専らアプリケーションライターの統制により機能します。各タスクは随時にリソースにアクセスできないというわけではなく、ミューテックスホルダーになることができる場合を除いては、あえてリソースにアクセスしないということです。

xSemaphoreCreateMutex() API 関数

FreeRTOS V9.0.0 には、xSemaphoreCreateMutexStatic() 関数も含まれています。この関数により、ミューテックスの静的な作成に必要なメモリがコンパイル時に割り当てられます。ミューテックスは一種のセマフォです。各種の FreeRTOS セマフォへのハンドルは、SemaphoreHandle_t 型の変数に保存されます。

ミューテックスは、使用する前に作成する必要があります。ミューテックス型のセマフォを作成するには、xSemaphoreCreateMutex() API 関数を使用します。

xSemaphoreCreateMutex() API 関数のプロトタイプは以下のとおりです。

SemaphoreHandle_t xSemaphoreCreateMutex( void );

次の表は、xSemaphoreCreateMutex() の戻り値を示しています。

セマフォを使用するための vPrintString() の書き換え (例 20)

次の例では、新しいバージョンの vPrintString() として prvNewPrintString() を作成し、この新しい関数を複数のタスクから呼び出します。prvNewPrintString() は機能的に vPrintString() と同等ですが、スケジューラをロックする代わりにミューテックスを使用して標準出力へのアクセスを制御します。

prvNewPrintString() の実装は以下のとおりです。

static void prvNewPrintString( const char *pcString )

{

/* The mutex is created before the scheduler is started, so already exists by the time this task executes. Attempt to take the mutex, blocking indefinitely to wait for the mutex if it is not available right away. The call to xSemaphoreTake() will only return when the mutex has been successfully obtained, so there is no need to check the function return value. If any other delay period was used, then the code must check that xSemaphoreTake() returns pdTRUE before accessing the shared resource (which in this case is standard out). Indefinite timeouts are not recommended for production code. */

xSemaphoreTake( xMutex, portMAX_DELAY );

{

/* The following line will only execute after the mutex has been successfully obtained. Standard out can be accessed freely now because only one task can have the mutex at any one time. */

printf( “%s”, pcString );

fflush( stdout );

/* The mutex MUST be given back! */

}

xSemaphoreGive( xMutex );

}

prvNewPrintString() は、prvPrintTask() で実装されたタスクの 2 つのインスタンスによって繰り返し呼び出されます。各呼び出しの間でランダムな遅延時間が使用されます。タスクの各インスタンスに一意の文字列を渡すには、タスクのパラメータを使用します。prvPrintTask() の実装は以下のとおりです。

static void prvPrintTask( void *pvParameters )

{

char *pcStringToPrint;

const TickType_t xMaxBlockTimeTicks = 0x20;

/* Two instances of this task are created. The string printed by the task is passed into the task using the task’s parameter. The parameter is cast to the required type. */

pcStringToPrint = ( char * ) pvParameters;

for( ;; )

{

/* Print out the string using the newly defined function. */

prvNewPrintString( pcStringToPrint );

/* Wait a pseudo random time. Note that rand() is not necessarily reentrant, but in this case it does not really matter because the code does not care what value is returned. In a more secure application, a version of rand() that is known to be reentrant should be used or calls to rand() should be protected using a critical section. */

vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );

}

}

通常どおり、main() は単にミューテックスとタスクを作成し、次にスケジューラを起動します。

prvPrintTask() の 2 つのインスタンスは異なる優先度で作成されるため、優先度の低いタスクは優先度の高いタスクに取って代わられる場合があります。ミューテックスにより各タスクはターミナルに相互排他的にアクセスするため、プリエンプションが発生した場合でも、文字列は正しく表示され、破損されることはありません。タスクがブロック状態で費やす最大時間を減らすことで、プリエンプションの頻度を増やすことができます。この設定は xMaxBlockTimeTicks 定数で行います。

FreeRTOS Windows ポートで例 20 を使用する場合、以下の点に注意してください。

  • printf() を呼び出すと、Windows システム呼び出しが生成されます。Windows システム呼び出しは、FreeRTOS の制御の範囲外であり、不安定な状態が発生することがあります。
  • Windows システム呼び出しの実行方式では、ミューテックスを使用しなくても、文字列の破損はほとんど発生しません。

int main( void )

{

/* Before a semaphore is used it must be explicitly created. In this example, a mutex type semaphore is created. */

xMutex = xSemaphoreCreateMutex();

/* Check that the semaphore was created successfully before creating the tasks. */

if( xMutex != NULL )

{

/* Create two instances of the tasks that write to stdout. The string they write is passed in to the task as the task’s parameter. The tasks are created at different priorities so some preemption will occur. */

xTaskCreate( prvPrintTask, “Print1”, 1000, “Task 1 ***************************************\r\n”, 1, NULL );

xTaskCreate( prvPrintTask, “Print2”, 1000, “Task 2 —————————————\r\n”, 2, NULL );

/* Start the scheduler so the created tasks start executing. */

vTaskStartScheduler();

}

/* If all is well, then main() will never reach here because the scheduler will now be running the tasks. If main() does reach here, then it is likely that there was insufficient heap memory available for the idle task to be created. */

for( ;; );

}

出力は次のとおりです。

次の図は実行順序を示しています。

予期どおり、ターミナルに表示される文字列に破損はありません。ランダムな順序は、タスクで使用されるランダムな遅延期間が原因です。

優先度の逆転

上の図は、ミューテックスを使用して相互排他を提供する場合に生じ得るリスクの 1 つを示しています。図の実行順序では、低優先度のタスク 1 が制御を手放すまで高優先度のタスク 2 が待機する必要があります。このように低優先度のタスクによって高優先度のタスクが遅延されることは優先度の逆転と呼ばれます。この望ましくない状況は、高優先度のタスクがセマフォを待機中に、中優先度のタスクの実行が開始されると、さらに悪化します。この場合、低優先度のタスクが実行されないまま、高優先度のタスクが低優先度のタスクを待機し続けることになります。次の図は、この最悪のシナリオを示しています。

優先度の逆転は重大な問題を起こす可能性があります。ただし、小規模な埋め込みシステムでは、システムの設計時にリソースのアクセス方法を考慮することで問題を回避できる場合があります。

優先度の継承

FreeRTOS のミューテックスとバイナリセマフォはよく似ています。違いは、基本的な優先度の継承機構がミューテックスには含まれており、バイナリセマフォには含まれていないことです。優先度の継承は、優先度の逆転の悪影響を最小化する方法です。優先度の逆転は解消されませんが、常に逆転の期限を定めることで逆転の影響を軽減します。ただし、優先度の継承により、システムのタイミング解析が複雑になります。正確なシステムオペレーションが要求される場合、優先度の継承はお勧めできません。

優先度の継承では、ミューテックスを保持している低優先度のタスクの優先度を、同じミューテックスを取得しようとしている高優先度のタスクの優先度まで一時的に引き上げます。ミューテックスを保持している低優先度タスクが、ミューテックスを待機しているタスクの優先度を引き継ぎます。次の図は、ミューテックスホルダーがミューテックスを解放した後で、その優先度が元の値に自動的にリセットされることを示しています。

優先度の継承機能は、ミューテックスを使用しているタスクの優先度に影響します。このため、ミューテックを割り込みサービスルーチンから使用することはできません。

デッドロック (またはデッドリエンブレイス)

ミューテックスを相互排他で使用することの別のリスクは、デッドロックまたはデッドリエンブレイスです。

デッドロックは、1 つのタスクが待機しているリソースが別のタスクによって保持されており、この別のタスクが待機しているリソースが最初のタスクによって保持されているために、両方のタスクが続行できないときに発生します。次のシナリオで、アクションを実行するためにタスク A とタスク B の両方がミューテックス A とミューテックス B を必要とする場合を考えてみます。

  1. タスク A が実行を開始してミューテックス X を正常に取得します。
  2. タスク B がタスク A に取って代わります。
  3. タスク B は、ミューテックス Y を正常に取得した後でミューテックス X も取得しようとしますが、ミューテックス X はタスク A に保持されているために取得できません。タスク B はブロック状態に入り、ミューテックス X が解放されるまで待機します。
  4. タスク A が実行を継続し、ミューテックス Y を取得しようとしますが、ミューテックス Y はタスク B に保持されているために取得できません。タスク A はブロック状態に入り、ミューテックス Y が解放されるまで待機します。

タスク A が待機しているミューテックスがタスク B に保持され、タスク B が待機しているミューテックスがタスク A に保持されています。いずれのタスクも続行できないため、デッドロックが発生します。

優先度の逆転と同じように、デッドロックを回避する最適な方法は、デッドロックが発生しないようにシステムを設計することです。通常、タスクがミューテックスを取得するために (タイムアウトなしで) 無制限に待機する状況は適切ではありません。代わりに、ミューテックスを待機する最大所要時間より少し長いタイムアウトを使用します。この時間内にミューテックスを取得できない場合は、設計エラーの症状であり、デッドロックが発生している可能性があります。

デッドロックは小規模な埋め込みシステムでは大きな問題ではありません。システム設計者がアプリケーション全体をよく理解している場合、デッドロックが発生する可能性がある領域を特定して除去できます。

再帰的なミューテックス

タスクがそれ自体とのデッドロックを起こす場合もあります。このデッドロックは、タスクがミューテックスを取得して解放する前に、同じミューテックスを再度取得しようとすると発生します。次のシナリオを考えてみます。

  1. タスクがミューテックスを正常に取得します。
  2. このミューテックスを保持している間に、タスクはライブラリ関数を呼び出します。
  3. ライブラリ関数の実装が同じミューテックスを取得しようとして、このミューテックスが利用可能になるまでブロック状態で待機します。

タスクはブロック状態でミューテックスが解放されるまで待機しますが、タスクはすでにミューテックスホルダーになっています。タスクはブロック状態でそれ自体を待機しているため、デッドロックが発生しています。

この種のデッドロックを回避するには、標準のミューテックスの代わりに再帰的なミューテックスを使用します。再帰的なミューテックスは、同じタスクによって繰り返し取得可能です。それが解放されるのは、再帰的なミューテックスを取得する呼び出しごとに、再帰的なミューテックスを解放する呼び出しが実行された後に限られます。

標準のミューテックスと再帰的なミューテックスは同じ方法で作成および使用されます。

  • 標準のミューテックスを作成するには xSemaphoreCreateMutex() を使用します。再帰的なミューテックスを作成するには xSemaphoreCreateRecursiveMutex() を使用します。2 つの API 関数のプロトタイプは同じです。
  • 標準のミューテックスを取得するには xSemaphoreTake() を使用します。再帰的なミューテックスを取得するには xSemaphoreTakeRecursive() を使用します。2 つの API 関数のプロトタイプは同じです。
  • 標準のミューテックスを解放するには xSemaphoreGive() を使用します。再帰的なミューテックスを解放するには xSemaphoreGiveRecursive() を使用します。2 つの API 関数のプロトタイプは同じです。

次のコードは、再帰的なミューテックスを作成して使用する方法を示しています。

/* Recursive mutexes are variables of type SemaphoreHandle_t. */

SemaphoreHandle_t xRecursiveMutex;

/* The implementation of a task that creates and uses a recursive mutex. */

void vTaskFunction( void *pvParameters )

{

const TickType_t xMaxBlock20ms = pdMS_TO_TICKS( 20 );

/* Before a recursive mutex is used it must be explicitly created. */

xRecursiveMutex = xSemaphoreCreateRecursiveMutex();

/* Check the semaphore was created successfully. configASSERT() is described in section 11.2. */

configASSERT( xRecursiveMutex );

/* As per most tasks, this task is implemented as an infinite loop. */

for( ;; )

{

/* … */

/* Take the recursive mutex. */

if( xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms ) == pdPASS)

{

/* The recursive mutex was successfully obtained. The task can now access the resource the mutex is protecting. At this point the recursive call count (which is the number of nested calls to xSemaphoreTakeRecursive()) is 1 because the recursive mutex has only been taken once. */

/* While it already holds the recursive mutex, the task takes the mutex again. In a real application, this is only likely to occur inside a subfunction called by this task because there is no practical reason to knowingly take the same mutex more than once. The calling task is already the mutex holder, so the second call to xSemaphoreTakeRecursive() does nothing more than increment the recursive call count to 2. */

xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms );

/* … */

/* The task returns the mutex after it has finished accessing the resource the mutex is protecting. At this point the recursive call count is 2, so the first call to xSemaphoreGiveRecursive() does not return the mutex. Instead, it simply decrements the recursive call count back to 1. */

xSemaphoreGiveRecursive( xRecursiveMutex );

/* The next call to xSemaphoreGiveRecursive() decrements the recursive call count to 0, so this time the recursive mutex is returned.*/

xSemaphoreGiveRecursive( xRecursiveMutex );

/* Now one call to xSemaphoreGiveRecursive() has been executed for every proceeding call to xSemaphoreTakeRecursive(), so the task is no longer the mutex holder. */

}

}

}

ミューテックスとタスクのスケジュール設定

優先度の異なる 2 つのタスクが同じミューテックスを使用する場合は、FreeRTOS スケジューリングポリシーを使用してタスクの実行順序を明確にします。実行可能なタスクのうち、最高優先度のタスクが選択されて実行中状態に入ります。たとえば、高優先度のタスクがブロック状態で待機しているミューテックスが低優先度のタスクに保持されている場合、低優先度のタスクがミューテックスを解放すると同時に、高優先度のタスクが低優先度のタスクに取って代わります。高優先度タスクがミューテックスホルダーになります。このシナリオについては、「優先度の継承」セクションで説明済みです。

複数のタスクの優先度が同等である場合、タスクの実行順序は不正確になりがちです。タスク 1 とタスク 2 の優先度が同じであり、タスク 1 がブロック状態でタスク 2 が保持するミューテックスを待機している場合、タスク 2 がミューテックスを解放しても、タスク 1 はタスク 2 に取って代わりません。代わりに、タスク 2 は実行中状態のまま残ります。タスク 1 がブロック状態から準備完了状態から移行するだけです。

次の図で、縦線はティック割り込みが発生した時刻を示しています。

FreeRTOS スケジューラは、ミューテックスが利用可能になると同時にタスク 1 を実行中状態に移行させていないことがわかります。その理由は以下のとおりです。

  1. タスク 1 とタスク 2 の優先度が同じであるために、タスク 2 がブロック状態に入らない限り、次のティック割り込みまでタスク 1 への切り替えは起こりません (FreeRTOSConfig.h で configUSE_TIME_SLICING が 1 に設定されているものとします)。
  2. タスクがタイトなループでミューテックスを使用していて、タスクがミューテックスを解放するたびにコンテキストの切り替えが発生している場合、タスクが実行中状態に留まるのは短時間です。複数のタスクがタイトなループで同じミューテックスを使用している場合、タスク間での頻繁な切り替えは処理時間の浪費になります。

タイトなループ内のミューテックスを複数のタスクが使用しており、これらのタスクの優先度が同じである場合、各タスクの処理時間がほぼ同等であることを確認してください。上の図に示した実行順序は、次のコードに示すタスクの 2 つのインスタンスが同じ優先度で作成されている場合に発生することがあります。

/* The implementation of a task that uses a mutex in a tight loop. The task creates a text string in a local buffer, and then writes the string to a display. Access to the display is protected by a mutex. */

void vATask( void *pvParameter )

{

extern SemaphoreHandle_t xMutex;

char cTextBuffer[ 128 ];

for( ;; )

{

/* Generate the text string. This is a fast operation. */

vGenerateTextInALocalBuffer( cTextBuffer );

/* Obtain the mutex that is protecting access to the display. */

xSemaphoreTake( xMutex, portMAX_DELAY );

/* Write the generated text to the display. This is a slow operation. */

vCopyTextToFrameBuffer( cTextBuffer );

/* The text has been written to the display, so return the mutex. */

xSemaphoreGive( xMutex );

}

}

コード内のコメントは、文字列の作成が高速なオペレーションで、表示の更新が低速のオペレーションであることを示しています。したがって、表示の更新中はミューテックスが保持されるため、タスクはランタイムの大半でミューテックスを保持することになります。

次の図で、縦線はティック割り込みが発生する時刻を示しています。

この図のステップ 7 で、タスク 1 は再度ブロック状態に入っています。これは xSemaphoreTake() API 関数内で発生します。

タスク 1 がミューテックスを取得できるのは、タスク 2 がミューテックスホルダーではない短時間のいずれかとタイムスライスの開始が一致した場合に限られます。

このシナリオを回避するには、xSemaphoreGive() への呼び出し後に taskYIELD() への呼び出しを追加します。これを次のコードで示します。このコードでは、タスクがミューテックスを保持している間にティックカウントが変わると、taskYIELD() が呼び出されます。これにより、ループ内でミューテックスを使用するタスク間の処理時間はより均等化され、タスク間の頻繁な切り替えで処理時間が浪費されることもなくなります。

void vFunction( void *pvParameter )

{

extern SemaphoreHandle_t xMutex;

char cTextBuffer[ 128 ];

TickType_t xTimeAtWhichMutexWasTaken;

for( ;; )

{

/* Generate the text string. This is a fast operation. */

vGenerateTextInALocalBuffer( cTextBuffer );

/* Obtain the mutex that is protecting access to the display. */

xSemaphoreTake( xMutex, portMAX_DELAY );

/* Record the time at which the mutex was taken. */

xTimeAtWhichMutexWasTaken = xTaskGetTickCount();

/* Write the generated text to the display. This is a slow operation. */

vCopyTextToFrameBuffer( cTextBuffer );

/* The text has been written to the display, so return the mutex. */

xSemaphoreGive( xMutex );

/* If taskYIELD() was called on each iteration, then this task would only ever remain in the Running state for a short period of time, and processing time would be wasted by rapidly switching between tasks. Therefore, only call taskYIELD() if the tick count changed while the mutex was held. */

if( xTaskGetTickCount() != xTimeAtWhichMutexWasTaken )

{

taskYIELD();

}

}

}

ゲートキーパータスク

ゲートキーパータスクは、優先度の逆転やデッドロックのリスクなしに、相互排他を実装する適切な方法を提供します。

ゲートキーパータスクは、リソースの独占的所有権を持つタスクです。ゲートキーパータスクのみがリソースに直接アクセスできます。その他のタスクがリソースにアクセスするには、ゲートキーパーのサービスを利用して間接的にアクセスする必要があります。

ゲートキーパータスクを使用するための vPrintString() の書き換え (例 21)

次の例では、vPrintString() の別の実装を示します。今回は、ゲートキーパータスクを使用して標準出力へのアクセスを管理します。タスクは、標準出力にメッセージを書き込む場合、print 関数を直接呼び出しません。代わりに、ゲートキーパーにメッセージを送信します。

ゲートキーパータスクは FreeRTOS キューを使用して標準出力へのアクセスをシリアル化します。タスクの内部実装では、標準出力に直接アクセスできるタスクが他に存在しないため、相互排他を考慮する必要がありません。

ゲートキーパータスクは、大部分の時間をブロック状態で費やし、キューにメッセージが到着するまで待機します。メッセージが到着すると、ゲートキーパーはメッセージを単に標準出力に書き込み、再びブロック状態に戻って次のメッセージを待機します。

割り込みはキューに送信を行うことができるため、割り込みサービスルーチンもゲートキーパーのサービスを安全に使用してメッセージをターミナルに書き込むことができます。次の例では、ティックフック関数を使用して 200 ティックごとにメッセージを書き込みます。

ティックフック (またはティックコールバック) は、ティック割り込みごとにカーネルによって呼び出される関数です。ティックフック関数を使用する手順は次のとおりです。

  1. FreeRTOSConfig.h で、configUSE_TICK_HOOK を 1 に設定します。
  2. 次に示す関数名とプロトタイプを正確に使用して、フック関数の実装を提供します。

void vApplicationTickHook( void );

ティックフック関数は、ティック割り込みのコンテキスト内で実行されるため、非常に短くする必要があります。必要最低限のスタックスペースのみを使用し、「FromISR()」で終わらない FreeRTOS API 関数は呼び出さないようにします。

ゲートキーパータスクの実装は以下のとおりです。スケジューラは常にティックフック関数の直後に実行されるため、ティックフックから呼び出す割り込みセーフな FreeRTOS API 関数では pxHigherPriorityTaskWoken パラメータが不要であり、このパラメータは NULL に設定できます。

static void prvStdioGatekeeperTask( void *pvParameters )

{

char *pcMessageToPrint;

/* This is the only task that is allowed to write to standard out. Any other task wanting to write a string to the output does not access standard out directly, but instead sends the string to this task. Because this is the only task that accesses standard out, there are no mutual exclusion or serialization issues to consider within the implementation of the task itself. */

for( ;; )

{

/* Wait for a message to arrive. An indefinite block time is specified so there is no need to check the return value. The function will return only when a message has been successfully received. */

xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );

/* Output the received string. */

printf( “%s”, pcMessageToPrint );

fflush( stdout );

/* Loop back to wait for the next message. */

}

}

キューに書き込むタスクは以下のとおりです。前と同じように、タスクの個別のインスタンスを 2 つ作成し、タスクがキューに書き込む文字列をタスクのパラメータを使用してタスクに渡します。

static void prvPrintTask( void *pvParameters )

{

int iIndexToString;

const TickType_t xMaxBlockTimeTicks = 0x20;

/* Two instances of this task are created. The task parameter is used to pass an index into an array of strings into the task. Cast this to the required type. */

iIndexToString = ( int ) pvParameters;

for( ;; )

{

/* Print out the string, not directly, but by passing a pointer to the string to the gatekeeper task through a queue. The queue is created before the scheduler is started so will already exist by the time this task executes for the first time. A block time is not specified because there should always be space in the queue. */

xQueueSendToBack( xPrintQueue, &( pcStringsToPrint[ iIndexToString ] ), 0 );

/* Wait a pseudo random time. Note that rand() is not necessarily reentrant, but in this case it does not really matter because the code does not care what value is returned. In a more secure application, a version of rand() that is known to be reentrant should be used or calls to rand() should be protected using a critical section. */

vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );

}

}

ティックフック関数は、呼び出された回数をカウントし、カウントが 200 に達するたびにゲートキーパータスクにメッセージを送信します。デモンストレーションの目的に限り、このティックフックはキューの先頭に書き込み、タスクはキューの末尾に書き込みます。ティックフックの実装は以下のとおりです。

void vApplicationTickHook( void )

{

static int iCount = 0;

/* Print out a message every 200 ticks. The message is not written out directly, but sent to the gatekeeper task. */

iCount++;

if( iCount >= 200 )

{

/* Because xQueueSendToFrontFromISR() is being called from the tick hook, it is not necessary to use the xHigherPriorityTaskWoken parameter (the third parameter), and the parameter is set to NULL. */

xQueueSendToFrontFromISR( xPrintQueue, &( pcStringsToPrint[ 2 ] ), NULL );

/* Reset the count ready to print out the string again in 200 ticks time. */

iCount = 0;

}

}

通常どおり、main() はサンプルの実行に必要なキューとタスクを作成し、スケジューラを起動します。main() の実装は以下のとおりです。

/* Define the strings that the tasks and interrupt will print out via the gatekeeper. */

static char *pcStringsToPrint[] =

{

“Task 1 ****************************************************\r\n”, “Task 2 —————————————————-\r\n”, “Message printed from the tick hook interrupt ##############\r\n”

};

/*———————————————————–*/

/* Declare a variable of type QueueHandle_t. The queue is used to send messages from the print tasks and the tick interrupt to the gatekeeper task. */

QueueHandle_t xPrintQueue;

/*———————————————————–*/

int main( void )

{

/* Before a queue is used it must be explicitly created. The queue is created to hold a maximum of 5 character pointers. */

xPrintQueue = xQueueCreate( 5, sizeof( char * ) );

/* Check the queue was created successfully. */

if( xPrintQueue != NULL )

{

/* Create two instances of the tasks that send messages to the gatekeeper. The index to the string the task uses is passed to the task through the task parameter (the 4th parameter to xTaskCreate()). The tasks are created at different priorities, so the higher priority task will occasionally preempt the lower priority task. */

xTaskCreate( prvPrintTask, “Print1”, 1000, ( void * ) 0, 1, NULL );

xTaskCreate( prvPrintTask, “Print2”, 1000, ( void * ) 1, 2, NULL );

/* Create the gatekeeper task. This is the only task that is permitted to directly access standard out. */

xTaskCreate( prvStdioGatekeeperTask, “Gatekeeper”, 1000, NULL, 0, NULL

);

/* Start the scheduler so the created tasks start executing. */

vTaskStartScheduler();

}

/* If all is well, then main() will never reach here because the scheduler will now be running the tasks. If main() does reach here, then it is likely that there was insufficient heap memory available for the idle task to be created.*/

for( ;; );

}

出力は以下のとおりです。タスクからの文字列と割り込みからの文字列のすべてが、破損なしに適切に出力されているのがわかります。

ゲートキーパータスクに割り当てられている優先度はプリントタスクよりも低いため、ゲートキーパーに送信されたメッセージは、両方のプリントタスクがブロック状態に入るまで、キュー内に残存します。状況によっては、メッセージが即座に処理されるように、ゲートキーパーにより高い優先度を割り当てることが適切な場合もあります。ただし、ゲートキーパーによる保護リソースへのアクセスが完了するまで、ゲートキーパーより優先度の低いタスクが遅延されます。


Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です