FreeRTOS がアクセス違反/スレッドの安全性を解決する方法 (クリティカル セクション、ミューテックス、保留中のスケジューリング、ゲートキーパー タスク)

マルチタスク (マルチスレッド) システムには、マルチスレッド アクセス (FreeRTOS のタスク) という隠れた危険があります。タスク A がリソース (周辺機器、メモリなど) へのアクセスを開始したが、A がアクセスを完了していない場合、タスク B が実行されてアクセスが開始され、データの破損、エラー、その他の問題が発生します。

例えば:

2 つのタスクが液晶ディスプレイ (LCD) への書き込みを試みます。

1 タスク A が実行され、文字列「Hello world」の LCD への書き込みが開始されます。

2. タスク A は、文字列「Hello w」を出力した後、タスク B によってプリエンプトされます。

3. タスク B は、ブロッキング状態に入る前に「中止、再試行、失敗?」を LCD に書き込みます。

4. タスク A はプリエンプトされた時点から続行し、文字列「world」の残りの文字の出力を終了します。

LCD には「Hello wAbort, Retry, Fail? world」という文字列が表示されます。これは明らかに私たちが望んでいる結果ではありません。

元のリンク: FreeRTOS-8 の完全な分析. アクセス競合/スレッドの安全性の解決 (クリティカル セクション、保留中のスケジューリング、ミューテックス、ゲートキーパー タスク)

目次

1. いくつかの概念

1.1 アトミック操作と非アトミック操作

1.2 リエントラント関数

1.3 相互排除

2. クリティカルセクションとサスペンドスケジューラ

2.1 クリティカルセクション

2.2 スケジューラを一時停止(ロック)する

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

3.1 優先順位の逆転

3.2 優先順位の継承

3.3 デッドロック

3.4 再帰的ミューテックス

4. ゲートキーパーのタスク


1. いくつかの概念

1.1 アトミック操作と非アトミック操作

読み取り、変更、書き込み操作

変数 PORTA または 0x01 については、C 言語で次のように記述します。

PORTA |= 0x01;

コンパイルしてアセンブリに変換した後、次のようになります。

