OS自作入門 -Advent34-

NO IMAGE

Step9 優先度スケジューリング 延長戦

kozosファイルの修正が終わったのでシステムコールの追加を行なっていく。

システム・コールの追加

syscall.hの修正では、kozosファイルで利用していた構造体の定義を追加したり、kz_run()のパラメータに優先度を渡せるように修正を行う。

syscall.cの修正では、システム・コールのAPIとなる関数の実装を行なっている。

サンプル・プログラム

スレッド動作のサンプルとして、3種類のスレッドを動作させる。

これを実装。

アイドル・スレッド

main.cに対しての修正を行う。

kz_start()で初期スレッドが開始され、start_threads()test09_1_main(),test09_2_main(),test09_3_main()が別々のスレッドとして起動するようになっている。起動した後は、sleepで省電力モードに移行して、割込み待ち状態を作る。なので初期スレッドはアイドル・スレッドとなる。

最後にMakefileに対して修正を行いプログラムの修正は終わり。

プログラムの実行

完成したプログラムをmakeして、xmodemで転送してrunする。(今回からブートローダに手を入れる必要がなくなったので、loadだけすれば良くなった)

実はずっとELFファイルを転送していたが、今回くらいから8KBを超え初めて転送が上手くいかなくなる。公式サイトの方に

■ ステップ9付近でXMODEMでの転送がうまくいかなくなった場合
XMODEMでのOS転送が,当初はうまくできていたのにステップ9あたりでうまく いかなくなる,という現象に遭遇するかたがたまにいます.

この場合,転送ファイルに「kozos」でなく「kozos.elf」を指定していないかどうか, 確認してみてください.

これらのファイルはどちらもELF形式のためKOZOSのブートローダーで解釈できますが, 「kozos.elf」はstripによるシンボル情報削除前のファイルのため,ファイルサイズが 大きめになっています.そしてKOZOSのブートローダーが扱えるファイルのサイズは (書籍中にもありますが)8KB程度までです.

で,OSの開発を書籍通りに進めていくとて9ステップあたりでkozos.elfが8KBを 越えてしまうため,転送ができなくなる,ということになるようです.

書籍中では「kozos.elf」でなく「kozos」を指定することになっていますが 「kozos.elf」でも最初のうちはサイズが小さくうまく転送できるため, 気付かずにそのまま「kozos.elf」を利用し続けてしまう場合があるようです.

ただし「kozos」にはシンボル情報等は残っていないため,以下のように使い分ける ことになります.

「kozos」…XMODEMでマイコンボードに転送するファイルはこちら
「kozos.elf」…readelfで解析したり(将来的に)デバッガの解析に利用したり するのはこちら

とあるので、kozosファイルの方を転送すれば良い。

転送して実行する。

../../tools/bin/kz_xmodem kozos /dev/tty.usbserial-FT0BTH3I
# =================================================
#  XMODEM for KOZOS H8/3069F (Version 0.0.2)
#  Copyright(C) 2012 Shinichiro Nakamura
# =================================================
# Flushing serial port.
# Wait.
# Setup load condition.
# Wait a NAK.
# ..........
# Transmit the target ELF file.
# File(kozos): 37 blocks + 100 bytes
# ......................................
# Wait a message from the target.
# Complete.
sudo cu -l /dev/tty.usbserial-XXXXXXX -s 9600
# Connected.
# 
# unknown.
# kzload> run
# starting from entry point: ffc020
# kozos boot succeed!
# test09_1 started.
# test09_1 sleep in.
# test09_2 started.
# test09_2 sleep in.
# test09_3 started.
# test09_3 wakeup in (test09_1).
# test09_1 sleep out.
# test09_1 chpri in.
# test09_3 wakeup out.
# test09_3 wakeup in (test09_2).
# test09_2 sleep out.
# test09_2 chpri in.
# test09_1 chpri out.
# test09_1 wait in.
# test09_3 wakeup out.
# test09_3 wait in.
# test09_2 chpri out.
# test09_2 wait in.
# test09_1 wait out.
# test09_1 trap in.
# test09_1 DOWN.
# test09_1 EXIT.
# test09_3 chpri out.
# test09_3 exit in.
# test09_3 EXIT.
# test09_2 wait out.
# test09_2 exit in.
# test09_2 EXIT.

