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【完結】

はじめに

Part7でヒープが動いて、VecStringが使えるようになりました。

今回はマルチタスクを実装します。複数のタスクを切り替えながら実行する、OSの醍醐味ですね。

正直に言います。コンテキストスイッチで1週間溶けました。レジスタの退避・復元を間違えると即死するので、デバッグがものすごく大変...

目次

  1. マルチタスクとは
  2. タスクの構造
  3. コンテキストスイッチ
  4. スケジューラ
  5. async/awaitで楽をする
  6. 動作確認

マルチタスクとは

1つのCPUで複数のタスクを「同時に」動かす仕組み。実際には高速に切り替えてるだけ。

時間 →
CPU: [タスクA][タスクB][タスクA][タスクC][タスクB]...

人間には同時に動いてるように見える。これがタイムシェアリング

協調的 vs プリエンプティブ

方式 説明 メリット デメリット
協調的 タスクが自発的にCPUを譲る 実装が簡単 タスクが譲らないと止まる
プリエンプティブ タイマー割り込みで強制切り替え 公平 実装が複雑

今回は協調的マルチタスクをRustのasync/awaitで実装します。プリエンプティブは...またいつか。

async/awaitを使う理由

Rustのasync/await協調的マルチタスクの完璧な抽象化です。

async fn task_a() {
    loop {
        println!("A");
        yield_now().await;  // ここでCPUを譲る
    }
}

async fn task_b() {
    loop {
        println!("B");
        yield_now().await;
    }
}

コンパイラが状態機械に変換してくれるので、手動でコンテキストを保存する必要がない!

Executorを実装する

async関数を実行するにはExecutorが必要です。

// src/task/mod.rs
use alloc::boxed::Box;
use core::{
    future::Future,
    pin::Pin,
    task::{Context, Poll},
};

pub struct Task {
    future: Pin<Box<dyn Future<Output = ()>>>,
}

impl Task {
    pub fn new(future: impl Future<Output = ()> + 'static) -> Task {
        Task {
            future: Box::pin(future),
        }
    }

    fn poll(&mut self, context: &mut Context) -> Poll<()> {
        self.future.as_mut().poll(context)
    }
}

シンプルなExecutor

// src/task/simple_executor.rs
use super::Task;
use alloc::collections::VecDeque;
use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

pub struct SimpleExecutor {
    task_queue: VecDeque<Task>,
}

impl SimpleExecutor {
    pub fn new() -> SimpleExecutor {
        SimpleExecutor {
            task_queue: VecDeque::new(),
        }
    }

    pub fn spawn(&mut self, task: Task) {
        self.task_queue.push_back(task);
    }

    pub fn run(&mut self) {
        while let Some(mut task) = self.task_queue.pop_front() {
            let waker = dummy_waker();
            let mut context = Context::from_waker(&waker);
            match task.poll(&mut context) {
                Poll::Ready(()) => {} // タスク完了
                Poll::Pending => self.task_queue.push_back(task),
            }
        }
    }
}

ダミーWaker

Wakerは「タスクを起こす」ための仕組み。今回は何もしないダミー実装:

fn dummy_raw_waker() -> RawWaker {
    fn no_op(_: *const ()) {}
    fn clone(_: *const ()) -> RawWaker {
        dummy_raw_waker()
    }

    let vtable = &RawWakerVTable::new(clone, no_op, no_op, no_op);
    RawWaker::new(0 as *const (), vtable)
}

fn dummy_waker() -> Waker {
    unsafe { Waker::from_raw(dummy_raw_waker()) }
}

キーボード入力を非同期化

割り込みで受け取ったキー入力を、非同期で処理できるようにします。

// src/task/keyboard.rs
use conquer_once::spin::OnceCell;
use crossbeam_queue::ArrayQueue;
use core::{pin::Pin, task::{Context, Poll}};
use futures_util::{stream::Stream, task::AtomicWaker, StreamExt};
use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};

static SCANCODE_QUEUE: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();
static WAKER: AtomicWaker = AtomicWaker::new();

pub struct ScancodeStream {
    _private: (),
}

impl ScancodeStream {
    pub fn new() -> Self {
        SCANCODE_QUEUE.try_init_once(|| ArrayQueue::new(100))
            .expect("ScancodeStream::new should only be called once");
        ScancodeStream { _private: () }
    }
}

impl Stream for ScancodeStream {
    type Item = u8;

    fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<u8>> {
        let queue = SCANCODE_QUEUE.try_get().expect("not initialized");

        if let Some(scancode) = queue.pop() {
            return Poll::Ready(Some(scancode));
        }

        WAKER.register(&cx.waker());
        match queue.pop() {
            Some(scancode) => {
                WAKER.take();
                Poll::Ready(Some(scancode))
            }
            None => Poll::Pending,
        }
    }
}

割り込みハンドラを修正

// src/interrupts.rs
pub(crate) fn add_scancode(scancode: u8) {
    if let Some(queue) = SCANCODE_QUEUE.try_get() {
        if queue.push(scancode).is_err() {
            println!("WARNING: scancode queue full; dropping keyboard input");
        } else {
            WAKER.wake();  // タスクを起こす!
        }
    }
}

extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
    use x86_64::instructions::port::Port;
    let mut port = Port::new(0x60);
    let scancode: u8 = unsafe { port.read() };
    
    crate::task::keyboard::add_scancode(scancode);  // キューに追加
    
    unsafe {
        PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
    }
}