LOAD R1,[#PORTA] ; Read a value from PORTA into R1MOVE R2,#0x01 ; Move the absolute constant 1 into R2OR R1,R2 ; Bitwise OR R1 (PORTA) with R2 (constant 1)STORE R1,[#PORTA] ; Store the new value back to PORTA

最初の文では、PORTA のアドレスからデータを読み取り、R1 に保存します (読み取り操作)

2 番目の文、save 0x01 to R2; (読み取り操作)

3 番目の文では、R1 と R2 が OR 演算を実行し、それを R1 に格納します (修飾演算)

4 番目の文では、R1 の値を PORTA のアドレスに保存します。(書き込み操作)

これは、複数のアセンブリ命令を使用し、中断できるため、非アトミック操作と呼ばれます (逆に、1 つの命令のみを使用し、中断できない操作はアトミック操作と呼ばれます)。構造体の複数のメンバーの更新、または CPU 構造体のワード サイズより大きい変数の更新 (たとえば、16 ビット マシン上の 32 ビット変数の更新) は、非アトミック操作の例です。中断されると、データの損失または破損が発生する可能性があります。

次のシナリオを考えてみましょう。

1 タスク A は、PORTA の値をレジスタにロードします (操作の読み取り部分)。

2. タスク A は、変更と書き込み部分が完了する前にタスク B によってプリエンプトされます。

3. タスク B は PORTA の値を更新し、ブロッキング状態に入ります。

4. タスク A は、プリエンプトされた時点から続行します。すでにレジスタに保持されているPORTAの値を変更し、PORTAのアドレスを書き込みます。

このシナリオでは、タスク A によって使用される PORTA の値は期限切れに相当します (タスク B が PORTA を変更したため)。この問題はデータの不整合とも呼ばれます。

1.2 リエントラント関数

関数が複数のタスクから呼び出せる場合、またはタスクや割り込みから呼び出すことが安全な場合、その関数は「リエントラント」です。リエントラント関数は、データや論理演算の破損のリスクなしに複数のスレッドからアクセスできるため、「スレッドセーフ」であると言われます。

各タスクは、独自のスタックと独自のプロセッサ (ハードウェア) レジスタのセットを維持します。関数は、スタックに格納されているデータまたはレジスタに保持されているデータ以外のデータにアクセスしない場合、リエントラントでスレッドセーフです。

以下に示すように、lVar1 はスタックまたはレジスターを介して渡され、lVar2 はタスク自体のスタック内にあるため、これは再入可能関数です。各タスクがこのコードにアクセスするとき、lVar1 と lVar2 は異なるアドレスになります。

long lAddOneHundred( long lVar1 ){
   
     long lVar2;  lVar2 = lVar1 + 100;  return lVar2;}

以下に示すように、これは再入可能ではなく、lVar1 はグローバル変数であり、lState は静的に変更され、データ セグメントに格納されます。各タスクによってアクセスされる lVar1 と lState は同じです。

long lVar1;long lNonsenseFunction( void ){
   
     static long lState = 0;  long lReturn;  switch( lState )  {
   
       case 0 : lReturn = lVar1 + 10;      lState = 1;      break;    case 1 : lReturn = lVar1 + 20;      lState = 0;      break;  }}

1.3 相互排除

データの一貫性を常に確保するには、タスク間またはタスクと割り込み間で共有されるリソースを「相互排他」を使用して管理する必要があります。テクノロジー。目標は、タスクが非再入可能かつ非スレッドセーフな共有リソースへのアクセスを開始すると、リソースが一貫した状態に戻るまで、その同じタスクがそのリソースに排他的にアクセスできるようにすることです。

FreeRTOS は、相互排他を実装するために使用できる機能をいくつか提供していますが、相互排他を実現する最良の方法は、(通常は現実的ではないため、可能であれば) リソースが共有されないようにアプリケーションを設計することです。単一のタスクからのみアクセスできます。

2. クリティカルセクションとサスペンドスケジューラ

2.1 クリティカルセクション

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

taskENTER_CRITICAL();PORTA |= 0x01;taskEXIT_CRITICAL();

LCD 競合の書き込みの例に戻ると、次のようになります。

void vPrintStringToLCD( const char *pcString ){
   
     taskENTER_CRITICAL();  LCD_printf( "%s", pcString );  fflush( stdout );  taskEXIT_CRITICAL();}

クリティカルセクションでの相互排除の実装は、非常に大雑把なアプローチです。これは、割り込みを完全に無効にするか、configMAX_SYSCALL_INTERRUPT_PRIORITY で設定された割り込み優先順位 (最も高い優先順位セット) まで無効にすることで機能します。

プリエンプティブなコンテキスト切り替え (タスク スケジューリング) は割り込み内でのみ実行できるため、taskENTER_CRITICAL() を呼び出すタスクは、クリティカル セクションが終了するまで割り込みが無効になっている限り、実行し続けることが保証されます。

クリティカル セクションのコードは非常に短く保つ必要があり、そうしないと割り込み応答時間に悪影響を及ぼします。taskENTER_CRITICAL() の各呼び出しは、taskEXIT_CRITICAL() の呼び出しと密接に組み合わせる必要があります。LCD または出力への書き込みが遅くなる場合は、クリティカル セクションを使用しないでください。

カーネルがネストの深さのカウントを保持しているため、クリティカル セクションのネストは安全です。ネストの深さがゼロに戻った場合にのみ、クリティカル セクションが終了します。

taskENTER_CRITICAL() および taskEXIT_CRITICAL() を呼び出すことは、タスクが FreeRTOS を実行しているプロセッサの割り込み有効状態を変更するための唯一の正当な方法です。他の方法で割り込み許可状態を変更すると、マクロのネスト数が無効になります。

taskENTER_CRITICAL() および taskEXIT_CRITICAL() は 'FromISR' で終了しないため、割り込みサービス ルーチンから呼び出すことはできません。taskENTER_CRITICAL_FROM_ISR() は taskENTER_CRITICAL() の割り込みセーフ バージョンであり、taskEXIT_CRITICAL_FROM_ISR() は taskEXIT_CRITICAL() の割り込みセーフ バージョンです。割り込みセーフ バージョンは、割り込みのネストを許可するプロセッサに対してのみ有効です。使用法:

void vAnInterruptServiceRoutine( void ){
   
     UBaseType_t uxSavedInterruptStatus;  uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();  taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );}

