見出し画像

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でも試せる上に、実装が簡単です。ではまた次回。

この記事が気に入ったらサポートをしてみませんか?