非同期キーボードタスク

pub async fn print_keypresses() {
    let mut scancodes = ScancodeStream::new();
    let mut keyboard = Keyboard::new(
        ScancodeSet1::new(),
        layouts::Us104Key,
        HandleControl::Ignore,
    );

    while let Some(scancode) = scancodes.next().await {
        if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
            if let Some(key) = keyboard.process_keyevent(key_event) {
                match key {
                    DecodedKey::Unicode(character) => print!("{}", character),
                    DecodedKey::RawKey(key) => print!("{:?}", key),
                }
            }
        }
    }
}

while let Some(scancode) = scancodes.next().awaitがキモ。キー入力があるまでawaitで待機します。

追加の依存クレート

# Cargo.toml
[dependencies]
conquer-once = { version = "0.4", default-features = false }
crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }

main.rsの更新

mod task;

fn kernel_main(boot_info: &'static BootInfo) -> ! {
    // ... 前回までの初期化 ...
    
    println!("Starting async executor...");
    
    let mut executor = task::simple_executor::SimpleExecutor::new();
    executor.spawn(Task::new(example_task()));
    executor.spawn(Task::new(task::keyboard::print_keypresses()));
    executor.run();
    
    println!("Tasks finished!");
    loop { x86_64::instructions::hlt(); }
}

async fn example_task() {
    let number = async_number().await;
    println!("async number: {}", number);
}

async fn async_number() -> u32 {
    42
}

動作確認

cargo bootimage
   Compiling conquer-once v0.4.0
   Compiling crossbeam-queue v0.3.12
   Compiling futures-util v0.3.31
   Compiling my-os v0.1.0 (C:\Users\Aqua\Documents\Qiita\my-os)
    Finished `dev` profile [optimized + debuginfo] target(s) in 12.43s
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
Starting async executor...
async number: 42

キーボードを押すと、非同期で文字が表示される!

改良版Executor

SimpleExecutorはビジーループで効率が悪い。改良版:

// src/task/executor.rs
use super::{Task, TaskId};
use alloc::{collections::BTreeMap, sync::Arc, task::Wake};
use core::task::{Context, Poll, Waker};
use crossbeam_queue::ArrayQueue;

pub struct Executor {
    tasks: BTreeMap<TaskId, Task>,
    task_queue: Arc<ArrayQueue<TaskId>>,
    waker_cache: BTreeMap<TaskId, Waker>,
}

impl Executor {
    pub fn new() -> Self {
        Executor {
            tasks: BTreeMap::new(),
            task_queue: Arc::new(ArrayQueue::new(100)),
            waker_cache: BTreeMap::new(),
        }
    }

    pub fn spawn(&mut self, task: Task) {
        let task_id = task.id;
        if self.tasks.insert(task.id, task).is_some() {
            panic!("task with same ID already in tasks");
        }
        self.task_queue.push(task_id).expect("queue full");
    }

    pub fn run(&mut self) -> ! {
        loop {
            self.run_ready_tasks();
            self.sleep_if_idle();
        }
    }

    fn sleep_if_idle(&self) {
        use x86_64::instructions::interrupts;

        interrupts::disable();
        if self.task_queue.is_empty() {
            interrupts::enable_and_hlt();
        } else {
            interrupts::enable();
        }
    }

    fn run_ready_tasks(&mut self) {
        while let Some(task_id) = self.task_queue.pop() {
            let task = match self.tasks.get_mut(&task_id) {
                Some(task) => task,
                None => continue,
            };
            let waker = self.waker_cache
                .entry(task_id)
                .or_insert_with(|| TaskWaker::new(task_id, self.task_queue.clone()));
            let mut context = Context::from_waker(waker);
            match task.poll(&mut context) {
                Poll::Ready(()) => {
                    self.tasks.remove(&task_id);
                    self.waker_cache.remove(&task_id);
                }
                Poll::Pending => {}
            }
        }
    }
}

sleep_if_idleで、やることがないときはhltで省電力モードに入ります。

コンテキストスイッチの闘い(おまけ)

最初は手動でコンテキストスイッチを実装しようとしました...

#[repr(C)]
struct TaskContext {
    rax: u64, rbx: u64, rcx: u64, rdx: u64,
    rsi: u64, rdi: u64, rbp: u64, rsp: u64,
    r8: u64, r9: u64, r10: u64, r11: u64,
    r12: u64, r13: u64, r14: u64, r15: u64,
    rip: u64, rflags: u64,
}

これをアセンブリで退避・復元するコードを書いて...

1日目: レジスタの順番間違えた → 即死
2日目: rspを間違えた → スタック破壊
3日目: rflags忘れた → 謎の挙動
4日目: 割り込み中に切り替え → 二重障害
5日目: やっと動いた!
6日目: 別のタスクで壊れた
7日目: async/awaitに逃げた ← いまここ

async/await最高です。コンパイラが全部やってくれる。

まとめ

Part8では以下を達成しました:

  • 協調的マルチタスクの概念
  • Executorの実装
  • async/awaitの活用
  • 非同期キーボード入力
  • 省電力対応(hlt)

手動コンテキストスイッチで1週間溶かしたけど、最終的にasync/awaitに落ち着きました。Rustの非同期ランタイムを自作できたのは良い経験。

次回(Part9)はファイルシステムを実装して、データの永続化に挑戦します!

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

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?