OS自作〜タイマー実装〜
前回のあらすじ
前回はQEMU用のビデオドライバの基礎を作りました。
そして、今回はビデオドライバの続き…のつもりだったのですが、ちょっと記事お順番を変えます。まずは順番を変えた理由から説明しようと思います。
今回分のコミットはこれ。
なぜ急にタイマー?
最初に言っておくと、実はすでに解像度の変更には成功しています。しかし」、ビデオドライバの仕事は解像度の変更するだけでなく、フレームバッファーをスペシャルファイルとして提供する必要があります。しかし、現状ファイルシステムがないので、スペシャルファイルどころか通常のファイル操作もできません。
というわけでファイルシステムを作ろう!と思って、QEMUのディスクデバイスを調べたところ、IDE Controllerが必要だと判明。そこでこのページをみて実装を始めたんですが…またも足りない機能を発見。実はまだHorseOSにはsleep関数がないので、処理待ちをすることができません。というわけでタイマーを作っていきます。
タイマーの種類
タイマーの種類については、OSdevのこのページが詳しいです。この中で今回実装を行ったのは、Local APIC、Power Management Timer、HPETです。ちなみに、最初はPITとかも調べたんですけど、結論から言うとコイツラは古いPCでしか使われていなくて、先程の3個の実装をしておけば十分だと思います。
Local APIC Timer
これは各CPUに一つずつあって、動作クロックはCPU依存です。したがって、これ単体では時間の単位がわかりません。ただし、CPU内蔵タイマーなので非常にアクセスが早いのが利点です。
Power Management Timer
mikan本では基本全てのPCに使われている固定クロックのタイマーということで、これを使ってLocal APICの動作クロックを調整しています。ちなみに最高の分解能は多分1μsぐらい。
HPET(High Precision Event Timer)
こいつが一番精度が高い。メモリーマップ方式なので、アクセスも早い方で、最高の分解能は10ns。ベンチマーク取るときもnsぐらいまでは出ることがあるから、これが使えればいいなーと思ってmikan本にはありませんが頑張って実装しました。
今回作るものの説明
基本はmikan本通りにすすめて、おまけでHPETがあったらPM Timerの上位互換として使う機能をつけます。
まず、Local APICをメインで使う上で、同時に複数のタイマーを走らせることができないという問題点があるので、これを解決するためにTimer Manager構造体を作って、論理タイマーを管理するようにします。それぞれの論理タイマーはLocal APICが周期(Periodic)モードで定期的に発生するTickを使ってカウントします。
そして、Local APICを初期化するときにFFTimer(Frequency Fixed TImerの略)構造体を作って、PM TimerかHPETを先に初期化して、Local APICの動作クロックの調整を行います。これでmikan本の内容は終わりです。
さきのFFTimer構造体はPM Timerを使うだけなら必要ないのですが、HPETとPM Timerを抽象化するために使っていて、HPETを探してなかったらPM Timerにフォールバックするようになっています。
文字で説明しても埒が明かないのでコードを示します。
Timer Manager
これがTimer Manager構造体の定義です。
pub struct TimerManager {
tick: u64,
timers: BinaryHeap<Timer>,
fft: FFTimer
}
tickが論理タイマー全体で共有されているLocal APICのカウントで、timersに全ての論理タイマーが保持されています。FFTimerはこの構造体のコンストラクタに渡されてきます。
ちなみにC++実装ではtimersはpriority_queueが使われていますが、RustではBinaryHeapが一番これに近いので使っています。Timerの方で工夫があるので後ほど説明します。
メソッドについては載せると長くなるので概要だけ説明します。
impl TimerManager {
pub fn new(fft: FFTimer) -> Self {}
pub fn add_timer(&mut self, timeout: u64, value: i32) {}
pub fn tick(&mut self) {}
pub fn wait_seconds(&self, sec: u64) {}
}
new:コンストラクタ
add_timer:timeoutをタイムアウトまでの時間、valueを識別番号として新たな論理タイマーを追加します
tick:Local APICの割り込みが発生するたびにtickを増やして、論理タイマーがタイムアウトしてないかを監視します
wait_secondsは指定された秒数だけFFTimerを使って待機します。
Timer(論理タイマー)
これがTimer構造体の定義です。
#[derive(Eq)]
struct Timer {
absolute_timeout: u128,
pub timeout: u64,
pub value: i32
}
timeoutとvalueはadd_timerで渡されていますが、absolute_timeoutに工夫があります。先程述べたとおり論理タイマーはTimer Managerのtickを使ってカウントしているのですが、tickがオーバーフローを起こして0に戻った時を考慮するにはtimeoutの倍の長さの時間を管理する必要があります。そのために作ったのがu128を使ったabsolute_timeoutです。
そして、この定義の下にはBinearyHeapで扱うためにOrdトレイトの実装が続きます。
FFTimer・PM Timer
PM Timerはmikan本通りなので構造体の定義だけ。
#[repr(packed, C)]
#[derive(Copy, Clone)]
pub struct PMTimer {
header: DescriptionHeader,
reserved1: [u8; 76-size_of::<DescriptionHeader>()],
pm_tmr_blk: u32,
reserved2: [u8; 112-80],
flags: u32,
reserved3: [u8; 276-116]
}
唯一注意が必要なことは、これに限らずいくつかの構造体はRustで生ポインタから読み出そうとすると「unaligned pointer」としてパニックになるので、予め構造体のデータ構造を「repr(packed, C)」として、読み出すときもread_unalignedを使う必要があります。
#[derive(Copy, Clone)]
pub enum FFTimer{
HPET(HpetController),
PM(PMTimer)
}
こちらもただのラッパーなので説明はなし。内部では切り替え処理を行ってるだけ。
HPET
ここまでですでに3000字あって書ききれないことを悟ったので、これも概要だけ説明して、詳しい話は別のサイトに丸投げします。まず、HPETはメモリーマップなので、割り込みの発生がLocalACPIに直接起こってくれません。これまでマウスなどで使ってきた割り込みベクターは各CPU内のもので、各CPUの割り込みはLocalAPICが管理しています。これに対し、CPU外部からの割り込みはI/O APICが管理していて、ここに来た割り込みはRedirection Tableを参照して各CPUに割り込みベクターと共に送られます。
ここで説明するのには限界があるので、僕がHPETを理解するのに使ったリンクを貼るので参照してください。
完成図
動いた写真だけ載せておきます。
次にやること
だいぶ雑になりましたが、なんとか全体像を伝えられたんじゃないかなと思います(写真が少ないせいもあって我ながら分かりにくい)。
次にやることなんですが、もともとはファイルシステムをタイマー使って実装する予定でしたが、冷静に考えてハードディスクに自作OSから書き込む機会は少ないと思われるうえ、これまたかなり大変な作業となるので、Live Bootの時のようにRAM上にファイルシステムを実装してみようかなと思います。これならハードディスクを壊したくないPCでも試せる上に、実装が簡単です。ではまた次回。
この記事が気に入ったらサポートをしてみませんか?