解説

  1. kz_start()によって優先度0でstart_threads()が起動する

    // main.c
    kz_start(start_threads, "idle", 0, 0x100, 0, NULL);
  2. test09_1, test09_2, test09_3のスレッドが起動する

    // main.c
    test09_1_id = kz_run(test09_1_main, "test09_1", 1, 0x100, 0, NULL);
    test09_2_id = kz_run(test09_2_main, "test09_2", 2, 0x100, 0, NULL);
    test09_3_id = kz_run(test09_3_main, "test09_3", 3, 0x100, 0, NULL);
  3. この時点で優先度が0〜3までのスレッドが起動しているが、すぐに動作を始めるわけではなくkz_chpri()で優先度を15(最低)に変更する。

    // main.c
    kz_chpri(15);
  4. 最も優先度の高い優先度1のtest09_1が起動する

    // test09_1.c
    puts("test09_1 started.\n");
    puts("test09_1 sleep in.\n");
  5. kz_sleep()によりスリープ状態になる

    // test09_1.c
    kz_sleep();
  6. 次にレディー状態で次に優先度の高い(優先度2)のスレッドであるtest09_2が動作する

    // test09_2.c
    puts("test09_2 started.\n");
    puts("test09_2 sleep in.\n");
  7. kz_sleep()によりスリープ状態になる

    // test09_2.c
    kz_sleep();
  8. 次にレディー状態で次に優先度の高い(優先度3)のスレッドであるtest09_3が動作する

    // test09_3.c
    puts("test09_3 started.\n");
    puts("test09_3 wakeup in (test09_1).\n");
  9. kz_wakeupによりtest09_1がレディー状態に戻る

    // test09_3.c
    kz_wakeup(test09_1_id);
  10. test09_1のスレッドが再開する

    // test09_1.c
    puts("test09_1 sleep out.\n");
    puts("test09_1 chpri in.\n");
  11. 優先度を1 => 3に変更する。これによりtest09_1test09_3は同じ優先度となり、この二つはラウンドロビン方式でスケジューリングされる。

    // test09_1.c
    kz_chpri(3);
  12. 優先度3のスレッドのレディー状態でFirstInされているtest09_3が再開する(test09_1は終端に接続されているため)

    // test09_3.c
    puts("test09_3 wakeup out.\n");
    puts("test09_3 wakeup in (test09_2).\n");
  13. test09_1に対して行なった処理と同様にtest09_2もwakeupして優先度を変更する

    // test09_3.c
    kz_wakeup(test09_2_id);
    // test09_2.c
    puts("test09_2 sleep out.\n");
    puts("test09_2 chpri in.\n");
    kz_chpri(3);
  14. 上記wakeupの時点で優先度3のキューではtest09_1, test09_3の順番になっているのでkz_chpri(3)の後はtest09_1の処理が再開する

    // test09_1.c
    puts("test09_1 chpri out.\n");
    puts("test09_1 wait in.\n");
    kz_wait();
  15. test09_1 => test09_3 => test09_2でキューイングされているので次はtest09_3

    // test09_3.c
    puts("test09_3 wakeup out.\n");
    puts("test09_3 wait in.\n");
    kz_wait();
  16. 次はtest09_2

    // test09_2.c
    puts("test09_2 chpri out.\n");
    puts("test09_2 wait in.\n");
    kz_wait();
  17. 一周してtest09_1が再開。トラップ命令を実行して割込みを発生させる。

    // test09_1.c
    puts("test09_1 wait out.\n");
    puts("test09_1 trap in.\n");
    asm volatile ("trapa #1");

    割込みベクタのうち、トラップ命令要因の場合は割込み番号が8〜11でベクタ・アドレスが0x0000200x00002cである。
    今回は#1のオペランドを渡しているので9番の割込みが発生する(#08番の割込みはシステム・コールの実装で利用している)

    vectors[]の定義を見ると割込みベクタ9番にはintr_softerrが設定されておりSOFTVEC_TYPE_SOFTERRの割込みが発生することになる。このハンドラはsofterr_intr()が設定されているのでDOWNというメッセージを出力し、thread_exit()によってスレッドを終了する挙動となる。そのためDOWN, EXITの出力を行い、それ以降のメッセージが表示されていないのが確認できる。

    # test09_1 trap in.
    # test09_1 DOWN.
    # test09_1 EXIT.
    // ...以後 test09_1に関するメッセージなし

    このようにして、不正アドレス・アクセスがあった場合には、割込みハンドラにintr_softerrを設定しスレッドの終了処理を行えば、何かしらバグ等があった場合にもシステム全体を落とすのではなく、特定のサービスのみ停止するような動作をさせることができるようになる。

  18. 次に動くのはtest09_3kz_exit()を呼んでスレッドを終了している。

    // test09_3.c
    puts("test09_3 chpri out.\n");
    puts("test09_3 exit in.\n");
    kz_exit();
  19. 最後にtest09_2が再開する。

    // test09_2.c
    puts("test09_2 wait out.\n");
    puts("test09_2 exit in.\n");
    return 0;

    スレッドの動作開始時にはthread_run()により、thread_init()が最初に起動する関数として設定される。具体的にはスレッドがディスパッチされた時にプログラム・カウンタにthread_init()のアドレスが格納されるようになっている。なので、スレッドの動作開始時にはスレッドのメイン関数ではなくtread_init()である。

    // kozos.c
    static void thread_init(kz_thread *thp)
    {
      thp->init.func(thp->init.argc, thp->init.argv);
      thread_end();
    }

    なので、最終的にはthread_end()が呼ばれてスレッドが終了するという流れになる。

    # test09_2 wait out.
    # test09_2 exit in.
    # test09_2 EXIT.

スレッドに優先度をつけて実行することができるようになった。処理するものがない時にはスリープ状態となり割込み待ちとする実装も追加した。ちなみにこの「優先度をベースとしたスケジューリング」は組込みOS特有とかなんとか。。。汎用OSでは「一定時間毎のスケジューリング」になるらしい。んー深いな。

汎用OSではユーザーの手によって様々なアプリがインストールされるため、それらが システム全体が致命的におかしな状態になることを避ける が至上命題。なので今回のような優先度の高いものが無限ループで先に進まなくなったりすると大問題。そのため各アプリ(スレッド)が満遍なく動作するように設計されているためだそうだ。

組込みOSの場合は、ユーザーがアプリを入れるのではなく製品の一部として開発されているため、こちらの手のひらの上でアプリを組むことができ、全体のバランスを見ながら開発元が調整することができる。逆にハードウェア制御などが多く処理の内容に優先度を持たせたい場合が多くあるためこのような設計思想になっている。

とのことだった。

動きの流れは追いやすいが、処理の細かいところとかがなかなか難しい。。。あとやっぱデバッグが辛い。動かないとそのstepで書いてた全部を読み直さないと思ったように出力してくれないのがつらみ。