2018年4月1日日曜日

kona linux 4.0 jack リアルタイムカーネル (RTLinux) を試す (2) スケジューラ優先度

前回はkona linux4.0にRTlinuxをインストした顛末を書きました.RTLinuxでは周期的割り込みの時間精度が高いことを確認できました.  →前回へ

ただ、RTLinuxで周期割り込みthreadをどうやって実装してるんだろという点についてはさっぱりわからんちんでした.

今回はアプリソフトを弄ってみて、どうやってるんだろの疑問に自分で答えます.

印象としては、1chip-CPUのtimer割り込みの単純さに比較すると、鬼のようにめんどくさいことをしてます.はぁ~というかんじー.

参考にしたのはcyclictestというtoolです.周期実行threadの時間誤差を統計してくれるtoolです.これの肝の部分を抜き出してどんな動きをさせているのかを調べました.

なお、本稿はLinux kernel 4.14.24と、リアルタイムパッチrt19の組み合わせでのハナシです.本稿を書いている時点では新しいversionかと思います.

間違いがあるかもしれません.その際は素直に死んでくださいませ.

-----
【timer割り込みと、周期実行threadの大きな違い】
わたしはOSに詳しい人ではないので、周期割り込みというと1チップCPUのtimer割り込みしか知りません.timer割り込みの仕組みは、カウンタを廻しておいてカウント値が所定の値になったらピキッと割り込みがかかるようになっています.

ところが周期実行threadの場合は、誤解を恐れずに述べるならば、
2018.4.1.16:39:49:0293840
のような実時間時計を参照します.(正確にはUNIX時刻だと思われる=1970年1月1日午前0時0分0秒からの経過秒数)
例えば1秒後にthreadが起動されたいなら、
2018.4.1.16:39:50:0293840
に目覚まし時計をセットして眠ります.
眠るという表現は的を得ていて、clock_nanosleep(時刻)という関数を呼び出して眠ります.寝ている間はOSにCPUを大政奉還しているものと思います.

threadでは実時間時計を使うのですから、system callで現在時刻を読む→未来の目覚まし時刻を計算→目覚ましセット→寝る、という仕事が生じます.

ふ~んそうなんだ、、、正直言ってめんどくさいですかねぇ.もしかしたら、Kernel2.xの頃のRTLinuxにはXXmSec毎に目覚まし鳴らせ!的な関数が提供されていたのかもしれません.また、Kernel名人ならmoduleを駆使して簡単に処理する技があるのかもしれません.わたしにはそんな技はありません.


【通常KernelとリアルタイムKernelの扱い方の違い】
呼び出す関数が異なる、などのような本質的な違いは無いようです.周期実行threadのexeファイルは、通常KernelでもRT Kernelでも動きましたから.ただし結果には差が生じます.RT Kernelの方が周期誤差が少ないです.

大雑把にはそういう理解なのですが、もう少し細かい理解が必要です.周期誤差を改善するためにschedulerに根性を注入する必要があります.たとえRT Kernelであってもschedulerが根性なしだと周期誤差はだらしないままです.
つまりこう云えます.
  1)RT Kernelを意識せずにthreadのコードを書くのが基本だが、
  2)高優先度のscheduler設定にするのがRTさを享受するために必須

ヤレヤレ、schedulerと来たもんだ...

PC関連書籍には流行り廃りがあるようで、今本屋へ云ってもpthreadの書籍はほとんど売られていません.10年以上前でしたかね、windowsのスレッドの本がたくさん在ったのは.絶版で古本でしか入手できない書籍を除くと、今出回っているのはこれだけのように思います.帯に書かれた「レガシーな技術を今あえて学ぶ!」があまり売れないジャンルを示しています.
この書籍はpthreadの生成消滅についてとても判りやすく書かれていて好感が持てるのですが、初級者向けだからかschedulerについては解説が無いんです.立ち読みしたけど買ってません.

もう一つ、1998年刊行で絶版と思われるのがこちらの怖い本.毛虫の表紙だなんてどうかしています.怖くて触れません.古本を買ってすぐに表紙を切って捨てました.この毛虫本にはschedulerについて少しですが解説されていますので有り難い.しかし英文和訳文は読みにくいので有り難くない.


以下ではソースコードの説明などです.ソースは300行ぐらいです.

【ソースコードとビルドと実行】
2coreのceleronマシンでしか動作確認しておりません.
ソースはこちら.GPLとか判らなくてゴメン.
ビルドはこれで出来ています.RTライブラリは指定してません.
cc -c jitter.c -Wall -O3 -o jitter.o -lpthread
cc -Wall -O3 -o jitter jitter.o -lpthread
root権限のひとにしか実行できないと思いますのでsu rootなどよしなにやってください.

