読者です 読者をやめる 読者になる 読者になる

【組込OS自作入門】8thステップのメモ

OSとは何か(概念のお勉強)

OSの本質は、割込みをベースとした割込みドリブンなプログラム

スレッドとは何か

プログラムをタスクごとに独立して動作できるようにしたものがスレッド。
次に動作すべきスレッドの選択:スケジューリング
選択されたスレッドの処理再開:ディスパッチ

アプリとは何か

OSの上でタスクとして独立して動作するプログラムを一般にアプリケーションプログラムという。
(直訳すると応用プログラム。それに対してコンピュータシステムを動作させるために必要となる基本的プログラムは基本プログラムなどと呼ばれる)

スレッドの切り替えの契機

スレッドの切り替えは割込み処理で行う。
割込み処理は現在の処理内容を一時中断して別の処理を行うというものなので、現在の処理の中断と保存という機能を本質的に持っている。
割込み処理をそのまま応用すれば、レジスタの値の保存などによりスレッドのコンテキストの保存が行える。

システムコール

アプリはシステムコール命令を実行することで、自身の動作に割り込んでOSのサービスを呼び出すことができる。

タスクコントロールブロック(TCB)とは何か

TCBとは各タスクの情報を格納する領域。
kozosでは以下をもつ。

  • スレッドをレディキューに繋げるときのポインタ
  • スレッド名
  • スレッドの持つスタック領域のアドレス
  • スレッド起動時のスタートアップへのパラメータ
  • システムコール呼び出しの時のパラメータ
  • スレッドのコンテキスト情報の格納領域
レディキューとは何か

レディキューとは実行可能なスレッドのTCBをつないでいるキューのこと。
CPUは1つしかないので、一度に実行できるスレッドは1つ。
実行中のスレッドのことをカレントスレッドと呼ぶ。
カレントスレッドの実行に区切りができたら、カレントスレッドはレディキューの終端に接続し、レディキューの先頭のスレッドをカレントスレッドとする。
つまりKOZOSはラウンドロビン方式である。

OSの実装

スタックの分離と明確化

これまでは割込み発生時にも、現在利用しているスタックをそのまま利用していた。
スレッド化に伴い、スタックはスレッドごとに確保する。

必要なスタックは以下の3種

  • 起動処理で利用されるスタック(ブートスタック)
  • 割り込み処理で利用されるスタック(割込みスタック)
  • スレッドごとに確保されるスタック(ユーザスタック)

ブートローダ

intr.Sの修正

アセンブラ部分が特に混乱したのでメモ
割込みハンドラの入り口でスタックポインタを割り込みスタックに切り替えるように修正

#include "intr.h"
	.h8300h
	.section .text

	.global _intr_softerr
#	.type 	_intr_softerr,@function
_intr_softerr:
	mov.l	er6,@-er7 # spのアドレスの箇所にer6を退避
	mov.l	er5,@-er7 #
	mov.l	er4,@-er7 #
	mov.l	er3,@-er7 #
	mov.l	er2,@-er7 #
	mov.l	er1,@-er7 #
	mov.l	er0,@-er7 # spのアドレスの箇所にer0を退避
	mov.l	er7,er1   # er1にspを退避
	mov.l	#_intrstack,sp # spを割込みスタックのアドレスに置き換え
	mov.l	er1,@-er7 # もともとのspを割込みスタックに積む
	mov.w	#SOFTVEC_TYPE_SOFTERR,r0
	jsr	@_interrupt
	mov.l	@er7+,er1 # もとのspの値を割込みスタックから復旧する
	mov.l	@er1, er7 # spをもとのspとする
	mov.l	@er7+,er0
	mov.l	@er7+,er1
	mov.l	@er7+,er2
	mov.l	@er7+,er3
	mov.l	@er7+,er4
	mov.l	@er7+,er5
	mov.l	@er7+,er6
	rte

OS側

この章はあちらこちらに処理が転々とし,
似た名前の関数も多いので理解しづらい.

処理の切り替え

処理の切り替えは、

  1. 実行中の処理に関する情報を一旦退避
  2. 次に実行する処理に関する情報をセットし実行
  3. 1.にもどる

1.は割込みハンドラの実装を利用する。(上記intr.S参照)
2.dispatch()を呼び出すと、その呼び出し時の引数のspを使って、スタックに積んだコンテキスト情報がレジスタに渡され、rteを実行することでスタックにある関数のアドレス値がPCに設定され関数が実行される。

スケジューリング

タスク切り替えは割込みが発生した時に実行される。
割込みが発生すると、最終的にthread_intr()が呼びだされ、その関数の中でスケジューリングされる。

システムコール
  • kz_run()
    • パラメータ設定して,スレッドを生成する
    • 生成したスレッドを起動(kz_syscall()の実行により)
  • kz_exit()
    • スレッドを終了する
ライブラリ関数
  • kz_start()
    • 初期スレッドを生成する
  • kz_shutdown()
    • 致命的なエラーが発生したときに呼ぶと,メッセージを出力して停止
  • kz_syscall()
    • システムコールの呼び出しを行う共通関数
    • トラップ命令を発行し,予め割り込みベクタに登録しておいた8番の割り込み(intr_syscall)が発生する
    • intr_syscallには,kz_start()でのsetintr()の実行により,thread_intr()が設定されている.
    • thread_intr()内部のハンドラ呼び出しからsyscall_intr()が呼び出されシステムコールの処理が行われる.
    • syscall_intr()ではsyscall_proc()が呼ばれ,その中ではcall_functionsが呼ばれる.
    • call_functionsの中ではthread_runが呼ばれスレッドが起動する
ユーザスレッドのメイン関数
  • test08_1_main()
    • ユーザスレッドのメイン関数
起動処理の流れ

大枠としては,

  1. main関数からkz_start()呼び出し
  2. kz_start()の中で
    • データの初期化
    • 割り込みハンドラの設定
      • syscall_intr
      • softerr_intr
    • 初期スレッドを生成
    • 初期スレッドをdispatch()により起動
  3. dispatch()によりthread_init()が呼び出される
  4. thread_init()によりstart_threads()が呼び出される
  5. start_threads()によりkz_run()が呼び出される
    1. 受け取ったパラメータを構造体に設定
    2. kz_syscall()でシステムコール発行
  6. kz_syscall()
    1. 受け取ったシステムコールの種類(type)と設定した構造体を,current->syscall.xxxに退避
    2. asm volatile("trapa #0")でトラップ命令の割り込みを自発的に起こす
  7. トラップ命令が発生するとsetintr()で設定したthread_intr()が呼び出される
  8. thread_intr()
    1. spのコンテキストをcurrent->contextに保存
    2. handlers[type]()でsetintr()のときに登録したハンドラのうち,syscall_intr()を呼び出す
  9. syscall_intr()よりsyscall_proc()が呼び出される
  10. syscall_proc()
    1. getcurrent()によりかレッドスレッドをデキューする.このときキューは空となる
    2. call_functions()を呼び出す
  11. call_functions()
    1. KZ_SYSCALL_TYPE_RUNが今回のシステムコールの種類なので,thread_run()が実行される
    2. パラメータはkz_syscall()のときに退避させておいたcurrent->syscall_param
  12. thread_run()が実行されたら,処理がthread_intrまで戻る
  13. thread_intr()戻ると,schedule()が実行され,キューの先頭がカレントスレッドになる.
  14. dispatch()で割り込みで中断されていたstartスレッドが再開する

上記処理の手順を理解するためには,コードを眺める必要がある.
ところが本に載っているコードは細切れで,とても全体を追いきれない.