本稿について
先日Githubに公開したラズベリーパイ用Haribote OSについてオリジナルのX86版からの変更箇所解説2回目です。齢のせいか、たったの一月で何をやったのかどんどん忘れて来ているので、なるべく急いで進めて行こうと思います。今回はタスクスイッチです。
ラズベリーパイ用Haribote OS (RPiHaribote)はこちらに公開してあります
github.com
関連記事はこちら
uzusayuu.hatenadiary.jp
uzusayuu.hatenadiary.jp
uzusayuu.hatenadiary.jp
注意事項:あくまで筆者にとってOS開発は趣味の範疇の上に勉強中の身のため、内容については不正確な点もあると思います。そのような点が見つかった場合の指摘、または、さらによい方法の提案などしていただける場合は、喜んで参考にさせていただきます。なお、本記事はあくまで上記プログラムの解説であり、ARMの解説記事ではありません。また、記事の内容の正確性や実機での動作の再現性についてはいずれも保証できませんし、本記事の内容を使って起こるいかなる損害についても責任をもちません。
オリジナルのHariboteのタスクスイッチ
元々の「30日でできる!OS自作入門」では、タスクスイッチにはTSS(タスク状態セグメント)という仕組みを使っています。
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; int ldtr, iomap; };
これはタスクの情報とレジスタの内容を保存した構造体で、これをセグメントとして設定しておき、そこにジャンプすることで対象のタスクにスイッチできます。タスクの情報、レジスターの内容は自動でこの構造体から読み込まれ、セグメントもスタックも切り替えが行われ、CPUのステートも自動で変わります。(便利ですね。)
ラズベリーパイで使われているARMv6でもこういった仕組みがあるのかなと思って探してみたのですが見つかりませんでした。ということは、レジスターの値の待避と読み込みCPUのステートの変更はOSが行う必要があります。OS自作入門の中にセグメント切り替えに関して、
ちなみに、タスク切り替えは最初からTSSを使ってやりましたが、あれもTSSを使わないでやる方法があります。その場合は、これくらいに大変になります。
という記述がありますが、ラズベリーパイでは「これくらい」(セグメント切り替えをソフトウェアでやるくらい)大変になるわけです。(他にもっと楽な方法があるようなら教えていただければ感謝します)
RPiHariboteのタスクスイッチ
PCB (Process Control Block)
ARM社の「ARMコンパイラツールチェーン」という文書を見ると、「6.17 コンテキストスイッチ」という項があり、ここでPCB(Process Control Block)という物を使ったコンテキストスイッチが紹介されています。ページにでかでかとSupersededと書かれていますが、他に良い方法が見つからなかったのでより良い方法が見つかるまではこちらを参考にして進めます。
まず、以下のような構造体を作りました。名前はTSS32を流用していますが、元のHaribote用のTSS32とはまったく違う物です。
struct TSS32 { uint32_t cpsr, pc, r0, r1; uint32_t r2, r3, r4, r5; uint32_t r6, r7, r8, r9; uint32_t r10, r11, r12, sp; uint32_t lr, sp_svc, sp_sys, sp_usr; uint32_t run_flag, vmem_table; };
意味は以下の通りです
- cpsr: プロセスのCPSR(Current Processor Status Register: プロセッサーの状態の納められた特殊レジスタ)の値を保存するのに使う
- pc: 対応するプロセスの中断したプログラムのアドレス+4*1
- r0 - r12、sp, lr : レジスタの保存用です。(ここまでPCB。これ以降はプロセス管理に関わる情報)
- sp_svc: スーパーバイザーモード用スタックポインタ。タスク切り替え時には使わない
- sp_sys: システムモード用スタックポインタ。タスク切り替え時には使わない。*2
- sp_usr: ユーザーモード用スタックポインタ。タスク切り替え時には使わない。*3
- run_flag: アプリが動作しているかどうかのフラグ*4
- vmem_table: 仮想テーブルのエントリー
上記でわかるように、RPiHariboteでは各プロセス毎に、システムモード用(task_aとconsoleが使用)と、スーパーバイザー用(スーパーバイザーコール時のみに使用)、ユーザーモード用(アプリケーションが使用)の3つのスタックが割り当てられています。ラズベリーパイのベアメタルプログラミングではスーパーバイザーモードを使う事が多いようですが、その場合タスク切り替え時の条件分けが複雑になるため、RPiHariboteではtask_aとconsoleはシステムモードで動作させています。
タスク切り替え
「ARMコンパイラツールチェーン」ではレジスタr12に次のPCBのポインタを代入しておくことで切り替え先のプロセスを指定しますが、RPiHariboteでは、r0を使ってPCBを指定しますタイマー割り込み時にタスクスイッチが発生すると、割り込みハンドラーから帰ってくるときに戻りに値に次のPCBの値が代入されているようにしてあります。(戻り値が0の場合はタスクスイッチせず割り込み元のプロセスに帰る)ARMのABI(Application Binary Interface: C言語などの関数との引数と戻り値に関する規約)では戻り値はr0に納められることになっているので、結果的にPCBのアドレスがr0に入ることになります。
実際にタスクを切り替えるアセンブラのルーチンは以下のようになります。なお、この部分は割り込み処理の一環としてIRQモードで処理されます。また、この処理の前にタスク切り替え以外の割り込み処理(キーボード・マウスのチェック、タイマー処理、など)があり、sp以外の汎用レジスタはIRQ突入時にスタックにプッシュしてあります。
_TaskSwitchIRQ: str r0, _next_pcb ldmia sp!, {r0-r12, lr} // restore all registers at IRQ entry from sp_irq str sp, _sp_save // Save SP_irq ldr sp, _cur_pcb // load current pcb adr, sp = pcb->cpsr add sp, #8 // move to PCB.r0 position stmia sp, {r0-lr}^ // store all usr registers (sp_usr, lr_usr) mrs r0, spsr // stmdb sp, {r0, lr} // spsr and return address ldr r0, _next_pcb // r0 = &pcb_b str r0, _cur_pcb // switch virtual memory table ldr r0, [r0, #0x54] // load vmem table address bl switch_vmem // switch virtual memory table // retrieve next registers ldr r0, _next_pcb // r0 = &pcb_b add sp, r0, #8 // move sp to PCB.r0 position ldmdb sp, {r0, lr} msr spsr, r0 ldmia sp, {r0-lr}^ ldr sp, _sp_save // retrieve sp_irq nop subs pc, lr, #4 _sp_save: .word 0
一つ一つ見ていきます。
まず、r0に入っている次のPCBのアドレスを_next_pcbというラベルの位置に保存します。
str r0, _next_pcb
次に、割り込み発生時に保存したSP以外のレジスターをIRQ用のスタックからポップして、割り込み突入時のレジスター状態を復元します。
ldmia sp!, {r0-r12, lr} // restore all registers at IRQ entry from sp_irq
IRQ用SPを一旦_sp_saveというラベルの位置に保存します。
str sp, _sp_save // Save SP_irq
現在のPCBへのポインターを_cur_pcbというラベルの位置から読み出し、+8します。これでspはTSS32.r0のアドレスを示すことになります
ldr sp, _cur_pcb // load current pcb adr, sp = pcb->cpsr add sp, #8 // move to PCB.r0 position
割り込み元の汎用レジスターの値を_cur_pcbが示すPCB領域に書き込みます。ここで^は、ユーザーモード又はシステムモードのspとlrを書き出すことを示します。(sp, lrはIRQモードで独立していますが、システムモードとユーザーモード間では共有されます。)
stmia sp, {r0-lr}^ // store all usr registers (sp_usr, lr_usr)
次にユーザーモードのCPSRの値が保存されている特殊レジスタであるSPSRをr0にコピー
mrs r0, spsr //
割り込み元のプログラムアドレス+4が保存されているlrと、spsrが保存されているr0をPCBに書き込み
stmdb sp, {r0, lr} // spsr and return address
これでタスクスイッチ前のレジスタが待避できました。次はスイッチ先のPCBの読み込みです。
次に、先ほど保存した次のタスクに対応するPCBへのポインターを呼び出します。
ldr r0, _next_pcb // r0 = &pcb_b
現在のPCBを保存しておく_cur_pcbというラベルが示すアドレスに同じ値を保存しておきます(次回のタスクスイッチの時に参照するため)
str r0, _cur_pcb
ここで仮想メモリーのテーブル(TTBR)を変更します。まず、TSS32.vmem_tableをr0に読み込み、仮想メモリーテーブル変換ルーチンを呼び出します。(割り込みルーチンなどが読み込まれるアドレスはどの仮想メモリー空間でも変わらないように設定してあるので、以下の処理に影響はありません。)仮想メモリーについては以前のエントリーを参照ください。
ldr r0, [r0, #0x54] // load vmem table address bl switch_vmem // switch virtual memory table
再びspに次のPCBのアドレス+8を読み込ませ、TSS32.r0のアドレスを参照させます。
ldr r0, _next_pcb // r0 = &pcb_b add sp, r0, #8 // move sp to PCB.r0 position
PCBから次のpc+4、cpsrを、spsrとlrに読み込ませます。割り込みから戻る際にこれらの値はcpsrとpcに書き戻されます。
ldmdb sp, {r0, lr} msr spsr, r0
汎用レジスターを、ユーザーモードのレジスターに読み込みます。ユーザーモードのspとlrはIRQからもどる際に復帰されます
ldmia sp, {r0-lr}^
最初に保存しておいたIRQ用のspを読み込みます。
ldr sp, _sp_save // retrieve sp_irq
おまじない(nop)の後、IRQからユーザーモード(又はシステムモード)への復帰手順を使い、次のプロセスに移ります。
nop subs pc, lr, #4
ユーザーモードに戻る際に、先ほど読み込んだspsrはcpsrにコピーされ、spとlrはユーザーモード用の物に切り替わります。
これで、指定されたPCBの示すプロセスに復帰することができました。
まとめ
RPiHariboteのタスクスイッチについて解説しました。オリジナルHariboteではTSSとCPUの機能を使ってタスクスイッチしているところを、RPiHariboteではPCBとソフトウェアによって状態の待避と復帰を行っています。
短いアセンブラルーチンですが、安定して動作するようになるまでずいぶん試行錯誤を繰り返しました。現状、PCBのアドレスの保存やSPの一時保存にtextセグメント内のアドレスを使っているのがあまり好みで無いので、そのうちdataセグメントに移しておこうと思います。