ZigでOSを作るシリーズ
| Part1 基本 | Part2 ブート | Part3 割り込み | Part4 メモリ | Part5 プロセス | Part6 FS |
|---|---|---|---|---|---|
| ✅ Done | ✅ Done | ✅ Done | ✅ Done | 👈 Now | - |
はじめに
前回はメモリ管理を実装した。
今回はプロセス管理!タスク構造体、コンテキストスイッチ、スケジューラで、複数のタスクを同時実行できるようになる。
マルチタスクが動くと、いよいよOSっぽくなってくるよね。楽しいパートだよ!
プロセス管理の概要
┌─────────────────────────────────────────────────────────────────┐
│ プロセス管理の全体像 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Task 0 │ │ Task 1 │ │ Task 2 │ │ Task N │ │
│ │ (Idle) │ │ (Shell) │ │ (App) │ │ ... │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ └─────────────┴──────┬──────┴─────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Scheduler │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Ready Queue: [T1] → [T2] → [T3] → ... │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────┬─────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Context Switcher │ │
│ │ Save/Restore: Registers, Stack, Page Table │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
タスク構造体
// task.zig
const heap = @import("heap.zig");
const vmm = @import("vmm.zig");
pub const TaskState = enum {
ready,
running,
blocked,
sleeping,
terminated,
};
pub const KERNEL_STACK_SIZE: usize = 16 * 1024; // 16KB
// CPU状態を保存する構造体
pub const Context = extern struct {
// 汎用レジスタ(callee-saved)
r15: u64,
r14: u64,
r13: u64,
r12: u64,
rbp: u64,
rbx: u64,
// 命令ポインタ
rip: u64,
// スタックポインタ
rsp: u64,
// ページテーブル
cr3: u64,
};
pub const Task = struct {
id: u32,
name: [32]u8,
state: TaskState,
priority: u8,
// コンテキスト
context: Context,
// スタック
kernel_stack: u64,
kernel_stack_top: u64,
// スケジューリング情報
time_slice: u32,
remaining_slice: u32,
total_ticks: u64,
// リンク(スケジューラキュー用)
next: ?*Task,
// ページテーブル
page_table: u64,
// 終了コード
exit_code: i32,
pub fn init(id: u32, name: []const u8, entry: *const fn () void) ?*Task {
const task = heap.alloc(@sizeOf(Task)) orelse return null;
const task_ptr: *Task = @ptrCast(@alignCast(task));
// スタックを確保
const stack = heap.alloc(KERNEL_STACK_SIZE) orelse {
heap.free(task);
return null;
};
// 名前をコピー
var task_name: [32]u8 = [_]u8{0} ** 32;
for (name, 0..) |c, i| {
if (i >= 31) break;
task_name[i] = c;
}
const stack_top = @intFromPtr(stack) + KERNEL_STACK_SIZE;
task_ptr.* = .{
.id = id,
.name = task_name,
.state = .ready,
.priority = 1,
.context = .{
.r15 = 0,
.r14 = 0,
.r13 = 0,
.r12 = 0,
.rbp = stack_top,
.rbx = 0,
.rip = @intFromPtr(entry),
.rsp = stack_top,
.cr3 = getCR3(),
},
.kernel_stack = @intFromPtr(stack),
.kernel_stack_top = stack_top,
.time_slice = 10,
.remaining_slice = 10,
.total_ticks = 0,
.next = null,
.page_table = getCR3(),
.exit_code = 0,
};
return task_ptr;
}
pub fn deinit(self: *Task) void {
heap.free(@ptrFromInt(self.kernel_stack));
heap.free(@ptrCast(self));
}
};
fn getCR3() u64 {
return asm volatile ("mov %%cr3, %[cr3]"
: [cr3] "=r" (-> u64),
);
}
コンテキストスイッチ
ここはアセンブリで実装する必要がある部分なんだよね。
// context_switch.zig
const Task = @import("task.zig").Task;
const Context = @import("task.zig").Context;
// Zigからアセンブリを呼び出す
extern fn switch_context(old: *Context, new: *Context) void;
// アセンブリで実装されるコンテキストスイッチ
comptime {
asm (
\\.global switch_context
\\.type switch_context, @function
\\switch_context:
\\ // 現在のコンテキストを保存
\\ mov %r15, 0(%rdi)
\\ mov %r14, 8(%rdi)
\\ mov %r13, 16(%rdi)
\\ mov %r12, 24(%rdi)
\\ mov %rbp, 32(%rdi)
\\ mov %rbx, 40(%rdi)
\\
\\ // リターンアドレスを保存
\\ lea 1f(%rip), %rax
\\ mov %rax, 48(%rdi)
\\
\\ // スタックポインタを保存
\\ mov %rsp, 56(%rdi)
\\
\\ // CR3を保存
\\ mov %cr3, %rax
\\ mov %rax, 64(%rdi)
\\
\\ // 新しいコンテキストを復元
\\1:
\\ mov 0(%rsi), %r15
\\ mov 8(%rsi), %r14
\\ mov 16(%rsi), %r13
\\ mov 24(%rsi), %r12
\\ mov 32(%rsi), %rbp
\\ mov 40(%rsi), %rbx
\\
\\ // ページテーブルを切り替え(同じなら不要)
\\ mov 64(%rsi), %rax
\\ mov %cr3, %rcx
\\ cmp %rax, %rcx
\\ je 2f
\\ mov %rax, %cr3
\\2:
\\
\\ // スタックを切り替え
\\ mov 56(%rsi), %rsp
\\
\\ // 新しいタスクにジャンプ
\\ jmp *48(%rsi)
);
}
pub fn switchTo(current: *Task, next: *Task) void {
switch_context(¤t.context, &next.context);
}
スケジューラ
ラウンドロビン方式のスケジューラを実装。
// scheduler.zig
const Task = @import("task.zig").Task;
const TaskState = @import("task.zig").TaskState;
const context_switch = @import("context_switch.zig");
const vga = @import("vga.zig");
const pic = @import("pic.zig");
const MAX_TASKS: usize = 64;
var task_list: [MAX_TASKS]?*Task = [_]?*Task{null} ** MAX_TASKS;
var current_task: ?*Task = null;
var ready_queue_head: ?*Task = null;
var ready_queue_tail: ?*Task = null;
var next_task_id: u32 = 0;
var scheduler_enabled: bool = false;
pub fn init() void {
// アイドルタスクを作成
const idle = createTask("idle", idleTask) orelse unreachable;
idle.priority = 0; // 最低優先度
current_task = idle;
idle.state = .running;
scheduler_enabled = true;
}
fn idleTask() void {
while (true) {
asm volatile ("hlt");
}
}
pub fn createTask(name: []const u8, entry: *const fn () void) ?*Task {
const id = next_task_id;
next_task_id += 1;
const task = Task.init(id, name, entry) orelse return null;
// タスクリストに追加
for (&task_list) |*slot| {
if (slot.* == null) {
slot.* = task;
break;
}
}
// Readyキューに追加
addToReadyQueue(task);
return task;
}
fn addToReadyQueue(task: *Task) void {
task.state = .ready;
task.next = null;
if (ready_queue_tail) |tail| {
tail.next = task;
ready_queue_tail = task;
} else {
ready_queue_head = task;
ready_queue_tail = task;
}
}
fn removeFromReadyQueue() ?*Task {
if (ready_queue_head) |head| {
ready_queue_head = head.next;
if (ready_queue_head == null) {
ready_queue_tail = null;
}
head.next = null;
return head;
}
return null;
}
// タイマー割り込みから呼ばれる
pub fn tick() void {
if (!scheduler_enabled) return;
if (current_task) |task| {
task.total_ticks += 1;
task.remaining_slice -= 1;
if (task.remaining_slice == 0) {
schedule();
}
}
}
pub fn schedule() void {
if (!scheduler_enabled) return;
const current = current_task orelse return;
// 次のタスクを選択
const next = selectNextTask() orelse return;
if (current == next) return;
// 現在のタスクをキューに戻す
if (current.state == .running) {
current.remaining_slice = current.time_slice;
addToReadyQueue(current);
}
// 次のタスクを実行
next.state = .running;
current_task = next;
context_switch.switchTo(current, next);
}
fn selectNextTask() ?*Task {
// 優先度ベースの選択(簡略化版)
var best: ?*Task = null;
var best_priority: u8 = 0;
var prev: ?*Task = null;
var best_prev: ?*Task = null;
var current = ready_queue_head;
while (current) |task| {
if (task.priority > best_priority or best == null) {
best = task;
best_priority = task.priority;
best_prev = prev;
}
prev = task;
current = task.next;
}
if (best) |task| {
// キューから削除
if (best_prev) |p| {
p.next = task.next;
} else {
ready_queue_head = task.next;
}
if (ready_queue_tail == task) {
ready_queue_tail = best_prev;
}
task.next = null;
return task;
}
// 実行可能なタスクがない場合はアイドルタスク
return task_list[0];
}
pub fn yield() void {
schedule();
}
pub fn sleep(ms: u32) void {
if (current_task) |task| {
task.state = .sleeping;
// TODO: タイマーキューに追加
schedule();
}
}
pub fn exit(code: i32) void {
if (current_task) |task| {
task.state = .terminated;
task.exit_code = code;
schedule();
}
}
pub fn getCurrentTask() ?*Task {
return current_task;
}
pub fn getTaskCount() usize {
var count: usize = 0;
for (task_list) |slot| {
if (slot != null) count += 1;
}
return count;
}
pub fn printTaskList() void {
vga.print("\n=== Task List ===\n");
vga.print("ID Name State Ticks\n");
vga.print("---- ---------------- --------- --------\n");
for (task_list) |slot| {
if (slot) |task| {
printNumber(task.id, 4);
vga.print(" ");
printName(&task.name, 16);
vga.print(" ");
printState(task.state);
vga.print(" ");
printNumber(task.total_ticks, 8);
vga.print("\n");
}
}
}
fn printNumber(n: u64, width: usize) void {
var buf: [20]u8 = undefined;
var i: usize = buf.len;
var num = n;
if (num == 0) {
while (i > buf.len - width + 1) : (i -= 1) {
buf[i - 1] = ' ';
}
buf[i - 1] = '0';
i -= 1;
} else {
while (num > 0) {
i -= 1;
buf[i] = @truncate('0' + (num % 10));
num /= 10;
}
}
while (buf.len - i < width) {
i -= 1;
buf[i] = ' ';
}
for (buf[i..]) |c| {
vga.putChar(c);
}
}
fn printName(name: []const u8, width: usize) void {
var printed: usize = 0;
for (name) |c| {
if (c == 0) break;
vga.putChar(c);
printed += 1;
}
while (printed < width) : (printed += 1) {
vga.putChar(' ');
}
}
fn printState(state: TaskState) void {
const str = switch (state) {
.ready => "Ready ",
.running => "Running ",
.blocked => "Blocked ",
.sleeping => "Sleeping ",
.terminated => "Term ",
};
vga.print(str);
}
TSS(Task State Segment)
x86_64では、割り込み時のスタック切り替えにTSSが必要。
// tss.zig
const gdt = @import("gdt.zig");
pub const TSS = extern struct {
reserved0: u32 = 0,
rsp0: u64 = 0, // Ring 0 スタックポインタ
rsp1: u64 = 0,
rsp2: u64 = 0,
reserved1: u64 = 0,
ist1: u64 = 0, // Interrupt Stack Table
ist2: u64 = 0,
ist3: u64 = 0,
ist4: u64 = 0,
ist5: u64 = 0,
ist6: u64 = 0,
ist7: u64 = 0,
reserved2: u64 = 0,
reserved3: u16 = 0,
iopb_offset: u16 = @sizeOf(TSS),
};
var tss: TSS = .{};
var interrupt_stack: [4096]u8 align(16) = undefined;
pub fn init() void {
// 割り込みスタックを設定
tss.rsp0 = @intFromPtr(&interrupt_stack) + interrupt_stack.len;
tss.ist1 = tss.rsp0;
// GDTにTSSディスクリプタを追加
gdt.installTss(@intFromPtr(&tss), @sizeOf(TSS));
// TRにロード
loadTr();
}
pub fn setKernelStack(stack: u64) void {
tss.rsp0 = stack;
}
fn loadTr() void {
asm volatile ("ltr %[selector]"
:
: [selector] "r" (@as(u16, 0x28)), // TSSセレクタ
);
}
サンプルタスク
// tasks.zig
const scheduler = @import("scheduler.zig");
const vga = @import("vga.zig");
const pit = @import("pit.zig");
// カウンタータスク
pub fn counterTask() void {
var count: u32 = 0;
while (true) {
count += 1;
vga.setCursor(0, 20);
vga.print("Counter: ");
printNumber(count);
vga.print(" ");
// 少し待つ
for (0..100000) |_| {
asm volatile ("nop");
}
scheduler.yield();
}
}
// スピナータスク
pub fn spinnerTask() void {
const chars = "|/-\\";
var idx: usize = 0;
while (true) {
vga.setCursor(50, 20);
vga.print("Spinner: ");
vga.putChar(chars[idx % 4]);
idx += 1;
for (0..50000) |_| {
asm volatile ("nop");
}
scheduler.yield();
}
}
// 計算タスク
pub fn calcTask() void {
var result: u64 = 0;
while (true) {
// フィボナッチ計算
var a: u64 = 0;
var b: u64 = 1;
for (0..40) |_| {
const tmp = a + b;
a = b;
b = tmp;
}
result = b;
vga.setCursor(0, 21);
vga.print("Fib(40) = ");
printNumber(result);
vga.print(" ");
scheduler.yield();
}
}
fn printNumber(n: u64) void {
var buf: [20]u8 = undefined;
var i: usize = buf.len;
var num = n;
if (num == 0) {
vga.putChar('0');
return;
}
while (num > 0) {
i -= 1;
buf[i] = @truncate('0' + (num % 10));
num /= 10;
}
for (buf[i..]) |c| {
vga.putChar(c);
}
}
メインの更新
// boot.zig
const scheduler = @import("scheduler.zig");
const tasks = @import("tasks.zig");
const tss = @import("tss.zig");
const vga = @import("vga.zig");
export fn kernel_main() callconv(.C) noreturn {
vga.clear();
vga.print("Zig OS Kernel booting...\n\n");
// 初期化...
// (前回までのコード)
// TSS初期化
vga.print("[OK] TSS initialized\n");
tss.init();
// スケジューラ初期化
vga.print("[OK] Scheduler initialized\n");
scheduler.init();
// タスクを作成
_ = scheduler.createTask("counter", tasks.counterTask);
_ = scheduler.createTask("spinner", tasks.spinnerTask);
_ = scheduler.createTask("calc", tasks.calcTask);
vga.print("[OK] Tasks created\n\n");
// タスク一覧を表示
scheduler.printTaskList();
vga.print("\nStarting multitasking...\n\n");
// 割り込み有効化
idt.enableInterrupts();
// シェル起動(メインタスクとして動作)
shell.run();
while (true) {
asm volatile ("hlt");
}
}
タイマーハンドラの更新
// pit.zig
const scheduler = @import("scheduler.zig");
pub fn timerInterrupt() callconv(.Interrupt) void {
tick_count += 1;
// スケジューラを呼び出し
scheduler.tick();
pic.sendEoi(0);
}
実行結果
Zig OS Kernel booting...
[OK] GDT initialized
[OK] IDT initialized
[OK] PIC initialized
[OK] PIT initialized (1000Hz)
[OK] Keyboard initialized
[OK] TSS initialized
[OK] Scheduler initialized
[OK] Tasks created
=== Task List ===
ID Name State Ticks
---- ---------------- --------- --------
0 idle Running 0
1 counter Ready 0
2 spinner Ready 0
3 calc Ready 0
Starting multitasking...
Counter: 12345 Spinner: /
Fib(40) = 165580141
zigOS>
まとめ
今回実装したこと:
| コンポーネント | 説明 |
|---|---|
| Task構造体 | ID、状態、コンテキスト、スタック |
| コンテキストスイッチ | レジスタ保存/復元 |
| スケジューラ | ラウンドロビン + 優先度 |
| TSS | 割り込み時のスタック切り替え |
┌──────────────────────────────────────────────────────────────┐
│ タスク状態遷移図 │
├──────────────────────────────────────────────────────────────┤
│ │
│ create() │
│ ┌──────────────────────────────┐ │
│ │ ↓ │
│ │ ┌──────────┐ │
│ │ │ Ready │◄────────┐ │
│ │ └────┬─────┘ │ │
│ │ │ │ │
│ │ schedule()│ │ │
│ │ ↓ │ │
│ │ ┌──────────┐ │ │
│ │ │ Running │─────────┘ │
│ │ └────┬─────┘ preempt │
│ │ │ │
│ │ ┌──────────────────┼──────────────────┐ │
│ │ │ │ │ │
│ │ ↓ ↓ ↓ │
│ │ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ │ Blocked │ │ Sleeping │ │ Term │ │
│ │ └────┬─────┘ └────┬─────┘ └─────────┘ │
│ │ │ │ │
│ │ └───────┬──────────┘ │
│ │ │ wakeup │
│ │ ↓ │
│ │ ┌──────────┐ │
│ └───────────→│ Ready │ │
│ └──────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
次回は最終回、ファイルシステムを実装するよ。RAMディスク上の簡単なFSで、ファイルの読み書きができるようになる!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!