30日OS自作入門15日目(Win10)
はじめに
FreeBSDのインストールに格闘している毎日です。
目次
マルチタスク
タスクスイッチに挑戦(harib12a)
序盤のマルチタスクの説明を読んでいて思ったのが、タスクスイッチのスピードは速ければ良い、ということではないということです。0.001秒で切り替えていると、CPUの処理能力が10%も遅くなってしまうので、タスクが増えるごとに目に見えて遅くなっていくということみたいです。
タスクスイッチを行うとき、CPUと連携して切り替えを行っているみたいです(タスクもCPU君が頑張っているとは思わなかった)。
セグメントが関連していて、GDTの一部みたいですね。
そして、この際にfarモードの使用例としては、asmhead.nas
での
JMP DWORD 2*8:0x0000_001b ; 可視化のためアンダーバーを追可している。
が該当するみたいです。この際、この命令を行った際に指定したセグメントがTSSだった場合にCPUはタスクスイッチと判断し、TSSで指定されたタスクにチェンジするといった仕組みみたいです。
まずは、タスクスイッチを行う際に、レジスタをバックアップする構造体は以下のような感じみたいです。
struct TSS32 { /* 32bit ver task status segment */ int backlink esp0, ss0, esp1, ss1, esp2, ss2, cr3; /* タスク用変数 */ int eip, eflags, eax, ecx, edx, esp, ebp, esi, edi: /* 32bitレジスタ */ int es, cs, ss, ds, fs, gs; /* 16bitレジスタ */ int ldtr, iomap; /* LDTR=0, iomap=0x4000_0000 */ };
タスクを管理するためのメタデータとレジスタの情報をバックアップ
後は、Harimainに構造体にレジスタをバックアップする処理を書きます。
また、TRレジスタの処理は、C言語ではサポートできないので、アセンブリの関数としてnaskfunc.asmに追加します。
TRレジスタにアクセスする際に使用する命令は、LTR命令みたいです。
後は、farジャンプを行えば、タスクスイッチが成功するという仕組みみたいです。
この際に注意しなきゃいけないのが、タスクから戻ってきたときにアクセスするのはfarジャンプを行ったJMP命令の次の命令の後の命令になるようです。この際に、C言語の処理に戻ってあげなければなりませんから、RET命令が必要というわけである。
で、設定した構造体を初期化しなければならないので
/* タスクスイッチ */ tss_a.ldtr = 0; /* 規定 */ tss_a.iomap = 0x40000000; /* 規定 */ tss_b.ldtr = 0; /* 規定 */ tss_b.iomap = 0x40000000; /* 規定 */ set_segmdesc(gdt + 3, 103, (int) &tss_a, AR_TSS32); /* タスクAをGDTに登録 */ set_segmdesc(gdt + 4, 103, (int) &tss_b, AR_TSS32); /* タスクBをGDTに登録 */ load_tr(3 * 8); /* TRレジスタへの代入 */ task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024; /* タスクBのためのスタック領域の確保(ESPはスタック領域の最終番地なことに注意) */ tss_b.eip = (int) &task_b_main; /* HLTを行うだけの番地 */ tss_b.eflags = 0x00000202; /* IF = 1(STI後のフラグ) */ tss_b.eax = 0; tss_b.ecx = 0; tss_b.edx = 0; tss_b.ebx = 0; tss_b.esp = task_b_esp; /* スタック領域のセット */ tss_b.ebp = 0; tss_b.esi = 0; tss_b.edi = 0; tss_b.es = 1 * 8; /* GDTの1番目(以下bootpack.cのセグメントを指定) */ tss_b.cs = 2 * 8; /* GDTの2番目 */ tss_b.ss = 1 * 8; /* GDTの1番目 */ tss_b.ds = 1 * 8; /* GDTの1番目 */ tss_b.fs = 1 * 8; /* GDTの1番目 */ tss_b.gs = 1 * 8; /* GDTの1番目 */
こんな感じになっている、注意すべき点はスタック領域をタスクBで設定する扱いとどこのセグメントに置いているかだと思う。
ちなみに、(int)でキャスト演算子による型の再定義なのを忘れていた…恥ずかしい。
実行すると以下のようになった
30日自作OS本(harib12a)
— 猫(1010) (@Wagahaiha_toto) 2020年1月25日
タスクBによるHLT攻撃 pic.twitter.com/oFvFZzjcL5
もっとタスクスイッチ(harib12b)
タスクスイッチにおいて、タスクBに一方通行であったのをタスクAへ戻ってくるように修正するようである。
戻るためには、タスクBの関数を修正し、戻ってくるようにすれば良いらしい。
void task_b_main(void) { struct FIFO32 fifo; struct TIMER *timer; int im fifobuf[128]; fifo32_init(&fifo, 128, fifobuf); timer = timer_alloc(); timer_init(timer, &fifo, 1); timer_settime(timer, 500); for (;;) { io_cli(); if (fifo32_status(&fifo) == 0) { io_sti(); io_hlt(); } else { i = fifo32_get(&fifo); io_sti(); if (i == 1) { /* 5秒タイムアウト */ taskswitch3(); /* タスクAに戻る */ } } } }
これがタスクBのコードである。バッファを経由し、タイマのデータを受け取っている。それを使い、5秒後にタスクAに戻る。
次に、アセンブラの処理を書くようである。単純に、GDT3番目であるタスクAにタスクスイッチする処理である。
_taskswitch3: ; void taskswitch3(void); JMP 3*8:0 ; GDT3番タスクに移動 RET
実行すると
30日自作OS本(harib12b)
— 猫(1010) (@Wagahaiha_toto) 2020年1月25日
タスクBから5秒復帰 pic.twitter.com/PM398NAzSj
簡単なマルチタスクをやってみる(1)(harib12c)
実際に、高速で切り替えることによるマルチタスクの実現にトライするようです。
その前に、タスクスイッチ用のアセンブラを毎回書くのは、効率が悪いのでfarジャンプを関数化し、C言語から指定できるようにするようです。
_farjmp: ; void farjmp(int eip, int cs); JMP FAR [ESP+4] ; eip, cs RET
これで、関数farjmpを用いてタスクスイッチができます。
後は、farjmpでタスクスイッチするようにするのと、0.02秒ごとにタスクスイッチするようにすればタスクスイッチが可能になる。
void task_b_main(void) { struct FIFO32 fifo; struct TIMER *timer_ts; int i, fifobuf[128]; fifo32_init(&fifo, 128, fifobuf); timer_ts = timer_alloc(); timer_init(timer_ts, &fifo, 1); timer_settime(timer_ts, 2); /* 0.02秒でのタスクスイッチ */ for (;;) { io_cli(); if (fifo32_status(&fifo) == 0) { io_sti(); io_hlt(); } else { i = fifo32_get(&fifo); io_sti(); if (i == 1) { /* タスクスイッチ */ farjmp(0, 3 * 8); /* タスクAを指定 */ timer_settime(timer_ts, 2); } } } }
これがタスクBの処理になる。
これを実行すると
30日自作OS本(harib12c)
— 猫(1010) (@Wagahaiha_toto) 2020年1月25日
タスクAとタスクBによる0.02秒刻みの高速キャッチボール pic.twitter.com/MKelTXQ5tB
簡単なマルチタスクをやってみる(2)(harib12d)
マルチタスクによる画面表示を行い、マルチタスクが起こっていることを可視化するようです。
かと言って画面表示だけだと、違いがわかりずらいので動きのある表示として数を数えることを行うみたいです。
そこで問題なのが、sht_backをどうやってタスクBに伝えるかになります。この問題に対する解決策として行われているのは、メモリを経由して参照してもらうということです。
参照するために、メモリ上の指定アドレスに値を格納
*((int *) 0x0fec) = (int) sht_back;
指定アドレスの値を参照して、同名の変数に格納
sht_back = (struct SHEET *) *((int *) 0x0fec);
実行すると
30日自作OS本(harib12d)
— 猫(1010) (@Wagahaiha_toto) 2020年1月26日
ほい!ほい!ほい!ほい!(タスクキャッチボール) pic.twitter.com/SqmReil7da
スピードアップ(harib12e)
やっぱり、半分ずつ資源を分け合っているので、性能が落ちているわけです。でも、これが半分に分けた資源を有効活用しているか?というと…そうではないらしく、毎回1回ずつ描画が変わることがネックになっているようです。それを改善するために、こちらの視覚にコンピューターの速度を合わせず、自由気ままにやらせるようです。
その時に、メモリ上を経由して渡すのではかっこ悪いと、スタックを通じてデータを渡すようで
使用するのは、タスクB用に確保したtask_b_espを利用するみたいです。要は、関数が引数を受け取るときにスタックを利用することを応用するということになります。これによりC言語コンパイラが翻訳したタスクBのコードが、引数が渡されていると勘違いし受け取るという仕組みです。
30日自作OS本(harib12e)
— 猫(1010) (@Wagahaiha_toto) 2020年1月27日
コンピューターに合わせたら爆速だった件について pic.twitter.com/Nu0ZJgM01F
速いわぁ…
スピード測定(harib12f)
めちゃめちゃ速いので測定してい見みようって内容みたいです。
以前やった測定方法をタスクBに仕込もむみたいで、タスクBのコードが以下のようになります。
void task_b_main(struct SHEET *sht_back) { struct FIFO32 fifo; struct TIMER *timer_ts, *timer_put, *timer_1s; int i, fifobuf[128], count = 0, count0 = 0; char s[12]; fifo32_init(&fifo, 128, fifobuf); timer_ts = timer_alloc(); timer_init(timer_ts, &fifo, 2); timer_settime(timer_ts, 2); /* 0.02秒でのタスクスイッチ */ timer_put = timer_alloc(); timer_init(timer_put, &fifo, 1); timer_settime(timer_put, 1); timer_1s = timer_alloc(); /* 計測用 */ timer_init(timer_1s, &fifo, 100); timer_settime(timer_1s, 100); for (;;) { count++; io_cli(); if (fifo32_status(&fifo) == 0) { io_sti(); } else { i = fifo32_get(&fifo); io_sti(); if (i == 1) { /* タスクスイッチ */ sprintf(s, "%11d", count); putfonts8_asc_sht(sht_back, 0, 144, COL8_FFFFFF, COL8_008484, s, 11); timer_settime(timer_put, 1); } else if (i == 2) { farjmp(0, 3 * 8); timer_settime(timer_ts, 2); } else if (i == 100) { sprintf(s,"%11d", count - count0); putfonts8_asc_sht(sht_back, 0, 128, COL8_FFFFFF, COL8_008484, s, 11); count0 = count; timer_settime(timer_1s, 100); } } } }
実行すると
30日自作OS本(harib12f)
— 猫(1010) (@Wagahaiha_toto) 2020年1月27日
下の表示ありの測定結果 pic.twitter.com/vioY3C08he
下の表示あり
30日自作OS本(harib12f)
— 猫(1010) (@Wagahaiha_toto) 2020年1月27日
下の表示なし pic.twitter.com/IP1vlt4h8C
下の表示あり
もっとマルチタスク(harib12g)
いままでのマルチタスクはタスク自身がCPUの使用権を解放し、相手にCPUの使用権を渡していました。しかし、現在の一般的なマルチタスクというのは、タスクがOSによって勝手に切り替えられるものです。このような切り替え方に修正するようです。
ちなみに、はりぼてOSのマルチタスクが該当しているかはわかりませんが、アプリケーション側からCPUの使用権を開放するのがノンプリエンプティブマルチタスク、OSがマルチタスクを制御するのをプリエンプティブマルチタスクというそうです。
タスクスイッチ用のプログラムであるmtask.cを作成し、そこで2つのマルチタスクをフラグを使って切り替えるようにさせます。その後、タイマの方で(inthandler20)タスクスイッチをするように設定します。その際にIFフラグがタスクスイッチによるレジスタのリセットで、1にならないように割り込み処理が終わった後にmt_taskswitch()は行うようです。
タスクスイッチ用のプログラムは
/* マルチタスク関係 */ #include "bootpack.h" struct TIMER *mt_timer; int mt_tr; /* TRレジスタの代用 */ void mt_init(void) /* mt_timerとmt_trの初期化 */ { mt_timer = timer_alloc(); /* timer_initは必要ないのでやらない */ timer_settime(mt_timer, 2); mt_tr = 3 * 8; /* 最初のセグメントをHarimainに指定 */ return; } void mt_taskswitch(void) { if (mt_tr == 3 * 8) { /* 次のタスクのセグメント指定 */ mt_tr = 4 * 8; } else { mt_tr = 3 * 8; } timer_settime(mt_timer, 2); /* 0.02秒後にセットしなおし */ farjmp(0, mt_tr); /* 指定したセグメントにタスクスイッチ */ return; }
こんな感じで、フラグを利用してタスクの種類を判別判別しているようです。
後は、タイマの方にタスクスイッチの処理を書きます。
/* IDT20の処理設定 */ void inthandler20(int *esp) { struct TIMER *timer; char ts = 0; /* タスクスイッチ用フラグ */ io_out8(PIC0_OCW2, 0x60); /* IRQ-00受付完了をPICに通知 */ timerctl.count++; /* 指定に従いカウントする */ if (timerctl.next > timerctl.count) { return; /* まだ次の時刻になっていないので、もうおしまい */ } timer = timerctl.t0; /* とりあえず先頭の番地をtimerに代入 */ for (;;) { /* timers のタイマは全て動作中のものなので、flagsを確認しない */ if (timer->timeout > timerctl.count) { break; } /* タイムアウト */ timer->flags = TIMER_FLAGS_ALLOC; if (timer != mt_timer) { fifo32_put(timer->fifo, timer->data); } else { ts = 1; /* mt_timer(マルチタスク用のタイマ)がタイムアウトした */ } timer = timer->next; /* 次のタイマの番地をtimerに代入 */ } /* 新しいずらし */ timerctl.t0 = timer; timerctl.next = timer->timeout; if (ts != 0){ mt_taskswitch(); /* タスクスイッチを行う */ } return; }
割り込み処理に影響を与えないようにタスクスイッチを最後にしているのがコツみたいです。
これを実行すると速度が上がっています。
30日自作OS本(harib12g)
— 猫(1010) (@Wagahaiha_toto) 2020年1月27日
FIFOバッファを使用してない分速いですね。 pic.twitter.com/RDMDYhbb9W
この理由は、毎回タスクスイッチをする際に、タスクに時間の情報をFIFOバッファを通して渡していたのがタイマの方で直接渡せるようになったからみたいです。これを知った時は「確かに!」って興奮しましたね。本当すごい!
アセンブラ言語
EIP
和訳で、「拡張命令ポインタ」と呼ばれる。32bit版であり、16bit版はIPである。
EIPの仕事は、次に実行する命令がどこのアドレスにあるかを記録しておくレジスタになる。
JMP命令いうのは、EIPレジスタに対するMOV命令のような存在であり、MOV命令だとEIPをいじれない代わりに入れ替える命令っぽい?
JMP命令(追加)
JMP命令には、普段使用するEIPを切り替えるだけのnearモードとEIPとCSを動かすfarモードが存在している。
farモードの使用例としては、asmhead.nas
での
JMP DWORD 2*8:0x0000_001b ; 可視化のためアンダーバーを追可している。
が該当するみたいです。この際、:(セミコロン)が判断するのに該当し2*8=16がCSへ代入する値であり、0x0000_001bがEIPに代入する命令になります。 そして、この命令を行った際に指定したセグメントがTSSだった場合にCPUはタスクスイッチと判断し、TSSで指定されたタスクにチェンジするといった仕組みみたいです。
最後に
マルチタスクってOS側のソフトウェア的な処理のみで行っていたと思っていたのですが、CPUもゴリゴリ頑張っていたのですね!これが一番印象的でした。