連載11-マルチコア動作のデバッグ
前回はマルチコアでのリアルタイムOSについて触れました。
今回は、マルチコアで動作している際に便利なデバッグ機能をご紹介します。
1.マルチコアでマルチタスクのデバッグ
前回の内容を振り返ってみましょう。
一つのチップに同じCPUコアが複数入っており、かつ、すべてのCPUがメモリ空間を共有(キャッシュ上のデータも含め)しているプロセッサを対称型マルチプロセッサ(Symmetric Multiprocessor、SMP)と言います。
SOLID-OSがサポートしているCortex-Aコアは、それです。
SOLID-OSが採用しているTOPPERSの場合、各CPUはタスク単位で割りついています。
割り込みについては、割り込みをハンドリングするCPUを指定することができます。
イベントフラグやメッセージ等のタスク間通信は違うCPU間でも可能。
共通リソースの排他制御も違うCPU間でも可能。
すなわち、マルチコアでのリアルタイムOSとはいっても、シングルコア上で動作させるのと同じようにプログラムを書くことができます。
違うのは、そのタスク、割り込みハンドラを実行させるCPUを指定できること。
マルチコアであろうと、シングルコアであろうと、「マルチタスク」と考えてしまえば(実行しているCPUが何であれ)両者の差は意識しなくてもプログラムは書けます。
じゃ、別にマルチコアだからって特別なデバッグ機能、要らないんじゃない?
いえいえ。
最初にタスク・割り込みハンドラを各CPUに割り付けていることを忘れてはいけません。
その割り当て、妥当ですか?という心配が出てきます。
それに、最初は妥当でも、システムを動かしている途中に、とあるCPUの負荷がやたら重くなってしまって「ちょっと今だけ他CPUにヘルプ!」というのがあるかもしれません。
せっかくマルチコアなのだから、その恩恵を最大限に受けよう!ということです。
今回はそのような時に便利なデバッグ機能、以下2つをご紹介します。
・イベントトラッカー
・コアウインドウ
では、具体的に見ていきましょう。
2.イベントトラッカー
各タスクや割り込みハンドラ等が、どのタイミングでどれくらいの時間動いているのかを、時間軸にそって表示する機能です。
以下URLに、機能紹介が掲載されています。
https://solid.kmckk.com/SOLID/doc/latest/user_guide/event_tracker.html
図を抜粋させて頂きます。
これはシングルコアの場合の図です。
最初タスク7が実行されていて、途中で割り込み番号29の割り込みが入り、ハンドル後またタスク7に戻り、ほどなくしてタスク6に遷移していることがわかります。
そしてまた割り込み番号29の割り込みが入り、ハンドル後タスク6に戻り、少し後にタスク5に遷移しています。
青色■は、「待ち状態イベント (Wait に遷移)」という意味です。
使い方や表示されている記号等については、先のURLにあるPDFファイルに詳しく記載されています。
さらに、マルチコアでの実行時は、さらに「どのCPUで」という情報も付加して表示します。
このグラフから、
「CPU1に負荷が偏ってるなー。一方、CPU0は暇そうだなー。」とか、
「CPU0からCPU1にタスクを変えてみたけどタイミング悪いかなー」とか、
各タスクの実行時間からCPUの負荷を確認することができ、負荷分散の調整に一役買います。
以下URLにさらに詳しい説明が記載されていますので、ご参照ください。
https://solid.kmckk.com/SOLID/wp-content/uploads/2019/07/20190708_SOLID_eventtracker.pdf
3.コアウインドウ
ブレークポイントで停止した時、ソースコードが表示されているのは、そのブレークポイントがヒットしたCPUで実行されているソースコードです。
マルチコアの場合、例えばCPUが2つ搭載されている場合、その瞬間に動作しているタスクも2つありますよね。
しかしソースコードウインドウは一つだし、ローカル変数を表示しているウインドウも一つ。(かといって二つも三つも表示されていたら、ややこしい、、、)
例えば、表示されているのがCPU0で実行されているソースコード、ローカル変数だとします。
「その時CPU1は何してるの?」
を知りたい場合に使うのが、です。
例えばデッドロックしてしまったような場合、「お互い何してる?」が知りたいですよね。
「コアウインドウ」機能により、ソースコード表示やローカル変数表示を、他のCPU用に、がさっと切り替えることができます。
4.試してみる
では、実際に見てみましょう。
前回に書いた、タスク&GPIO割り込み発生プログラムで試してみます。
実機は前回同様、4つのCPUのうちCPU0とCPU1がSOLID-OSに割り当てられているRaspberry Pi4です。
前回の最後に書いたプログラムを使用しますが、CPUの割り当てを以下のように変更します。
・途中から“test_task2”をCPU1に、引っ越しさせてみるところは削除します。
・プログラムの最初から“test_task1”はCPU0で動作させ、“test_task2”はCPU1で動作させます。
加えて、前回はターミナル表示を目視したいために、各タスク内に “dly_tsk()“を入れていましたが、今回は削除します。
4.1 イベントトラッカー
イベントトラッカーを使用するためには、必要なイベントデータをメモリに蓄積する必要があります。したがって、少し準備を行います。
以下URLに、方法が記載されている通り、コード追加&プロジェクト設定変更、です。
https://solid.kmckk.com/SOLID/doc/latest/user_guide/event_tracker.html
4.1.1 IMPL_EVTTRK_Init()実装
イベントデータ保存柳雄域確保のためのコードです。
以下URLに記載されている実装例をそのままコピペします。
https://solid.kmckk.com/SOLID/doc/latest/user_guide/event_tracker.html#c.IMPL_EVTTRK_Init
4.1.2 イベントトラッカーの有効化
ターゲットによっては、ソリューションプロパティファイルを編集し、イベントトラッカーの有効化を行う必要があります。
以下URL記載の内容です。
https://solid.kmckk.com/SOLID/doc/latest/user_guide/event_tracker.html#id4
ソリューションプロパティファイルがどこにあるか、等の説明は以下に記載されています。
https://solid.kmckk.com/SOLID/doc/latest/os/solution-config.html
今回使用しているRaspberry Pi4用のSOLIDでは、このファイルはなく、イベントトラッカーは常に有効になっていますので、この手順は不要です。
4.1.3 イベントトラッカーウインドウの準備
イベントを取得するために、準備を行います。
デバッグメニューから、「ウインドウ」⇒「イベントトラッカー」を選択します。
すると、イベントトラッカーウインドウが開きます。
ウインドウ上で右クリックし、メニューを表示します。
「DLLのロード」を選択します。
ここでロードするDLL(EvtFilterSOLID_TOPPERS.dll)と、コア数(=2)を選択します。
EvtFilterSOLID_TOPPERS.dll は以下の場所にインストールされています
SOLIDインストール先フォルダ\tools\eventtracker\dll
SOLIDインストール先フォルダは、Rasbberry pi 4用の場合はC:\ProgramData\KMCです。
OKボタンを押すと、以下のような表示に変わります。
これで準備完了です。
4.1.4 実行
では、動かしてみましょう。
このプログラムは、
・TID=4の“test_task1”
・TID=5の“test_task2”
・割り込み番号154の割り込み
の3要素があります。
これらがどのように実行されているのか、見てみましょう。
①ブレークポイントを設定
test_task1に適当にブレークポイントを設定し、実行してみます。
ブレークポイントで停止したら、イベントデータを吸い上げてイベントトラッカーウインドウに表示する操作をします。
まず、ウインドウ上で右クリックし、メニューを表示します。
「解析」を選択します。
このようなウインドウが表示されますので、何も変更せずにOKボタンを押します。
すると、イベントトラッカーウインドウにこのように表示されました。
空白が多いですね。これは筆者自身ボタンを押す操作を行うために要した時間です。
(どんくさくてすみません)
右側を拡大してみます。
大きな空白の後、INT145の割り込み(=GPIOボタン押下)が少し動作し、次にTID=5の“test_task2”が動作していることがわかります。
これら、共にCPU1で動作していることもわかります。
プログラムと見比べてみましょう。
イベントハンドラ:
int gpioint_handler(void *param, SOLID_CPU_CONTEXT *cnt)
{
:
SOLID_LOG_printf("Button was pressed!\n");
ER ercd;
ercd = set_flg(flag, event_from_interrupt);
return 0;
}
ボタン押下で、event_from_interruptイベントを発行していました。
このイベントを待っているのは、“test_task2”でした。
void test_task2(VP_INT exinf)
{
:
while (1)
{
:
ercd = wai_flg(flag, event_from_interrupt, TWF_ORW, &ptn);
SOLID_LOG_printf("[ TASK2] Got event from interrupt.\n");
ercd = clr_flg(flag, ~event_from_interrupt);
:
}
return;
}
また、GPIO割り込みハンドラ、“test_task2”共にCPU1で動作しています。
ソースコードを見てみましょう。
GPIO割り込みハンドラ初期化部:
void gpioint_init()
{
// ① 割り込みハンドラ登録用構造体を作成
g_handler.intno = 145;
g_handler.priority = 10;
g_handler.config = 0b10; // SPI, エッジトリガ
g_handler.func = gpioint_handler;
g_handler.param = NULL;
//割り込み対象のCPUコアを指定する場合は以下のマルチコア用APIを使用する。
int target_processor = 1;
// ② 登録
int ret = SOLID_INTC_RegisterWithTargetProcess(&g_handler, 1 << target_processor);
// ③ 有効化
ret = SOLID_INTC_EnableM(g_handler.intno);
}
test_task2初期化部:
// タスク動的生成のための情報設定
tsk2.tskatr = TA_NULL;
tsk2.task = test_task2;
tsk2.itskpri = MID_PRIORITY;
tsk2.stksz = STACK_SIZE;
tsk2.stk = NULL; // スタックはカーネルが自動的に割り当てる(SOLIDのスタックフェンスが有効)
tsk2.iprcid = 2;
tsk2.affinity = UINT_MAX;
確かに。両方ともCPU1で動作しています。
ところで、各CPUの負荷はどうでしょうか。
再度イベントトラッカーウインドウを見てみましょう。
ここだけ見ると、CPU1ばかり一生懸命動いていて、CPU0はなんだか暇そうですね。
このように、プログラムが意図通りに動作していることや、負荷の様子がわかります。
負荷の様子を変えたい場合、前回触れたmig_tsk()関数を使い、タスクを割り当てているCPUを途中で引っ越しもすることができます。
4.1.5 タスクを引っ越しさせてみる
先程のように、このタスクが実行しているCPUを途中から変えてみたい場合がありますよね。
そして、mig_tsk()関数を使えばそれができることは、前回書きました。以下URLです。
https://note.com/yn_2022/n/nb4eaabee8721#050f7ac5-0951-4dc3-9f14-c842bd484f0a
ここでは、このプログラムをもう一度動かしてみて、イベントトラッカーで実際の動作を確認してみたいと思います。
[プログラム]
・test_task2、test_task2共に最初はCPU0。
・ボタン押下直前、
test_task2はボタン押下待ちに入る。
test_task1はmig_tsk()関数をコールし、test_task2をCPU1に引っ越しさせる
・ボタンが押され、割り込みハンドラが動作し、test_task2の待ち状態を解除
・test_task2はCPU1で動き始める。
TID5(=test_task2)を左端から右端へ、見ていってください。
最初は緑線(CPU0)で動いていました。
途中、空白部分は筆者がボタンを押す操作でモタモタしているところです。
ボタンを押したら一番下のINT145が赤線(CPU1)で動きます。
そして、TID5が赤線(CPU1)で動き始めました。
mig_tsk()関数によりタスクが他のCPUに引っ越しできることが目視できました。
4.2 コアウインドウ
さて、今、ちょうど“test_task1”に設定したブレークポイントで停止しています。
RTOSビュワーを見ても、“test_task1”だけがRunningになっています。
※RTOSビュワーでの各コア番号の見え方については前回書きましたので、よろしければご参照ください。
以下URLの少し下にあります。
https://note.com/yn_2022/n/nb4eaabee8721#d79cb141-e161-4300-a43d-013187d7f16e
このプログラムでは、“test_task1”と“test_task2”が相互にイベントを発行しあうように作ってあるため、片方動作しているときはもう片方がイベント待ちでWaiting状態になっています。
“test_task1”はCPU0で動作していますので、ここで表示されているmain.cppのソースコードは、CPU0が実行している部分です。
では、CPU1は、今どこを走っているのでしょうか。
コアウインドウを使ってみてみましょう。
デバッグメニューから、「ウインドウ」⇒「コア」を選択します。
コアウインドウが起動します。
左端にある三角をクリックすると、コア情報が表示されます。
さらに、以下の部分をダブルクリックしてみます。
以下のウインドウが表示されました。
CPU1が実行している部分が表示されています。
といっても、残念ながら今回はカーネルのディスパッチルーチンにいたようで、ソースコードがないため、バンっと表示はされず、一旦「ソースは利用できません」と表示されました。
ですが、逆アセンブルの表示をクリックすると、実行中の命令コードが表示されました。
このように、各CPUが今何を実行しているのか、についても見ることができました。
5.まとめ
今回は、マルチコアCPUというメリットを生かすための便利なデバッグ機能をご紹介しました。
せっかくマルチコアデバイスを使っているのだから、効率良くCPU資源を利用したいですよね。このように動作の様子がわかる機能が提供されていると、思ってもいなかった偏りが発見できたりして、良い改善点を見つけられそうですね。
次回は、例外が発生した時の対処方法について書いていきます。