Rustで作るx86_64 自作OS入門シリーズ
Part1【環境構築】 | Part2【no_std】 | Part3【Hello World】| Part4【VGA】 | Part5【割り込み】 | Part6【ページング】 | Part7【ヒープ】 | Part8【マルチタスク】| Part9【ファイルシステム】 | Part10【ELFローダー】 | Part11【シェル】 | Part12【完結】
はじめに
Part2でVGAテキストバッファの抽象化とprintln!マクロができました。
でもまだ足りない!今回はスレッドセーフ版を実装して、パニック時の表示もかっこよくします。
目次
spin Mutexで安全にする
問題点
Part2の実装ではstatic mutを使っていました。
static mut WRITER: Option<Writer> = None;
これはスレッドセーフではない。将来マルチタスクを実装したとき、複数のタスクが同時にWRITERにアクセスしたら壊れます。
spinクレート
std::sync::MutexはOS機能(スレッドのスリープ等)に依存するので使えません。代わりにスピンロックを使います。
# Cargo.toml
[dependencies]
spin = "0.9"
lazy_static = { version = "1.4", features = ["spin_no_std"] }
Updating crates.io index
Locking 5 packages to latest compatible versions
Adding lazy_static v1.5.0
Adding lock_api v0.4.14
Adding scopeguard v1.2.0
Adding spin v0.9.8
スピンロックとは
通常のMutexはロック取得に失敗するとスレッドをスリープさせますが、スピンロックはロックが取れるまでCPUをビジーウェイトさせます。
通常のMutex:
[タスクA] lock() → 取得OK
[タスクB] lock() → 失敗 → スリープ💤 → 起きて再試行 → OK
スピンロック:
[タスクA] lock() → 取得OK
[タスクB] lock() → 失敗 → ループで待機🔄🔄🔄 → OK
OSがないとスレッドをスリープさせられないので、この方式しか使えません。
lazy_staticでグローバル初期化
問題
Rustのstaticは定数式しか使えません。
// これはダメ
static WRITER: Mutex<Writer> = Mutex::new(Writer::new());
// エラー: `new()` is not const
解決策: lazy_static
lazy_staticを使うと、初回アクセス時に初期化されるstaticを作れます。
use spin::Mutex;
lazy_static::lazy_static! {
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
column_position: 0,
row_position: 0,
color_code: ColorCode::new(Color::White, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
使い方
pub fn _print(args: fmt::Arguments) {
WRITER.lock().write_fmt(args).unwrap();
}
.lock()でMutexGuardを取得し、スコープを抜けると自動で解放されます。
volatile書き込み
コンパイラの最適化でVGAバッファへの書き込みが消えることがあります。core::ptr::write_volatileで防ぎます。
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= VGA_WIDTH {
self.new_line();
}
let row = self.row_position;
let col = self.column_position;
// volatile writeで最適化を防ぐ
unsafe {
core::ptr::write_volatile(
&mut self.buffer.chars[row][col] as *mut ScreenChar,
ScreenChar {
ascii_character: byte,
color_code: self.color_code,
}
);
}
self.column_position += 1;
}
}
}
カラフルな出力
せっかく色が使えるので、色付き出力の関数も追加します。
impl Writer {
pub fn set_color(&mut self, foreground: Color, background: Color) {
self.color_code = ColorCode::new(foreground, background);
}
}
使い方:
fn kernel_main(_boot_info: &'static BootInfo) -> ! {
vga::init();
// 白文字で出力
println!("Hello, OS World!");
// 色を変えて出力
{
let mut writer = vga::WRITER.lock();
writer.set_color(vga::Color::LightGreen, vga::Color::Black);
}
println!("This is green!");
{
let mut writer = vga::WRITER.lock();
writer.set_color(vga::Color::Yellow, vga::Color::Blue);
}
println!("Yellow on blue!");
loop {}
}
パニック画面を作る
OSといえばブルースクリーン(BSOD)!...じゃなくて、パニック時にわかりやすい画面を出しましょう。
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
// シリアルにも出力
serial_print("\n!!! KERNEL PANIC !!!\n");
let mut writer = SerialWriter;
let _ = write!(writer, "{}\n", info);
// 画面を赤くする
{
let mut vga = vga::WRITER.lock();
vga.set_color(vga::Color::White, vga::Color::Red);
vga.clear_screen();
}
println!("========================================");
println!(" KERNEL PANIC!");
println!("========================================");
println!();
println!("{}", info);
println!();
println!("System halted.");
loop {
unsafe { core::arch::asm!("hlt"); }
}
}
わざとパニックを起こしてみる
fn kernel_main(_boot_info: &'static BootInfo) -> ! {
vga::init();
println!("Hello, OS World!");
// パニックを起こす
panic!("Test panic!");
}
赤い画面が出た!かっこいい!
シリアル出力も整備
デバッグ用にシリアル出力もマクロ化しておきます。
// serial.rs
use core::fmt;
use spin::Mutex;
const SERIAL_PORT: u16 = 0x3F8;
pub struct SerialPort {
port: u16,
}
impl SerialPort {
pub const fn new(port: u16) -> SerialPort {
SerialPort { port }
}
pub fn init(&self) {
unsafe {
// 割り込み無効化
outb(self.port + 1, 0x00);
// DLAB有効化
outb(self.port + 3, 0x80);
// ボーレート設定(115200)
outb(self.port + 0, 0x01);
outb(self.port + 1, 0x00);
// 8N1
outb(self.port + 3, 0x03);
// FIFO有効化
outb(self.port + 2, 0xC7);
// モデム制御
outb(self.port + 4, 0x0B);
}
}
pub fn write_byte(&self, byte: u8) {
unsafe {
// 送信バッファが空くまで待機
while inb(self.port + 5) & 0x20 == 0 {}
outb(self.port, byte);
}
}
}
impl fmt::Write for SerialPort {
fn write_str(&mut self, s: &str) -> fmt::Result {
for byte in s.bytes() {
self.write_byte(byte);
}
Ok(())
}
}
unsafe fn outb(port: u16, value: u8) {
core::arch::asm!("out dx, al", in("dx") port, in("al") value);
}
unsafe fn inb(port: u16) -> u8 {
let value: u8;
core::arch::asm!("in al, dx", in("dx") port, out("al") value);
value
}
lazy_static::lazy_static! {
pub static ref SERIAL1: Mutex<SerialPort> = {
let serial = SerialPort::new(SERIAL_PORT);
serial.init();
Mutex::new(serial)
};
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).unwrap();
}
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => ($crate::serial::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($($arg:tt)*) => ($crate::serial_print!("{}\n", format_args!($($arg)*)));
}
動作確認
ビルド
cargo bootimage
WARNING: `CARGO_MANIFEST_DIR` env variable not set
Building kernel
Finished `dev` profile [optimized + debuginfo] target(s) in 0.14s
Building bootloader
Compiling bootloader v0.9.33
Finished `release` profile [optimized + debuginfo] target(s) in 1.82s
Created bootimage for `my-os` at `target\x86_64-my_os\debug\bootimage-my-os.bin`
実行
qemu-system-x86_64 -drive "format=raw,file=target\x86_64-my_os\debug\bootimage-my-os.bin" -serial file:serial.log
シリアルログ確認
Get-Content serial.log
=== My OS Booting ===
VGA initialized
println! test complete
Entering infinite loop...
ファイル構成
my-os/
├── Cargo.toml
├── x86_64-my_os.json
├── .cargo/
│ └── config.toml
└── src/
├── main.rs
├── vga.rs
└── serial.rs (新規)
まとめ
Part3では以下を達成しました:
- spin Mutexでスレッドセーフ化
- lazy_staticでグローバル初期化
- volatile書き込みで最適化防止
- カラー出力対応
- パニック画面の実装
- シリアル出力のマクロ化
次回(Part4)では、80x25の世界をもっと深掘りして、カーソル制御やスクロールの改良をします!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!