動作例1 (scheduler最強)  =RT Kernelの能力発揮できてる状況
$ jitter -p99
CPUs = 2      policy = fifo
Th: 0 CPU:0 Pol: 1 Pri:99 Intvl: 1000 Cyc:10000 Min: 4 Act:  9 Avg:  8 Max:  38
Th: 1 CPU:0 Pol: 1 Pri:99 Intvl: 1100 Cyc: 9091 Min: 3 Act: 10 Avg: 14 Max:  64
Th: 2 CPU:0 Pol: 1 Pri:99 Intvl: 1200 Cyc: 8334 Min: 3 Act: 10 Avg:  9 Max: 107
Th: 3 CPU:0 Pol: 1 Pri:99 Intvl: 1300 Cyc: 7693 Min: 4 Act:  8 Avg: 18 Max:  66
Th: 4 CPU:0 Pol: 1 Pri:99 Intvl: 1400 Cyc: 7143 Min: 4 Act: 36 Avg:  8 Max:  61
-p99はscheduler優先度が最高である意味.
CPUs=2は2coreである意味.
policy=fifoはschedulerの動作モードがfifoモードである意味.fifoは最強かと.
Th:0-4はスレッドを5つ起動している意味.
CPU:0は0番目のCPUで走行している意味.
Pol:1はscheduler動作モードfifoの意味.=最強
Pri:99はscheduler優先度99の意味.=最強
Intvl:1000はthread周期=1000uSecの意味 (2番目以降のthread周期は10%増し)
Cyc:10000はthreadを10000回呼び出す意味.
Min:Act:Avg:Max:はIntvlからの誤差uSecの意味.小さいと優秀

動作例2 (scheduler最弱) =これじゃ通常Kernelと変わらないよな状況
$ jitter
CPUs = 2      policy = other
Th: 0 CPU:0 Pol: 0 Pri: 0 Intvl: 1000 Cyc:10000 Min:18 Act: 68 Avg: 66 Max: 5281
Th: 1 CPU:0 Pol: 0 Pri: 0 Intvl: 1100 Cyc: 9097 Min:10 Act: 63 Avg: 70 Max: 1088
Th: 2 CPU:0 Pol: 0 Pri: 0 Intvl: 1200 Cyc: 8338 Min:12 Act: 74 Avg: 69 Max: 1768
Th: 3 CPU:0 Pol: 0 Pri: 0 Intvl: 1300 Cyc: 7697 Min: 6 Act: 65 Avg: 60 Max: 1001
Th: 4 CPU:0 Pol: 0 Pri: 0 Intvl: 1400 Cyc: 7145 Min: 8 Act: 61 Avg: 67 Max: 5387
動作例1よりも誤差uSecが悪化しています.その理由は、
Pol:0はscheduler動作モードがotherの意味.otherは最弱.
Pri:0はscheduler優先度0の意味.=最弱

動作例3 (thread 20個、1000回で終了)
$ jitter -t20 -l1000
CPUs = 2      policy = other
Th: 0 CPU:0 Pol: 0 Pri: 0 Intvl: 1000 Cyc: 1000 Min:  5 Act: 63 Avg: 63 Max: 1103
Th: 1 CPU:0 Pol: 0 Pri: 0 Intvl: 1100 Cyc:  910 Min:  6 Act: 82 Avg: 69 Max:  885
Th: 2 CPU:0 Pol: 0 Pri: 0 Intvl: 1200 Cyc:  835 Min: 10 Act: 91 Avg: 52 Max:  668
Th: 3 CPU:0 Pol: 0 Pri: 0 Intvl: 1300 Cyc:  770 Min:  9 Act: 70 Avg: 61 Max:  490
  :
Th:16 CPU:0 Pol: 0 Pri: 0 Intvl: 2600 Cyc:  384 Min: 26 Act: 63 Avg: 74 Max: 4408
Th:17 CPU:0 Pol: 0 Pri: 0 Intvl: 2700 Cyc:  371 Min:  6 Act: 60 Avg: 58 Max:  144
Th:18 CPU:0 Pol: 0 Pri: 0 Intvl: 2800 Cyc:  357 Min: 14 Act: 67 Avg: 77 Max: 4338
Th:19 CPU:0 Pol: 0 Pri: 0 Intvl: 2900 Cyc:  344 Min: 18 Act: 63 Avg: 74 Max: 3920

動作例4   (その他のoption)
-?         ヘルプ表示
-i456     thread0周期=456uSec   (2番目以降のthread周期は10%増し)
-s25      25秒で自動停止


【main()】
流れの主要箇所だけ抜き出します.エラー処理も割愛します.

