前回からの続きです。
このテーマを最初からご覧になる場合はこちらからどうぞ。
スリープからセマフォへの置き換え
前回、ソフトウェアをμITRONからZephyrへに移植する際に、両者ではスリープのシステムコールの振る舞いに差異があることを説明しました。
これが結構「罠」的なもので、私が執った回避策は、スリープをセマフォに置き換えるという決断でした。
これにより、以下の2点の問題を解決することができます。
1.Zephyrの「k_sleep()」は、その起床原因がタイムアウトなのか、ウェイクアップ要求によるものかの区別がない
2.Zephyrの「k_sleep()」は、待ち状態期間中の「k_wakeup()」のキューイングを行わない
今回は、そのスリープをセマフォに置き換える過程を「zephye-sample」を改造しながら説明したいと思います。
改造したものは「zephyr-sample」リポジトリの「semaphore」ブランチで実装していますので、せっかちな方はそちらをご覧ください。
Cygwinなどで以下のように入力すれば、今の「master」ブランチから「semaphore」ブランチに切り替えられますよ。
$ git checkout -b semaphore origin/semaphore
「sample1.c」の102行目以降に注目してください。
オブジェクト実体の定義の下に、新たに作成するセマフォ(スレッドが3つなのでセマフォも3つ)とポインター配列を追記します。
赤い部分が修正点です。
- ...
- /*
- * オブジェクト実体を定義
- */
- struct k_thread thread1;
- K_THREAD_STACK_DEFINE(thread_stack1, STACK_SIZE);
- struct k_thread thread2;
- K_THREAD_STACK_DEFINE(thread_stack2, STACK_SIZE);
- struct k_thread thread3;
- K_THREAD_STACK_DEFINE(thread_stack3, STACK_SIZE);
- struct k_sem sem1;
- struct k_timer cychdr1;
- /*
- * スリープ用セマフォを定義
- */
- struct k_sem sem_slp1;
- struct k_sem sem_slp2;
- struct k_sem sem_slp3;
- struct k_sem* p_sem_slp[3];
- /*
- * 並行実行されるスレッドへのメッセージ領域
- */
- char message[3];
- ...
上記が追記されたことを前提として「sample1.c」の154行目以降に注目してください。
子スレッド関数です。
ここで、実際にスリープをセマフォに置き換えています。
- ...
- /*
- * 並行実行されるスレッド
- * (idle スレッド)
- */
- void thread(void *exinf, void *dummy2, void *dummy3)
- {
- volatile uint32_t i;
- int n = 0;
- int threadno = (int)exinf;
- const char *graph[] = { "|", " +", " *" };
- char c;
- int32_t timeout;
- uint32_t stime1, stime2;
- int32_t v;
- int ret;
- while (true) {
- syslog(
- "thread%d is running (%03d). %s\r\n",
- threadno,
- ++n,
- graph[threadno-1]
- );
- /*
- * 以下の空ループは,k_busy_wait(400000); としてもよい.
- */
- for (i = 0; i < task_loop; i++);
- /*
- * イールド処理
- */
- v = k_thread_priority_get(k_current_get()) - HIGH_PRIORITY;
- if (yield_flag[v]) {
- yield_flag[v] = false;
- k_yield();
- }
- c = message[threadno-1];
- message[threadno-1] = 0;
- switch (c) {
- case 'e':
- syslog("#%d#return\r\n", threadno);
- /*
- * Zephyrにはext_tsk()に相当するAPIは存在しない.
- * 従ってreturnによってスレッド関数を抜けることでスレッドを終了させる.
- * (この処理はext_tsk()でも同様)
- */
- return;
- case 's':
- wakeup_flag[threadno-1] = WAKEUP_BY_TIMEOUT;
- syslog("#%d#k_sleep(K_FOREVER)\r\n", threadno);
- /*
- * k_sleep()をk_sem_take()に置き換える
- */
- //k_sleep(K_FOREVER); // コメントアウト!
- k_sem_take(p_sem_slp[threadno-1], K_FOREVER); // 追記!
- if (wakeup_flag[threadno-1] == WAKEUP_BY_RELEASE_WAIT) {
- /*
- * 強制的に起床させられた場合
- */
- syslog(
- "Release Wait reported by "
- "`k_sleep(K_MSEC(K_FOREVER))\' in line %d of `%s\'.\r\n",
- __LINE__,
- __FILE__
- );
- }
- break;
- ...
243行目に注目です。
ソースのコメントの通りなのですが…。
μITRONの「tslp_tsk()」の場合は、タイムアウトだった場合は「E_TMOUT」を返します。
Zephyrの「k_sem_take()」の場合は、タイムアウトだった場合は「-EAGAIN」というエラー値を返します。
(「EAGAIN」の値の頭にマイナス「-」を入れてますので、負の値です。)
普通に待ち解除(セマフォの資源が返された)された場合は「0」が返ります。
このように「k_sleep()」とは異なり「k_sem_take()」の場合は、起床原因が判断できるのです!
- ...
- case 'S':
- wakeup_flag[threadno-1] = WAKEUP_BY_TIMEOUT;
- syslog("#%d#k_sleep(K_MSEC(10000))\r\n", threadno);
- /*
- * k_sleep()をk_sem_take()に置き換える
- */
- //k_sleep(K_MSEC(10000)); // コメントアウト!
- ret = k_sem_take(p_sem_slp[threadno-1], K_MSEC(10000)); // 追記!
- if (wakeup_flag[threadno-1] == WAKEUP_BY_RELEASE_WAIT) {
- /*
- * 強制的に起床させられた場合
- */
- syslog(
- "Release Wait reported by "
- "`k_sleep(K_MSEC(10000))\' in line %d of `%s\'.\r\n",
- __LINE__,
- __FILE__
- );
- } else
- /*
- * k_sem_take()はタイムアウトの場合は「-EAGAIN」が返る
- */
- //if (wakeup_flag[threadno - 1] == WAKEUP_BY_TIMEOUT) { // コメントアウト!
- if (ret == -EAGAIN) { //追記!
- /*
- * タイムアウトで起床した場合
- */
- syslog(
- "Timeout reported by "
- "`k_sleep(K_MSEC(10000))\' in line %d of `%s\'.\r\n",
- __LINE__,
- __FILE__
- );
- }
- break;
- ...
上記が追記されたことを前提として「sample1.c」の339行目以降に注目してください。
メインスレッド関数の冒頭部分です。
まず、新しく作ろうとするセマフォをポインタに代入します。
これは、子スレッドで使いやすいようにするためです。
配列のインデックスが「threadno - 1」となります。
forループで回している「k_sem_init()」というシステムコールは、セマフォの初期期化を行っています。
- ...
- /*
- * メインルーチン
- * (main スレッド)
- */
- int main(void)
- {
- T_THREAD_DEFINE_t thread_cfg[] = {
- {THREAD1, thread_stack1, K_THREAD_STACK_SIZEOF(thread_stack1), (void *)1, MID_PRIORITY},
- {THREAD2, thread_stack2, K_THREAD_STACK_SIZEOF(thread_stack2), (void *)2, MID_PRIORITY},
- {THREAD3, thread_stack3, K_THREAD_STACK_SIZEOF(thread_stack3), (void *)3, MID_PRIORITY},
- };
- char c;
- k_tid_t threadid = THREAD1;
- int threadno = 1;
- int threadpri;
- #ifndef TASK_LOOP
- volatile uint32_t i;
- uint32_t stime1, stime2;
- #endif /* TASK_LOOP */
- /*
- * セマフォの初期化
- *
- * syslog()マクロ内でターミナル出力の排他処理のために使用.
- */
- k_sem_init(SEM1, 1, 1);
- /*
- * スリープ用セマフォを定義
- */
- p_sem_slp[0] = &sem_slp1;
- p_sem_slp[1] = &sem_slp2;
- p_sem_slp[2] = &sem_slp3;
-
- /*
- * スリープ用セマフォを扱いやすいようにポインターの配列に代入
- */
- for (i = 0; i < 3; i++)
- k_sem_init(p_sem_slp[i], 0, 1);
- syslog("Sample program starts.\r\n");
- ...
ここで「k_sem_init()」の説明を少しだけ。
システムコールのプロトタイプは、以下のようになっています。
- int k_sem_init (struct k_sem * sem,
- unsigned int initial_count,
- unsigned int limit )
第1引数「sem」は、セマフォのポインタです。
第2引数「initial_count」は、セマフォカウントの初期値です。
第3引数「limit」は、セマフォカウントの上限値です。
…なんのこっちゃ?
「sem」は、初期化をするセマフォを指定することは分かりますね?
セマフォというのは、ビーチフラッグのように複数のスレッドがカウント(資源とも言う)を取り合います。
用意されたフラッグの本数が「limit」であり、よーいドン!の段階で砂浜に立てらえたフラッグの本数が「initial_count」です。
このプログラムでは「limit = 1」なので用意されたフラッグが1本だけです。
そして「initial_count = 0」なので競技を開始した時点で砂浜にはフラッグが立てられていない状態です。
これでは、選手(スレッド)はスタッフがフラッグを立ててくれるまで待っていなければなりません。
これが、セマフォ資源待ち状態というヤツです。
フラッグが立てられると、一番早い選手がフラッグを奪い、いずれはスタッフに返します。
負けた選手はスタッフがそのフラッグを再び立ててくれるまで、やはり待ち状態となります。
勝った選手は「とったどー!」ということで「k_sem_take()」の返り値で「0」を返し、時間まで待ってもフラッグを取れなかった選手は「待ってたのにスタッフが次のフラッグを立ててくれなかった…スタッフゥー!?」と言って文句、つまり「-EAGAIN」を返します。
このセマフォ資源待ち状態をスリープに利用しようというのが今回の改造のポイントです。
まあ、今回の場合はフラッグ取りに行く選手は一人だけですけどね…。
では、実際にフラッグを立てる部分ですが、「sample1.c」の538行目以降に注目してください。
553行目の「k_sem_give()」というのが、セマフォ資源を1つ返すシステムコールです。
つまり、ビーチフラッグで言うところのスタッフ(メインスレッド)が選手(子スレッド)のためにフラッグを1本立てているところです。
- ...
- case 'w':
- /*
- * このサンプルでは,スリープ中のスレッドが起床された原因を判断するため,
- * wakeup_flag[]と組み合わせて同等の動作を再現する.
- *
- * TOPPERS/ASP の場合はwup_tsk()による起床要求を1回までキューイングできる.
- * (つまり2回以降はE_QOVRが返る)
- * 一方,Zephyrのk_wakeup()は起床要求のキューイングをしないことに留意する.
- */
- syslog("#k_wakeup(%d)\r\n", threadno);
- wakeup_flag[threadno-1] = WAKEUP_BY_WAKEUP;
- /*
- * k_wakeup()k_sem_give()に置き換える
- */
- //k_wakeup(threadid); // コメントアウト!
- k_sem_give(p_sem_slp[threadno-1]); // 追記!
- break;
- ...
これで準備は整いました。
この改造した「zephyr-sample」をビルドし、ターゲットで動かしてみます。
ターゲットをパソコンに接続しTeraTermで見てみましょう。
まずは、起床原因の区別です。
分かりやすいように「thread1」の優先度を他のスレッドよりも上げておきましょう。
ターミナルに対し「1」、「>」と入力します。
これから「thread1」をスリープさせるのですが、その間に「thread2」が起床します。
このように「thread1」だけ優先度を上げておけば、「thread1」が起床した時に「thread2」を押しのけて処理を取り戻すようになるので動作が分かりやすくなります。
では早速「thread1」をスリープさせます。
ターミナルに対し「S」と入力します。
大文字の「S」ですので、「thread1」に10秒間のスリープを命令したことになります。
この直後から「thread2」に処理が移っていることがわかりますね。
10秒以内にターミナルに対し「w」と入力し、待ち状態解除を命令します。
まずは、普通に待ち状態が解除された場合の反応です。
待ち状態解除を受けて「thread1」が復活しましたね。
これは「k_sem_take()」が返り値「0」を返した結果です。
次に、タイムアウトの場合を見てみましょう。
ターミナルに対し大文字の「S」を入力します。
このあと、何もせずにずっと見ていましょう。
で、10秒経つと…
…タイムアウトした旨が表示されます。
これは「k_sem_take()」がタイムアウトを示す返り値「-EAGAIN」を返した結果です。
ビーチフラッグの例えを思い出してください。
このようにすることで、選手(スレッド)は自身がフラッグ(資源)を得たのか?あるいは、時間まで待っても取れなかった(タイムアウト)か?を知っていますので待ち解除の原因は明確になります。
これで、起床原因の区別に関しては、一件落着です。
次に、キューイングの件です。
あるタスク(スレッド)に対し、それがスリープ状態ではない時に起床命令を実行した場合の違いです。
早速、すでに今起床している(スリープ状態ではない)「thread1」に対して起床命令を発行してみましょう。
念の為一度ターゲットをリセットし(他のスレッドよりも高くした「thread1」の優先度を元に戻す意味)、ターミナルに対し「1」、「w」と順に入力します。
この後、スリープ命令を小文字の「s」で発行すると、以下のように「thread1」はスリープになりません。
スリーブをセマフォに置き換えたことにより、キューイングが効くようになった証左です。
もう一度、スリープ命令を小文字の「s」で1回発行すると、ようやく「thread1」はスリープし、代わりに「thread2」に処理が移ります。
前回の起床命令により、キューイングを使い果たしたからです。
これにより「zephyr-sample」は、μITRON、すなわちTOPPERS/ASPのサンプルプログラムと全く同じ振る舞いとなりました。
選手(スレッド)が今待ち状態かどうかに関わらず、カウント(フラッグの本数)で待ちを解除するか否かを判断するため、キューイングもできていることになります。
このように、μITRONのデバイスドライバやアプリケーションをZephyrに移植する際には、μITRONのスリーブ処理をZephyr側ではセマフォに置き換えることにより、全く同じ動作を再現することができます。
そして、特にキューイングの件は、スレッドの遷移が難解であることもあり、これが原因の不具合は極めて発見し難いものとなります。(体験済み…。)
割り込みハンドラからの起床命令を、何故かスレッドのスリープが取りこぼす…なんて場合は、まずこれが原因であることを疑うべきです。
さて、このμITRONとZephyrとでのスリーブの違いに関しては、実際に自分が痛い目に遭ってしまったので気合を入れて書いておこうと思いました。
逆を言えば他に両者の間に異なることは少なく、μITRONのプログラマーは、比較的短時間でZephyrを操れるようになります。
<続く>