2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FreeRTOS理解その13(デッドロック)

Posted at

デッドロック

今回のお題は、FreeRTOS上でデッドロックを故意に発生させるもの。Arduino IDE環境下で、ESP32搭載のM5Stackを使う。

方針

2つのタスクが2つの資源(LCDおよびシリアル回線)を利用し、これらの資源の排他制御が不適切な例を考える。2つのタスクは、M5Stackの左ボタン押下時に、LCD表示及びシリアル経由でPCへの表示を行うタスク、右ボタン押下時に同様のことを行うタスクである。また、表示内容はボタンが押された回数とする。

その前に(排他制御せず)

デッドロック以前に、排他制御自体を行わないと、どうなるかをトライ。

NoExclusion.cpp
# include <M5Stack.h>

# define L_BTN 39
# define R_BTN 37

uint32_t      l_btn_cnt;
uint32_t      r_btn_cnt;
xTaskHandle   xTaskL, xTaskR;

void IRAM_ATTR l_btn_intr() {
  l_btn_cnt++;
}

void IRAM_ATTR r_btn_intr() {
  r_btn_cnt++;
}

void TaskL(void *arg) {
  uint32_t old_cnt = l_btn_cnt;
  char buf[16];
  while (1) {
    if (l_btn_cnt != old_cnt) {
      sprintf(buf, "TaskL %d", l_btn_cnt);
      Serial.println(buf);
      M5.Lcd.fillRect(20, 20, 200, 40, BLACK);
      M5.Lcd.drawString(buf, 20, 20);
      old_cnt = l_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void TaskR(void *arg) {
  uint32_t old_cnt = r_btn_cnt;
  char buf[16];
  while (1) {
    if (r_btn_cnt != old_cnt) {
      sprintf(buf, "TaskR %d", r_btn_cnt);
      Serial.println(buf);
      M5.Lcd.fillRect(20, 60, 200, 40, BLACK);
      M5.Lcd.drawString(buf, 20, 60);
      old_cnt = r_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void setup() {
  Serial.begin(115200);
  M5.begin();
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);  
  pinMode(L_BTN, INPUT);
  pinMode(R_BTN, INPUT);
  attachInterrupt(digitalPinToInterrupt(L_BTN), l_btn_intr, FALLING);
  attachInterrupt(digitalPinToInterrupt(R_BTN), r_btn_intr, FALLING);
  l_btn_cnt = 0;
  r_btn_cnt = 0;
  Serial.println("Task Start");
  xTaskCreate(&TaskL, "TaskL", 1024, NULL, 1, &xTaskL);
  xTaskCreate(&TaskR, "TaskR", 1024, NULL, 1, &xTaskR);
}

void loop() {
  vTaskDelay(1);
}

左ボタン押下用のTaskLと右ボタン押下用のTaskRとが存在し、それぞれのボタン押下時のハンドラl_btn_intr()とr_btn_intr()とでは、ボタン押下のたびにカウントをインクリメントする。

これを実行し、両方のボタン押下を連続的に続けていくと、リブートしてしまう、、、。
排他制御なし.png

LCD表示ではこんな感じ。(ピンぼけすみません。)

デッドロック1(不適切な排他制御(資源の確保))

LCDおよびシリアル回線利用のための排他制御を入れるが、あえてデッドロックするような制御をおこなう。

TaskLでは、シリアル回線資源が確保できたときのみ、LCD資源の確保を実施し、TaskRでは、LCD資源が確保できたときのみ、シリアル回線資源の確保、、、を実施する。これにより、TaskLではシリアル回線資源確保後のLCDの解放待ち、TaskRではLCD資源確保後のシリアル回線資源の解放待ち、、、という状況を比較的簡単に作ることができる(ボタン押下のタイミング次第ではあるが)。

DeadLock1.cpp
# include <M5Stack.h>

# define L_BTN 39
# define R_BTN 37

uint32_t      l_btn_cnt;
uint32_t      r_btn_cnt;
xTaskHandle   xTaskL, xTaskR;
SemaphoreHandle_t xSemSerial, xSemLCD;

void IRAM_ATTR l_btn_intr() {
  l_btn_cnt++;
}

void IRAM_ATTR r_btn_intr() {
  r_btn_cnt++;
}

void TaskL(void *arg) {
  uint32_t old_cnt = l_btn_cnt;
  char buf[16];
  while (1) {
    if (l_btn_cnt != old_cnt) {
      sprintf(buf, "TaskL %d", l_btn_cnt); // \n
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
          M5.Lcd.fillRect(20, 20, 200, 40, BLACK);
          M5.Lcd.drawString(buf, 20, 20);
          xSemaphoreGive(xSemLCD);          
        }
        xSemaphoreGive(xSemSerial);
      }
      old_cnt = l_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void TaskR(void *arg) {
  uint32_t old_cnt = r_btn_cnt;
  char buf[16];
  while (1) {
    if (r_btn_cnt != old_cnt) {
      sprintf(buf, "TaskR %d", r_btn_cnt); // \n
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 60, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 60);
        if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
          Serial.println(buf);
          xSemaphoreGive(xSemSerial);        
        }
        xSemaphoreGive(xSemLCD);
      }
      old_cnt = r_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void setup() {
  Serial.begin(115200);
  M5.begin();
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);  
  pinMode(L_BTN, INPUT);
  pinMode(R_BTN, INPUT);
  attachInterrupt(digitalPinToInterrupt(L_BTN), l_btn_intr, FALLING);
  attachInterrupt(digitalPinToInterrupt(R_BTN), r_btn_intr, FALLING);
  l_btn_cnt = 0;
  r_btn_cnt = 0;
  Serial.println("Task Start");
  xTaskCreate(&TaskL, "TaskL", 1024, NULL, 1, &xTaskL);
  xTaskCreate(&TaskR, "TaskR", 1024, NULL, 1, &xTaskR);
  xSemSerial = xSemaphoreCreateMutex();
  xSemaphoreGive(xSemSerial);  
  xSemLCD = xSemaphoreCreateMutex();
  xSemaphoreGive(xSemLCD);  
}

void loop() {
  vTaskDelay(1);
}

ここでは、排他制御にはセマフォを利用。xSemaphoreCreateMutex()によるセマフォ作成、xSemaphoreTake()による資源確保、xSemaphoreGive()による資源開放が行われる。最初のxSemaphoreTake()成功時のみ、次のxSemaphoreTake()を呼び出しているところがミソ。

デッドロック中のLCD表示。(ピンぼけすみません。)

その時のPC側の表示及び約3時間後の強制リブート(M5 Stack赤ボタン押下)の様子。
デッドロック後リブート.png

デッドロック2

TaskL()およびTaskR()のみ記載。その他のコードは、デッドロック1と同じ。

DeadLock2.cpp
void TaskL(void *arg) {
  uint32_t old_cnt = l_btn_cnt;
  char buf[16];
  while (1) {
    if (l_btn_cnt != old_cnt) {
      sprintf(buf, "TaskL %d", l_btn_cnt); // \n
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        xSemaphoreGive(xSemSerial);
      }
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 20, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 20);
        xSemaphoreGive(xSemLCD);                  
      }
      old_cnt = l_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void TaskR(void *arg) {
  uint32_t old_cnt = r_btn_cnt;
  char buf[16];
  while (1) {
    if (r_btn_cnt != old_cnt) {
      sprintf(buf, "TaskR %d", r_btn_cnt); // \n
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 60, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 60);
        xSemaphoreGive(xSemLCD);
      }
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        xSemaphoreGive(xSemSerial);        
      }
      old_cnt = r_btn_cnt;
    }
    vTaskDelay(1);
  }
}

TaskLではシリアル回線⇒LCDの順で資源確保、TaskRではLCD⇒シリアル回線の順で資源確保を行っている。TaskLでのLCDの資源確保待ち、TaskRでのシリアル回線の資源確保待ちということがありうるが、それぞれのタスクで最初の資源確保後、次の資源確保までにタスクスイッチが行われずに、このコードでデッドロックを起こすことはできなかった。

デッドロック3

xSemaphoreTake()の成功したときに、vTaskDelay()をコールすれば、タスクスイッチが行われる。例えば、TaskRがLCD資源を確保中に、TaskLがLCD資源確保を行う状況を作り出せるのではないか、、と考えた。ここでも、TaskL()およびTaskR()のみ記載。

DeadLock3.cpp
void TaskL(void *arg) {
  uint32_t old_cnt = l_btn_cnt;
  char buf[16];
  while (1) {
    if (l_btn_cnt != old_cnt) {
      sprintf(buf, "TaskL %d", l_btn_cnt); // \n
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        vTaskDelay(1);  // ここでタスクスイッチ!
        xSemaphoreGive(xSemSerial);
      }
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 20, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 20);
        xSemaphoreGive(xSemLCD);                  
      }
      old_cnt = l_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void TaskR(void *arg) {
  uint32_t old_cnt = r_btn_cnt;
  char buf[16];
  while (1) {
    if (r_btn_cnt != old_cnt) {
      sprintf(buf, "TaskR %d", r_btn_cnt); // \n
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 60, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 60);
        vTaskDelay(1);  // ここでタスクスイッチ!
        xSemaphoreGive(xSemLCD);
      }
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        xSemaphoreGive(xSemSerial);        
      }
      old_cnt = r_btn_cnt;
    }
    vTaskDelay(1);
  }
}

論理的にはデッドロックが発生しえるが、このコードでも発生させることはできなかった。ボタン押下のタイミングが影響しているのであろう。

おわりに

故意にデッドロック起こすのにも頭を使う。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?