int main(int argc, char **argv)
{
        options(argc, argv);      コマンドoptionの解読

        parametersはthreadの諸元を記述します.statisticsは測定結果を格納します.
        それらのメモリエリアを確保します.
        num_threadsには造るthread数が入ってます.
        parameters = calloc(num_threads, sizeof(struct thread_param *));
        statistics = calloc(num_threads, sizeof(struct thread_stat *));

        このfor()でnum_threads個のthreadを生成します.
        for (i = 0; i < num_threads; i++) {
                各threadの諸元を格納する構造体の確保
                parameters[i] = par = calloc(1,sizeof(struct thread_param));

                各threadの測定結果を格納する構造体の確保
                statistics[i] = stat = calloc(1,sizeof(struct thread_stat));

                構造体のデータ設定
                par->prio = priority;        プロセス優先度
                schedulerポリシー、FIFOは最強、OTHERは最弱で通常
                if (priority && (policy == SCHED_FIFO)) par->policy = policy;
                else    par->policy = SCHED_OTHER;
                par->interval = interval + interval*0.1*i;      thread周期
                par->max_cycles = max_cycles;            thread loop回数
                par->stats = stat;                測定結果格納場所の指定
                par->tnum = i;                   thread番号
                stat->min = 1000000;
                stat->max = 0;
                stat->avg = 0.0;

                thread生成.timerthread()を紐付け.
                pthread_create(&stat->thread, &attr, timerthread, par);
        }     thread生成ここまで

        全threadがon goingゆえshutdown=0である限りloopする
        while (!shutdown) {
               usleep(100000);      100mS毎に測定結果を表示する
               for (i = 0; i < num_threads; i++){
                      printf(.....)       測定結果を表示
               }
        }

        for (i = 0; i < num_threads; i++)     全threadの終了を待つ
               pthread_join(statistics[i]->thread, NULL);
}


【timerthread()】
周期実行threadの本体.主要部分だけ.
loopの動作はこうなっています.
目覚ましセット→寝る→目覚める→現在時刻を読む→次の目覚まし時刻を計算

static void *timerthread(void *param)
{
        scheduler設定
        memset(&schedp, 0, sizeof(schedp));
        schedp.sched_priority = par->prio;          優先度
        sched_setscheduler(0, par->policy, &schedp);      ポリシー

        threadの周期時間(秒)を計算する.整数秒とnsec単位の少数秒で表現
        interval.tv_sec = par->interval / USEC_PER_SEC;
        interval.tv_nsec = (par->interval % USEC_PER_SEC) * 1000;

        clock_gettime(0, &now);     現在時刻を採取

        目覚まし時計時刻を計算= next = now + interval
        next = now;
        next.tv_sec += interval.tv_sec;
        next.tv_nsec += interval.tv_nsec;
        tsnorm(&next);           少数秒が1を超えてたら桁上げ

        N秒後に自動停止する時刻を計算= stop = now + N
        memset(&stop, 0, sizeof(stop));
        if (duration) {
          stop = now;
          stop.tv_sec += duration;
        }

        周期loop
        while (!shutdown) {      測定終了したthreadがshutdown=1にする
                目覚まし時計をセットして寝る
                ret = clock_nanosleep(0, TIMER_ABSTIME, &next, NULL);

                ret = clock_gettime(0, &now);       目覚めた時刻を採取

                diff = calcdiff(now, next);      diff = 目覚めた時刻 - 目覚まし時刻

                if (diff < stat->min)   stat->min = diff;       測定値update
                if (diff > stat->max)   stat->max = diff;
                stat->avg += (double) diff;
                stat->act = diff;

                停止時刻ならオシマイにする
                if (duration && (calcdiff(now, stop) >= 0)) shutdown=1;

                stat->cycles++;          loop回数カウント

                目覚まし時刻を計算 = next = next + interval
                next.tv_sec += interval.tv_sec;
                next.tv_nsec += interval.tv_nsec;
                tsnorm(&next);        少数秒が1を超えてたら桁上げ

                loop回数満了ならオシマイにする
                if (par->max_cycles == stat->cycles) shutdown=1;
        }     周期loopここまで
        pthread_exit(&ret);       threadの自己終了
}


【まとめ】
ふーん、RT Kernelってそうなのかぁ、なんだかなぁ...
でもやり方が判ったからまぁいいや.

その1へ           その3へ

かしこ

4 件のコメント:

  1. 毛虫の表紙だなんてどうかしています.怖くて触れません.古本を買ってすぐに表紙を切って捨てました
    ーーー
    もうすぐ桜の木の下を通るとき、消毒剤でやられた毛虫ちゃんがぼとぼと落ちてきます。ご用心を。

    返信削除
    返信
    1. 桜の木の下を歩けません、禁忌禁忌禁忌

      削除
  2. たかちゃん2020年3月31日 8:54

    毛虫の表紙?
    面白い!
    オライリーの本て、内容と関係のない動物の絵が表紙だけど
    誰のこだわりだろうw
    最近買ったディープラーニングの本では、表紙は魚なんだけど本文中の挿絵がサソリですww

    返信削除