2.2 スケジューラを一時停止(ロック)する

クリティカル セクションは、スケジューラを一時停止することによっても作成できます。一時停止中のスケジューラは、「ロック」スケジューラとも呼ばれます。クリティカル セクションは、他のタスク割り込みによるコード領域へのアクセスを保護しますスケジューラを一時停止することによって実装されたクリティカル セクションは、割り込みがまだ有効になっているため、コードの領域が他のタスクからアクセスされないように保護するだけです。

クリティカル セクションが長すぎて割り込みを無効にするだけでは達成できない場合は、スケジューラを一時停止することで達成できます。ただし、スケジューラーを再開 (または「中断解除」) すると時間がかかるため、それぞれのケースでどの方法を使用するのが最適であるかを考慮する必要があります。

スケジューラは、vTaskSuspendAll() を呼び出すことによって一時停止されます。スケジューラを一時停止すると、コンテキストの切り替えは発生しなくなりますが、割り込みは可能になります。スケジューラの一時停止中にタスクの切り替え要求があった場合、その要求は保留状態のままとなり、スケジューラが再開されるとき (一時停止ではないとき) にのみ実行されます。スケジューラがハングしている間は、FreeRTOS API 関数を呼び出すことができません。

void vTaskSuspendAll( void )BaseType_t xTaskResumeAll( void );

vTaskSuspendAll() および xTaskResumeAll() への呼び出しのネストは、カーネルがネストの深さのカウントを保持しているため安全です。スケジューラは、ネストの深さが 0 を返した場合にのみ再開されます。

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

セマフォがわからない場合は、このFreeRTOS の完全な分析-8 を読んでください。

ミューテックス (またはミューテックス、私は Linux をよく使います。ロックの呼び出しには慣れています。呼び出しには FreeRTOS の方が適しています) は特殊なタイプのバイナリ セマフォで、2 つ以上のタスク間のリソースへのアクセスの共有を制御するために使用されます。「Mutex」(相互排他ロック)という言葉は、「相互排他」に由来しています。(相互に排他的)

ミューテックスを有効にするには、FreeRTOSConfig.h の configUSE_MUTEXES を 1 に設定する必要があります。

相互排除が必要なシナリオでミューテックスが使用される場合、ミューテックスは共有リソースに関連付けられたトークンと考えることができます。タスクがリソースに合法的にアクセスするには、まずトークンを正常に「取得」する (トークン所有者になる) 必要があります。トークン所有者は、リソースの使用が終了したら、トークンを「返す」必要があります。トークンが返された場合にのみ、別のタスクがトークンを正常に取得し、同じ共有リソースに安全にアクセスできます。タスクがトークンを保持しない限り、共有リソースへのアクセスは許可されません。

ミューテックスはバイナリ セマフォに似ていますが、同じではありません。主な違いは、セマフォが取得された後に何が起こるかです。相互排他に使用されたセマフォは常に返される必要があります (取得後ギブ)。同期に使用されるセマフォは通常、破棄され、返されません (ギブ アフター テイクはありません)。もう 1 つの違いは、ミューテックスには優先順位の継承があることです (この記事で後述します)。

ミューテックスは次のように使用されます。取得および解放関数はセマフォに使用される関数と同じです。

static void prvNewPrintString( const char *pcString ){
   
     xSemaphoreTake( xMutex, portMAX_DELAY );  printf( "%s", pcString );  fflush( stdout );  xSemaphoreGive( xMutex );}

使用前に作成するには、次の関数を呼び出します。

SemaphoreHandle_t xSemaphoreCreateMutex( void );

例えば:

SemaphoreHandle_t xMutex; xMutex = xSemaphoreCreateMutex();

ミューテックスを使用した完全な例:

static void prvNewPrintString( const char *pcString ){
   
     xSemaphoreTake( xMutex, portMAX_DELAY );  printf( "%s", pcString );  fflush( stdout );  xSemaphoreGive( xMutex );}static void prvPrintTask( void *pvParameters ){
   
     char *pcStringToPrint;  const TickType_t xMaxBlockTimeTicks = 0x20;  pcStringToPrint = ( char * ) pvParameters;  for( ;; )  {
   
       prvNewPrintString( pcStringToPrint );    vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );  }}int main( void ){
   
     xMutex = xSemaphoreCreateMutex();  if( xMutex != NULL )  {
   
       xTaskCreate( prvPrintTask, "Print1", 1000,    "Task 1 ***************************************\r\n", 1, NULL );    xTaskCreate( prvPrintTask, "Print2", 1000,    "Task 2 ---------------------------------------\r\n", 2, NULL );    vTaskStartScheduler();  }  for( ;; );}

3.1 優先順位の逆転

まず上記の例で何が起こるかを見てみましょう

タスク 1 の優先度は 1、タスク 2 の優先度は 2 です。

Task1 が最初に実行され、ミューテックスを取得します。Task2 は優先度が高くなりますが、ミューテックスを取得していないためブロッキング状態になります。Task1 がミューテックスを解放した場合にのみ実行できます。

これは、相互排他を提供するためにミューテックスを使用する場合の潜在的な落とし穴を表しています

優先度の高いタスク 2 は、優先度の低いタスク 1 がミューテックスの制御を放棄するまで待つ必要があります。このように、優先度の高いタスクが優先度の低いタスクによって遅れることを「優先度の逆転」と呼びます。

次のような原因で悪化します。

図に示すように、LP 低優先タスク、MP 中優先タスク、HP 高優先タスクの 3 つのタスクがあります。

LP が実行され、ミューテックスを取得し、HP がプリエンプトしようとしますが、ミューテックスが取得できないため、ブロッキングに入ることしかできず、LP は実行を続けますが、LP の動作中に、ミューテックスを必要としない MP によってプリエンプトされます。

LP が実行されていないと、ミューテックスは解放されず、解放されなければ、HP は実行できません。その結果、最も優先度の高いタスクが最も優先度の低いタスクを待機することになります。

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

3.2 優先順位の継承

FreeRTOS ミューテックスとバイナリ セマフォの違いは、ミューテックスには「優先順位継承」メカニズムがあるのに対し、バイナリ セマフォにはそれがないことです。優先順位の継承は、優先順位の逆転による悪影響を最小限に抑えるためのスキームです。優先順位の逆転を「修正」するのではなく、逆転が常に時間制限されるようにすることで影響を軽減するだけです。ただし、優先順位の継承はシステムのタイミング解析を複雑にするため、適切なシステム動作のために優先順位の継承に依存することはお勧めできません。

優先度の継承は、ミューテックス保持者の優先度を、同じミューテックスを取得しようとする最も優先度の高いタスクの優先度に一時的に上げることによって実現されます。ミューテックスを保持する優先度の低いタスクは、ミューテックスを待っているタスクの優先度を「継承」します。ミューテックス保持者の優先順位は、ミューテックスが返されると自動的に元の値にリセットされます。

この仕組みにより、上記の状況は次のようになります。

LP が実行され、ミューテックスを取得し、HP が実行しようとしますが、ミューテックスがないためブロッキング状態になります。同時に、HP の優先度が高いため、LP は HP の優先度を継承し、プリエンプトされなくなります。議員による。LP がミューテックスを解放すると、HP が実行できるようになります。

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

3.3 デッドロック

「デッドロック」は、ミューテックスを相互に排他的にすることによるもう 1 つの潜在的な落とし穴です。

デッドロックは、2 つのタスクが両方とももう一方のタスクが保持するリソースを待っているときに発生します。次のシナリオを考えてみましょう。タスク A とタスク B の両方が、操作を実行するためにミューテックス X と Y を取得する必要があります。

1 タスク A が実行され、ミューテックス X の取得に成功します。

2. タスク A はタスク B によってプリエンプトされます。

