雑記帳
ここはhideyosiの雑記帳です。テケトーに書き散らしてるだけなので間違っていたりとは普通にしてます。信用度は相当低いことをあらかじめご了承を。またご覧のようにWikiを使ってますが、hideyosi意外は書き込めません。
OS自作入門はとりあえず一通り終わったんだけど、どうもオイラはタスクの理解が甘いようだ。ちょっとこれだけにスポットを当ててもう一回復習してみよう。
Table of contents
15日目
- CPUは「切り替えろ!」と命令がくると、レジスタのほぼ全てをメモリに書き出す。(退避させるというほうがいいかも)
- そして今度は、切り替わるべき別のタスク用のレジスタ内容を一気に読み込んでしまう。
- 最期に、eip(次は何番地のメモリの命令を読み込んで実行するか?)の値をその新しいタスク用のものにパチンと切り替える。
- これで次の命令からは、あたかもずーっとその仕事をしていたかのように動作を始める。
- これを高速に交互に行うのがマルチタスク
どこに退避させる??
- 退避先のメモリはどこでもいいわけではない。あるフォーマットと属性で作られた、専用のセグメントを使わないといけない。この特殊なセグメントをtssという。
- tssはCPU内部のレジスタのほぼ全てを退避できる量(逆に言えばそれ以上の余計な量は必要ない)を確保する。通常104バイト。内訳は
struct TSS32 { int backlink,esp0,ss0,esp1,ss1,esp2,ss2,cr3; /*タスクの状態を表すもの int eip,eflags,eax,ecx,edx,ebx,esp,ebp,esi,edi; /*通常のレジスタ int es,cs,ss,ds,fs,gs; /* 〃 (16ビットのもの) int ldtr,iomap; /*タスクの状態を表すもの }
- tssは切り替わるタスクの退避場所なので、素直に考えると、タスクが10個平行して動く場合はtssも10個用意するべきもの。
tssの作成&登録
- 先に書いたように、tssはセグメントの一種。なので、作成や登録はセグメント管理部分であるGDTに書き込みを行うことで実現する。
- tssの作成と登録の実際
struct TSS32 tss_a,tss_b; /*構造体として親切する tss_a.ldtr = 0; /*ldtrとiomapはとりあえずこの数値にする。 tss_a.iomap = 0x40000000; tss_b.ldtr = 0; tss_b.iomap = 0x40000000; /*gdtへの登録 set_segmdesc(gdt + 3,103,(int) &tss_a,AR_TSS32); /*それぞれ、GDT3番と4番に set_segmdesc(gdt + 4,103,(int) &tss_a,AR_TSS32); /*登録した。
疑問点:一つのタスクにはそれぞれtssとメモリ空間が必要。で、tssはセグメント。メモリ空間も当然セグメント。ということは、理屈ではタスクは4,095個以上は絶対に作れないということでOKなのか?
タスクの準備
- タスクという概念で考えると、現在動いている部分とてタスクとなる。(つまり、一つ目のタスクということになる)
- これまではマルチタスクではなかったので意識していなかったが、一番最初にやらなくてはならないことは、「俺も実はタスク! で、オレが動いているセグメントは2番!」ということのほかに、「俺が動いているタスクの退避用tssはセグメントの3番だよ!」という登録をしてやらないといけない。
- そこで、load_tr(3 * 8)関数を実行する。これにより、「俺が動いているタスクの退避用tssはセグメントの3番だよ!」という設定になる。(言い方を変えれば、このタスクが別のタスクに切り替わる時には、セグメント3番のtssに退避される)
- タスクを複数用意しなければ当然切り替えなどできない。
- タスクは仕事を続けるものでないといけない。(一つ仕事をしてすぐ終わってしまうのでは単なる関数)
- 通常はなんらかのループ構造にするもの。たとえば
void task_b_main(void){ for(;;){io_hlt();} }
- よし! これでOKだ! と思うがそうではない。task_bにはまだ準備がいる。
- まず、そもそもtask_bはまだ一度も動いていないのだ。最初の切り替わりで始めて動き出す。しかも、その最初の動作時も「切り替わり」なのだから、tss4からドバっとレジスタの値を読み込んで動き始める。tss4にはまだいい加減な数字しか入っていない。(これでは切り替わった瞬間に暴走の危険がある)
- そのため、とりあえず各レジスタの初期値を設定しておく。また、タスク、そもそもこのタスクに切り替わった場合、どこを実行するのかやスタックなどの設定も調整しておかなくてはならない。
P296の初期設定 tss_b.eip = (int) &task_b_main; /* 関数task_b_mainの先頭アドレス /* これはtask_b専用のスタックの設定。mamman_allocと使い64kbを確保。 /* 後ろの計算式はスタックの最終番地を計算したもの int task_b_esp; task_b_esp = memman_alloc_4k(memman,64*1024)+64*1024; tss_b.esp = tssk_b_esp; tss_b.eflags = 0x00000202; /*??? どういう意味だ? tss_b.eax = 0; /* 通常の各レジスタの初期値。 tss_b.ecx = 0; tss_b.edx = 0; tss_b.ebx = 0; tss_b.ebp = 0; tss_b.esi = 0; tss_b.edi = 0; tss_b.cs = 2 * 8; /* task_bはGDT2番のセグメントでプログラムを実行する。 /* 今回は単に面倒なので。もちろん別に用意してもいい。 tss_b.es = 1 * 8; /* 同じ理由でGDTの1番を使用する。 tss_b.ss = 1 * 8; tss_b.ds = 1 * 8; tss_b.fs = 1 * 8; tss_b.gs = 1 * 8;
準備完了! タスクスイッチ!
- ここまでできれば、あとはtaskswich4関数(中身は単にアセンブラでfarJumpしているだけ。切り替えたいタスクのtss(task_bは4番)にfarJumpする。
P298のタスク戻し
- これは説明するまでもない。解る。
疑問点:タスクはかならずしもメモリを別セグメントにして動かすとは限らないことが上記の簡易切り替えで解った。そうなるとメモリ用に1個として、理屈ではタスクは8,190個以上は絶対に作れないということでOKなのか?
自動的にタスクを行き来
void HariMain(void) { (中略) /*タイマー timer_tsを新設。*/ struct TIMER *timer, *timer2, *timer3, *timer_ts; (中略) /*新設したタイマーを初期化*/ timer_ts = timer_alloc(); timer_init(timer_ts, &fifo, 2); /* タイムアウトしたら「2」をFIFOに送るよう設定 timer_settime(timer_ts, 2); /* タイムアウト時間を0.02秒に設定 (中略) /* 上記は設定。ここ以下のforループがtask_aの本質部分*/ for (;;) { (中略) /* FIFOに2が来たら(timer_tsがタイムアウトしたら */ if (i == 2) { farjmp(0, 4 * 8); /*タスクをtask_bに切り替える timer_settime(timer_ts, 2); /*ここに復帰した時、再度タイマーをセットする } else if (256 <= i && i <= 511) { /* キーボードデータ */ (中略) } void task_b_main(void) { struct FIFO32 fifo; /*これら構造体は、タイマーを使う struct TIMER *timer_ts; /*ために必要なもの。 int i, fifobuf[128]; fifo32_init(&fifo, 128, fifobuf); /*FIFOを初期化して準備する。 timer_ts = timer_alloc(); /*そうしないとタイマーが使えないから timer_init(timer_ts, &fifo, 1); /*タイムアウトすると1を返してくるタイマー。 timer_settime(timer_ts, 2); for (;;) { (中略) i = fifo32_get(&fifo); /*FIFOから一つ数値を得る。 io_sti(); if (i == 1) { /* タスクスイッチ */ /*もしその通知が1ならタスクスイッチ! farjmp(0, 3 * 8); timer_settime(timer_ts, 2); } } } }
- ・・・だめだ・・・P304がどうしても理解できない・・・・orz
- 「とにかくこの方法でsht_backの値をtask_b_mainに渡せる」と理解しておこう。(これ以上は無理)
- ただ、一つ解ったことがある。
- 通常、別のタスク(task_bとか)には、それ用のtssと同時にそれ用のスタックが必要。(そらそーだ。分けないとごっちゃごちゃになっちゃう)
- 別のタスクになにも変数を渡す必要がない場合。(たとえば void task_b_main() なんて場合)
- task_b_esp = memman_alloc_4k(memman,64*1024)+64*1024;
- こんなことして、task_b用のスタックを確保している。おそらく、大半のタスクでもこれは必要な行為だろう。(スタックを一切使用しない動作だけのタスクとかそういう特殊なのは別ね)
- それに対して、今回はようするに、task_bに対して値を渡したいというだけ。
- で、task_bは引数を持つ関数になっている。(void task_b_main(struct SHEET *sht_back); )
- 普通に関数を呼ぶだけならなんの問題もない。そのまま呼べばよい。
- しかし、今回の相手は「別に動くタスク」だ。なので、呼び出しで引数を渡せない
- しかし、今回どうしても理解できなかった部分の登場となる。いまだにわからない。
- ・・・しかし、このわからない部分は、
- task_b_esp = memman_alloc_4k(memman,64*1024)+64*1024 -8;
- である。普通に用意するスタックから 8 を引いているだけだ。
- これがヒントになるかもしれない。いずれ解るかも。
自動タスクスイッチ
- 上記のソースを見ての通り、これまではお互いのタスクが相手のタスクに切り替わるという動作をしていたが、こういうプログラム部をなくしてもフルオートで切り替えが出来ないものだろうか?
- タスクスイッチ専用の特殊なタイマーを1個確保したと考えればよい。
疑問点解決?:タスクはかならずしもメモリを別セグメントにして動かすとは限らない(上記参照)。また、状況によってはtssを共有することすら可能。(タスクA,B,Cとあった場合、絶対tssが3個必要というわけではない。屁理屈チックだが、タスクAとタスクCが絶対同時に動かないような調整をすればtssは2個でも十分マルチタスクは可能)よってこの場合は、タスクではなくtssはどう頑張っても8,190個以上は作れないということでOKのようだ。(・・・なんかあったりまえな答えになってしまったな・・・(^^;))
Last-modified: 2024-01-06 (Sat) 22:39:13 (JST) (476d) by