Moiz's journal

プログラミングやFPGAなどの技術系の趣味に関するブログです

Raspberry Pi用Haribote OSの変更箇所2 - タスクスイッチ

本稿について

先日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の示すプロセスに復帰することができました。

タイマー割り込み以外でのタスクスイッチ

プロセスがスリープに入る場合にタイマーを待たずタスクスイッチがおきることがあります。この場合、_task_switch_asmというアセンブラのルーチンを使います。
_task_switch_asmでは、モードをIRQに切り替えて上記のタスクスイッチルーチンにジャンプしています。単純な処理なので解説は省略します。

まとめ

RPiHariboteのタスクスイッチについて解説しました。オリジナルHariboteではTSSとCPUの機能を使ってタスクスイッチしているところを、RPiHariboteではPCBとソフトウェアによって状態の待避と復帰を行っています。
短いアセンブラルーチンですが、安定して動作するようになるまでずいぶん試行錯誤を繰り返しました。現状、PCBのアドレスの保存やSPの一時保存にtextセグメント内のアドレスを使っているのがあまり好みで無いので、そのうちdataセグメントに移しておこうと思います。

*1:+4されているのは、はARMが割り込みに入るときにPC+4がLRに保存されているのでこれにあわせた

*2:アプリケーション実行中にシステムモード用スタックポインタの値を一時保存するのに使用

*3:スーパーバイザーコール中に一時的にシステムモードに移る際に、アプリケーション用のスタックポインタの値を保存しておくのに使用

*4:Hariboteではtss.ss0で判断していたが、RpiHariboteでは専用フラグをおいている