3. タスク B は、ミューテックス X を使用しようとする前に、ミューテックス Y を正常に使用しましたが、ミューテックス X はタスク A によって保持されているため、タスク B はそれを使用できません。タスク B はブロッキング状態に入ることを選択し、ミューテックス X が解放されるのを待ちます。

4. タスク A は実行を続けます。ミューテックス Y を取得しようとしますが、ミューテックス Y はタスク B が保持しているため、タスク A は使用できません。タスク A はブロッキング状態に入ることを選択し、ミューテックス Y が解放されるのを待ちます。

タスク A はミューテックス X をブロックして待機し、タスク B はミューテックス Y をブロックして待機します。待機中のミューテックスはすべて相手の手に渡りますが、すべてブロックされた状態で実行できません。これは行き詰まりです。

優先順位の逆転と同様、デッドロックを回避する最善の方法は、デッドロックが発生しないようにこれを念頭に置いてシステムを設計することです。

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

3.4 再帰的ミューテックス

タスク自体がデッドロックになる可能性もあります。これは、タスクが最初にミューテックスを返さずに同じミューテックスを複数回使用しようとした場合に発生する可能性があります。次のシナリオを考えてみましょう。

1. タスクはミューテックス A を正常に取得します。

2. タスクはミューテックス A を保持したまま、ライブラリ関数を呼び出します。

3. ライブラリ関数は同じミューテックス A を使用しようとすると、ブロッキング状態になり、ミューテックス A を待ちます。

このシナリオの終了時に、タスクはミューテックスが返されるのを待ってブロックされますが、タスクはすでにミューテックスの所有者です。デッドロックは、タスクが自分自身を待っているブロッキング状態にあるため、つまり自分自身を待っているために発生します。

このタイプのデッドロックは、標準のミューテックスの代わりに再帰的ミューテックスを使用することで回避できます。タスクは同じミューテックスを複数回取得 (テイク) できますが、数回の take と複数回の give を忘れないでください。

作成:

xSemaphoreCreateRecursiveMutex().

ゲットテイクはテイクになります

xSemaphoreTakeRecursive().

解放すると与えられるようになる

xSemaphoreGiveRecursive()

4. ゲートキーパーのタスク

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

ゲートキーパー タスクは、リソースの所有権を単独で持つタスクです。リソースに直接アクセスできるのはゲートキーパー タスクだけです。リソースにアクセスする必要がある他のタスクは、ゲートキーパーのサービスを使用して間接的にリソースにアクセスすることしかできません。

次の例にあるように、考え方は非常に単純です。タスクは印刷されるものであり、出力はリソースです。タスクは直接印刷できません。タスクはキューを通じてゲートキーパー タスクに送信される必要があり、ゲートキーパー タスクは実行されます。印刷操作。

static void prvStdioGatekeeperTask( void *pvParameters ){
   
     char *pcMessageToPrint;  for( ;; )  {
   
       xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );    printf( "%s", pcMessageToPrint );    fflush( stdout );  }}static void prvPrintTask( void *pvParameters ){
   
     int iIndexToString;  const TickType_t xMaxBlockTimeTicks = 0x20;  iIndexToString = ( int ) pvParameters;  for( ;; )  {
   
       xQueueSendToBack( xPrintQueue, &( pcStringsToPrint[ iIndexToString ] ), 0 );    vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );  }}static char *pcStringsToPrint[] ={
   
     "Task 1 ****************************************************\r\n",  "Task 2 ----------------------------------------------------\r\n",  "Message printed from the tick hook interrupt ##############\r\n"};QueueHandle_t xPrintQueue;int main( void ){
   
     xPrintQueue = xQueueCreate( 5, sizeof( char * ) );  if( xPrintQueue != NULL )  {
   
       xTaskCreate( prvPrintTask, "Print1", 1000, ( void * ) 0, 1, NULL );    xTaskCreate( prvPrintTask, "Print2", 1000, ( void * ) 1, 2, NULL );    xTaskCreate( prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL );    vTaskStartScheduler();  }  for( ;; );}

過去のハイライト:

STM32F4+FreeRTOS+LVGLで高速開発を実現(ステッチモンスター)

おすすめ

転載: blog.csdn.net/freestep96/article/details/130211794