背景 / 目的
LinuxOSの仕組みや挙動について深い知見を得たいという興味/関心のもと、前回の1章~8章までの内容に基づいた投稿記事に続き「ゼロからのOS自作入門」9章~14章の内容/実施事項/得られた知見について整理しようと思い、記事を書きました。
大筋は前回と同様にまとめる想定ですが、今回からMikanOSに実装した機能がLinux Kernelではどのように実装されているのか?ということについて、少しだけ触れながらまとめていきます。
本来の目的であったLinuxOSへの理解を深めることを意識し、Mikan本に加えて、今回からは「Linuxカーネル2.6解読室」も参考にしながら実装を進めていきました。
2025/01 現在、Ubuntu24.04ではLinux Kernel 6.8が使われているので本書のLinux Kernel 2.6は実装面で少し古い部分もあるかと思います。
しかし、こちらの記事(参考[1])にも記載の通り、現在のLinux Kernelの動作や実装を理解する上で、本書の内容は役に立てられるものと自身も判断しました。
そのため一部カーネル解読室の内容にも触れながら実施事項をまとめていければと思います。
各章での実施事項 + 得られた知見について
前回と同様に、アウトプットとして以下2つの内容に基づいて記述してみます。
- 2章ごとに大まかな実装内容を要約する
- 個人的に得られた知見/関連事項/参考情報/試してみたこと等を簡単にまとめる
※最大限に気を付けますが、理解の異なる点などあれば是非コメント等でお知らせください!
第9章 ~ 第10章
■ 実装内容の要約
この2章の内容は、マウスとウインドウをそれぞれ別のレイヤーで描画し、マウスカーソル移動時にウインドウや背景が崩れないように改良したり、ドラッグ&ドロップを実装したり、描画処理を最適化したりとGUI機能を拡張していくことがメインとなっています。
途中、描画処理の最適化を行う上で処理効率の改善状況を把握するためのパフォーマンス計測機能を実装します。その際、Local APICタイマと呼ばれるもののカウンタ値を使って処理に要する時間を計測します。カウンタ値を利用する理由は、この時点の実装では現実世界の正確な時間を計測できないためです。また、ここで計測機能を実装する目的として、次の章以降で実装するタイマ割り込み機能に先立ち、Local APICタイマの機能を一部使える状況にしておく意図があるものと解釈しました。
■ 得られた知見や参考情報
- malloc () を使ったメモリ確保時のシステムコールに関して
本章の冒頭では8章までの実装を使い、サクッと動的にメモリ確保するよう機能追加します。これまで malloc() でヒープメモリを確保する際、sbrkシステムコールが呼ばれていることは知っていたものの、それがどのように機能しているのかあまり目を向けていませんでした。
しかし、ここでヒープメモリ領域の確保に関する実装を参照してみることで、
動的メモリ確保には以下のように2つの機能が必要であり、sbrkが後者の機能として呼び出されていることを改めて認識しました。
- 配置newの考え方を使った、メモリ領域の開始アドレス指定
- sbrkを使ったメモリ領域増分の確保とprogram breakの変更
- 配置newの考え方を使った、メモリ領域の開始アドレス指定
mallocとsbrkの動作を確認されたい方はこちら(参考[2])を参考にするとよいかと思います。
- Local APICタイマや関連するレジスタについて
Local APICは各CPUコア内に存在する割り込みコントローラであり、Local APICが各CPUに対して割り込み要求を通知する役割を果たします。x86_64の場合Local APICタイマを使った割り込み処理の設定は、LVT(Local Vector Table) Timerレジスタを使って制御します。レジスタの値とは言ってもメモリ空間にマッピングされているためC/C++プログラム内から読み書きが可能です。こちらの記事(参考[3])がわかりやすくまとまっているように感じました。
また自身で試したこととしてLinuxOS上からLocal APICに関する情報を取得してみました。
こちらの記事(参考[4])を参照し、下記の手順で、APICに関するデータを抽出してみました。
確かに、各CPUコアごとにLocal APICを保有していることや、Local APICがメモリアドレス 0xFEE0000 以下のメモリ空間にマッピングされていることが確認できました。
### Ubuntu 24.04.1 LTS + AMD Ryzen 7 3700X 8-Core Processor を利用
$ sudo cat /sys/firmware/acpi/tables/APIC > APIC.dat
$ iasl -d ACPI.dat
$ less APIC.dsl
### 出力結果は下記の通り(途中から結果中略)
[000h 0000 004h] Signature : "APIC" [Multiple APIC Description Table (MADT)]
[004h 0004 004h] Table Length : 0000015E
[008h 0008 001h] Revision : 03
[009h 0009 001h] Checksum : DF
[00Ah 0010 006h] Oem ID : "ALASKA"
[010h 0016 008h] Oem Table ID : "A M I "
[018h 0024 004h] Oem Revision : 01072009
[01Ch 0028 004h] Asl Compiler ID : "AMI "
[020h 0032 004h] Asl Compiler Revision : 00010013
[024h 0036 004h] Local Apic Address : FEE00000
[028h 0040 004h] Flags (decoded below) : 00000001
PC-AT Compatibility : 1
[02Ch 0044 001h] Subtable Type : 00 [Processor Local APIC]
[02Dh 0045 001h] Length : 08
[02Eh 0046 001h] Processor ID : 00
[02Fh 0047 001h] Local Apic ID : 00
[030h 0048 004h] Flags (decoded below) : 00000001
Processor Enabled : 1
Runtime Online Capable : 0
[034h 0052 001h] Subtable Type : 00 [Processor Local APIC]
[035h 0053 001h] Length : 08
[036h 0054 001h] Processor ID : 02
[037h 0055 001h] Local Apic ID : 02
[038h 0056 004h] Flags (decoded below) : 00000001
Processor Enabled : 1
Runtime Online Capable : 0
(以下、省略)
第11章 ~ 第12章
■ 実装内容の要約
この2章では大きく3点のことに着手します。1つ目は以前、第6章~第7章で実装したマウス操作の割り込み処理(MSI)を参考にLocal APICを利用したタイマ割り込みを実装します。これを実装する目的として今後OS上でマルチタスクを実現したい意図があります。例えば下記のような、
(タスクA) → タイマ割り込み → (タスクB) → タイマ割り込み → (タスクA) →・・・繰り返し
という具合にタスクを切り替える、いわゆるコンテキストスイッチを実装する下地作りです。
2つ目は、前の章で課題として残っていたカウンタ値の増分と現実世界での経過時間を紐付けられるようにACPI PMタイマを使ってLocal APICタイマの1カウントが何秒の経過時間に当たるのか計測できるようにします。3つ目はおまけ程度にキーボードからの入力機能を実装します。
■ 得られた知見や参考情報
- ACPI (Advanced Configuration and Power Interface)を使った現実時間の計測について
本書を読む以前はACPIと言われても何となく「電源管理周りの規格」ぐらいのふわっとした認識しか持っていませんでした。
しかし実装を読み進めたり調べたりしているうちに、OS起動時にBIOSがマシンの構成情報を取得し、取得した構成情報をACPI Tableという形でメモリ空間上に展開していることを理解しました。以下はACPI Tableの階層構造を示しています。(参考[5]より拝借しました)
第11章 ~ 第12章の中ではLocal APICタイマが1カウントに何秒かかるかを調べることで現実世界での経過時間を計測しようとしています。その際に必要な値「pm_tmr_blk」は上図の中の「FADT」内に存在しています。Mikan本の中では「RSDP → XSDT → FADT」の順でポインタを辿って「pm_tmr_blk」を参照しています。
もちろん、BIOSがOSを起動する時にはこの手順で値の取得が必要かと思います。ただ起動済みのLinuxOS上でどのような値が設定されているかを確認するだけであれば、先程と同様に下記の手順で確認可能です。
### Ubuntu 24.04.1 LTS + AMD Ryzen 7 3700X 8-Core Processor を利用
$ sudo cat /sys/firmware/acpi/tables/FACP > FACP.dat
$ iasl -d FACP.dat
$ cat FACP.dsl | grep "PM Timer Block Address"
### 得られた出力
[04Ch 0076 004h] PM Timer Block Address : 00000808
第13章 ~ 第14章
■ 実装内容の要約
個人的には、今回の記事の中で技術的に1番面白いパートがこの2章でした。
ここではコンテキストスイッチ機能を実装し、一定の時間間隔で実行中のタスクAの状態を保存して、タスクBへと切り替える…といったマルチタスク機能を使えるようにします。
また、タスクごとの優先度を設定できるようにすることで、マウス操作やキーボード操作などの割り込み処理を滑らかにしたり、処理すべきタスクが存在しない場合CPUを遊休状態に遷移する機能を実装します。タスクに対するsleepやwakeup操作を通じて、タスクの状態遷移を設定できるようにもなります。
■ 得られた知見や参考情報
- 実際のLinux Kernelで実装されているContext_switch関数との比較
Mikan本ではコンテキストスイッチをどのように実装しているかというと、タスクの実行バイナリやコマンドライン引数、環境変数、スタックメモリ、レジスタの値などをまとめたコンテキストを構造体として扱い、タスクが切り替わる瞬間に、その構造体の保存 & 次に実行するタスクが保持するコンテキスト(=構造体)の読み込みを行うことで実現されています。
実際にMikan本の中で、タスクのコンテキストを保存するための構造体は下記のようになっています(一部抜粋)。変数名から推察できる通り、主にレジスタの値を構造体に格納するようになっています。
// 構造体の定義
struct TaskContext {
uint64_t cr3, rip, rflags, reserved1; // offset 0x00
uint64_t cs, ss, fs, gs; // offset 0x20
uint64_t rax, rbx, rcx, rdx, rdi, rsi, rsp, rbp; // offset 0x40
uint64_t r8, r9, r10, r11, r12, r13, r14, r15; // offset 0x80
std::array<uint8_t, 512> fxsave_area; // offset 0xc0
} __attribute__((packed));
また、Mikan本の中ではSwitchTaskによる2つのタスクA, Bの切り替え処理は下記のように実装しています。
// タスクA, Bそれぞれの構造体
alignas(16) TaskContext task_b_ctx, task_a_ctx;
/***
SwitchContext関数について、
第一引数が次に実行するコンテキストの復帰処理、
第二引数が実行していたコンテキストの保存処理
をそれぞれ実行している
***/
void SwitchTask() {
TaskContext* old_current_task = current_task;
if (current_task == &task_a_ctx) {
current_task = &task_b_ctx;
} else {
current_task = &task_a_ctx;
}
SwitchContext(current_task, old_current_task);
}
次に実際にLinuxカーネル解読室に記載されているコンテキストスイッチを担う関数は下記のように記述されています。(参考[6])
/***
runqueue_t ... CPUごとの実行キュー。ここはMikan本では引数として与えていない
task_t型はtask_struct構造体と呼ばれるタスクのコンテキストを保存した構造体
第二引数が実行していたコンテキストの保存処理
第三引数が次に実行するコンテキストの復帰処理
***/
task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next)
{
(中略)
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
return prev;
}
上記2つの関数の概要をそれぞれ比較してみるとMikan本でも、実際のLinux Kernelでも、
・コンテキストを構造体として保存し、タスクごとの状態を管理していること
・コンテキストスイッチの際は構造体の保存処理と復帰処理をワンセットで行っていること
が確認でき今までイメージでしかなかった並行処理の解像度が少し高められたように感じます。
所感 / OS自作によって得られたメリット
正直、ボリューム満点だったため全ての知見をまとめきることが難しいなという感じでした。
以下、ここまでMikan本を読み進めてみて気が付いた点や感じたことを記載してみます。
■ 所感/実際の画面状況について
現在、14章までのMikanOSの動作画面を見てみると下図のようになっています。
かなりGUIの機能が拡張され、OSとしての機能しているように見受けられます。
マウス操作による割り込みやタスクのsleepとwakeupなどが機能しています。
ここから先はターミナルや、その上で動作するコマンド、そしてファイルシステムの実装など、今回、取り扱った内容よりはレイヤの高い実装が増えていくようで楽しみです。
現在は15章を進めているので、次回は第19章~第20章くらいまで進めた結果をまとめて記述しようと思います。
■ OS自作によって得られたメリット
実は最近、趣味でGBAを使ったプログラムを使用するタイミングがあったのですが、その際、ゲームカセットの起動までの手続きや、自前のROMを吸い出すためのプログラムの手続きなどがかなり鮮明に理解できました。
これらはMikan本で知識を取得したことによる部分が大きいと思います。楽しくなってしまい、つい、キュービックスタイル様(参考[7])から技術書やガジェットを購入してしまいました。
こんな人は稀かもしれませんが、もし、Mikan本からGBAの実装に興味を持たれら方などいらっしゃいましたら、こちらのGBA開発Wiki(参考[8])を見てみると面白いかもしれません。
参考にさせていただいた情報
- [1] 自作OSとかLinuxカーネルについて役立った本
- [2] mallocの動作を追いかける(main_arenaとsbrk編)
- [3] Local APICについて
- [4] ACPI Table
- [5] AMD Family 1Ah Models 00h–0Fh and Models 10h–1Fh ACPI v6.5 Porting Guide
- [6] https://github.com/hiboma/hiboma
- [7] CUBIC STYLE
- [8] GBA開発Wiki
以上
コメントを残す