デッドロック
今回のお題は、FreeRTOS上でデッドロックを故意に発生させるもの。Arduino IDE環境下で、ESP32搭載のM5Stackを使う。
方針
2つのタスクが2つの資源(LCDおよびシリアル回線)を利用し、これらの資源の排他制御が不適切な例を考える。2つのタスクは、M5Stackの左ボタン押下時に、LCD表示及びシリアル経由でPCへの表示を行うタスク、右ボタン押下時に同様のことを行うタスクである。また、表示内容はボタンが押された回数とする。
その前に(排他制御せず)
デッドロック以前に、排他制御自体を行わないと、どうなるかをトライ。
# 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()とでは、ボタン押下のたびにカウントをインクリメントする。
これを実行し、両方のボタン押下を連続的に続けていくと、リブートしてしまう、、、。
デッドロック1(不適切な排他制御(資源の確保))
LCDおよびシリアル回線利用のための排他制御を入れるが、あえてデッドロックするような制御をおこなう。
TaskLでは、シリアル回線資源が確保できたときのみ、LCD資源の確保を実施し、TaskRでは、LCD資源が確保できたときのみ、シリアル回線資源の確保、、、を実施する。これにより、TaskLではシリアル回線資源確保後のLCDの解放待ち、TaskRではLCD資源確保後のシリアル回線資源の解放待ち、、、という状況を比較的簡単に作ることができる(ボタン押下のタイミング次第ではあるが)。
# 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()を呼び出しているところがミソ。
その時のPC側の表示及び約3時間後の強制リブート(M5 Stack赤ボタン押下)の様子。
デッドロック2
TaskL()およびTaskR()のみ記載。その他のコードは、デッドロック1と同じ。
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()のみ記載。
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);
}
}
論理的にはデッドロックが発生しえるが、このコードでも発生させることはできなかった。ボタン押下のタイミングが影響しているのであろう。
おわりに
故意にデッドロック起こすのにも頭を使う。