4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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!マクロができました。

でもまだ足りない!今回はスレッドセーフ版を実装して、パニック時の表示もかっこよくします。

目次

  1. spin Mutexで安全にする
  2. lazy_staticでグローバル初期化
  3. カラフルな出力
  4. パニック画面を作る
  5. シリアル出力も整備

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の世界をもっと深掘りして、カーソル制御やスクロールの改良をします!

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?