自作 OS「FerriOS」の開発日記、第4回です。前回は ELF バイナリが動くようにして、必要最低限のシステムコールを実装しました。

今回は残りのシステムコールを実装していこうと思います。ズバリ今回実装するシステムコールは:

  • exit()
  • wait()
  • kill()
  • sbrk()
  • uptime()

の5つです。

まだファイルシステムは実装していないので open()read() などは実装していません。そちらは次回以降。

image-20260531053840914

RustでOS開発シリーズ

今回のゴール

  • exit() / wait() を実装する
  • 子プロセスの終了ステータスを親プロセスで受け取る
  • kill() で対象プロセスを終了させる
  • sbrk() でユーザプロセスのヒープを伸ばせるようにする

今回のメインは exit() / wait() です。前回 fork()exec() は実装しましたが、子プロセスを終了させたり、親プロセスが子プロセスの終了を待ったりする仕組みはまだありませんでした。

ついでに、ファイルシステム以外の主要なシステムコールとして kill()sbrk()uptime() も実装しました。

前準備

システムコール番号の追加

まずは abi クレートにシステムコール番号を追加しておきます。

abi/src/lib.rs

pub const SYS_UPTIME: SyscallNum = 6;
pub const SYS_EXIT: SyscallNum = 7;
pub const SYS_WAIT: SyscallNum = 8;
pub const SYS_KILL: SyscallNum = 9;
pub const SYS_SBRK: SyscallNum = 10;

ProcessState の追加

exit() / wait() を実装するにあたり、まず悩んだのがプロセスの状態をどこに持たせるかです。

これまでも ThreadState はありました。

pub enum ThreadState {
    Unused,
    Embryo,
    Sleeping,
    Runnable,
    Running,
    Zombie,
}

ただし、RunningRunnable は CPU に載る単位であるスレッドの状態です。一方で、親が wait() で見るのは「子プロセスが終了済みかどうか」です。

そこで、プロセス側にも状態を持たせます。

kernel/src/thread/uprocess/mod.rs

pub enum ProcessState {
    Unused,
    Alive,
    Zombie,
}

ThreadState はスケジューラが見る実行状態、ProcessState はプロセスの寿命状態、という分け方です。

Process 構造体の拡張

Process には、親プロセス、終了ステータス、kill フラグ、ヒープサイズを追加しました。

pub struct Process {
    pub pid: ProcessID,
    pub ppid: Option<ProcessID>,
    pub state: ProcessState,
    pub threads: [Option<usize>; NTHREAD_PER_PROCESS],
    pub nthread: usize,
    pub page_table: Option<PhysFrame>,
    pub exit_status: abi::RetValue,
    pub killed: bool,
    pub heap_size: usize,
}

ppidwait() で「自分の子プロセスかどうか」を判定するために使います。exit_statusexit(status) で保存し、親が wait() で受け取る値です。

killedheap_size は後ほど kill()sbrk() で使います。

wait() と exit() の実装

exit() の実装

exit(status) を実装するにあたって気にするべきは、子プロセスが返すステータス値をどう親プロセスに渡すかです。親プロセスがあとで wait() したときに読めるよう、終了ステータスを Process 構造体の exit_status に保存しています。

大まかな流れはこんな感じです。

pub fn exit(ret_value: abi::RetValue) -> Result<(), &'static str> {
    let pid = cpu::CPU.lock().current_pid().ok_or("no process")?;

    {
        let mut process_table = PROCESS_TABLE.lock();
        let process = process_table[pid].as_mut().ok_or("no process")?;
        process.state = ProcessState::Zombie;
        process.exit_status = ret_value;
    }

    // 所属スレッドを Zombie にする
    // スケジューラに戻る
    yield_from_syscall_context();
}

ここでは終了ステータスを設定しプロセスのステータスを Zombie に設定しておきます。Zombie プロセスは後ほど、カーネル・ユーザの間で切り替わる際に exit 処理が走ります。

また、exit() したプロセスのスレッドはスケジューラが実行しないようにしなければならないので、所属スレッドも ThreadState::Zombie に変更しておきます。

wait() の実装

wait() は、現在のプロセスの子プロセスを探し、その中に Zombie 状態のものがあれば回収します。すなわち、プロセスのメモリを解放して、プロセス構造体を初期化します。

pub fn wait() -> Result<(ProcessID, abi::RetValue), &'static str> {
    let process = cpu::CPU.lock().current_process().expect("no pid");
    let pid = process.pid;

    loop {
        let (mut zombie_child, havekids) = {
            let mut process_table = PROCESS_TABLE.lock();
            let mut zombie_child = None;
            let mut havekids = false;

            for i in 0..NPROCESS-1 {
                let is_child = process_table[i]
                    .as_ref()
                    .is_some_and(|child| child.ppid == Some(pid));

                if !is_child {
                    continue;
                }

                havekids = true;
                if process_table[i].as_ref().is_some_and(|child| child.state == ProcessState::Zombie) {
                    zombie_child = process_table[i].take();
                    break;
                }
            }

            (zombie_child, havekids)
        };

        if let Some(child) = zombie_child.as_mut() {
            free_process(child)?;
            return Ok((child.pid, child.exit_status));
        }

        if !havekids {
            return Err("no child process");
        }

        yield_from_syscall_context();
    }
}

本当は子プロセスがまだ生きている場合、親を Sleeping にして、子の exit() で wakeup するようにしたいのですが、まだ sleep/wakeup は実装していないので、今回は一度スケジューラに戻る方式にしています。

free_process() の実装

プロセスメモリなどの解放は free_process() に実装しています。wait() で Zombie の子を見つけたら、そのプロセスのリソースを解放します。

まずはプロセスが持っているスレッドを走査し、カーネルスタックを解放します。

for tid in process.threads.into_iter().flatten() {
    let thread = &mut thread_table[tid];

    if thread.kstack != 0 {
        let layout = alloc::alloc::Layout::from_size_align(memory::STACK_SIZE, 16)?;
        let kstack_base = (thread.kstack - memory::STACK_SIZE as u64) as *mut u8;
        unsafe {
            alloc::alloc::dealloc(kstack_base, layout);
        }
    }

    *thread = Thread::new();
}

続いて、ユーザ空間のページテーブルも解放します。

if let Some(page_table) = process.page_table {
    let (current_page_table, _) = Cr3::read();
    if current_page_table == page_table {
        return Err("cannot free cr3 active page table");
    }

    process.page_table = None;
    let mut frame_allocator_guard = memory::FRAME_ALLOCATOR.lock();
    let frame_allocator = frame_allocator_guard.as_mut().ok_or("FRAME_ALLOCATOR not initialized")?;
    memory::umem::free_uvm(page_table, frame_allocator)?;
}

ここでは、現在 CR3 に入っているページテーブルを解放しないようにチェックしています。

物理フレームの返却

プロセスのページテーブルを解放するために物理フレームの返却処理の実装も必要です。

Writing an OS in Rust で実装している BootInfoFrameAllocator は、ブートローダのメモリマップから順番に frame を配る仕組みのみ実装しており、フレームの返却と再割り当て処理は実装していません。そこで、返却済み frame を保持する recycled_frames を追加します。

pub struct BootInfoFrameAllocator {
    memory_map: &'static MemoryRegions,
    next: usize,
    recycled_frames: Vec<PhysFrame>,
}

allocate_frame() では、まず返却済み frame を優先して再利用します。

fn allocate_frame(&mut self) -> Option<PhysFrame> {
    if let Some(frame) = self.recycled_frames.pop() {
        return Some(frame);
    }

    let frame = self.usable_frames().nth(self.next);
    self.next += 1;
    frame
}

そして FrameDeallocator も実装しておきます。

impl FrameDeallocator<Size4KiB> for BootInfoFrameAllocator {
    unsafe fn deallocate_frame(&mut self, frame: PhysFrame) {
        self.recycled_frames.push(frame);
    }
}

free_uvm() の実装

ユーザ空間全体を解放するため、free_uvm() を実装しました。

解放順序はこんな漢字:

leaf data frame
→ PT frame
→ PD frame
→ PDPT frame
→ PML4 frame

ページテーブル階層を walk して順次畳んでいく形です。leaf frame を返却してから、空になった page table の frame も返却します。

wait(status) と copy_to_user()

wait() では、子プロセスの PID だけでなく終了ステータスも親に返すようにしたいです。

ユーザ側 API (userlib) はこうしました。

userlib/src/syscalls.rs

pub fn wait(status_ptr: Option<&mut abi::RetValue>) -> SysRet {
    let status_ptr = match status_ptr {
        Some(p) => p as *mut abi::RetValue as i64,
        None => 0,
    };

    unsafe {
        syscall(SYS_WAIT, status_ptr, 0, 0)
    }
}

Rust 側では Option<&mut RetValue> として扱い、syscall ABI では null pointer を 0 として渡します。

カーネル側では sys_wait()wait() の結果を受け取り、ステータスの書き込み先が指定されていれば copy_to_user() でユーザ空間にコピーします。

if status_ptr != abi::NULL_POINTER {
    let bytes = exit_status.to_ne_bytes();
    if copy_to_user(status_ptr, &bytes).is_err() {
        return abi::RET_ERROR;
    }
}

copy_to_user() では、ユーザ仮想アドレスをページテーブルで引き、PRESENTUSER_ACCESSIBLEWRITABLE を確認してからコピーしています。

syscall 中の yield

wait() は、子プロセスがまだ終了していない場合にスケジューラへ戻ります。そこで最初は普通の yield_from_context() を使っていました。

が、ここで page fault しました。

原因は SYSCALL 経路ではすでに swapgs 済みであることです。その状態のまま通常の yield と同じように別のユーザコンテキストへ切り替えると、GS / KernelGsBase が合わなくなります。

そこで syscall 実行中専用の yield を追加しました。

kernel/src/scheduler/mod.rs

pub fn yield_from_syscall_context() {
    get_scheduler().on_syscall_yield();
}

RoundRobin 側では、scheduler に戻る前後で swapgs します。

unsafe {
    core::arch::asm!("swapgs");
    switch_context(old_context, new_context);
    core::arch::asm!("swapgs");
}

saved_user_rsp の修正

syscall 中 yield を入れた後、別の問題も出ました。

親プロセスが wait() syscall の途中で yield し、その間に子プロセスが syscall を実行すると、cpu.saved_user_rsp が子の値で上書きされます。その後、親が syscall から戻るとき、親のコードに戻っているのにスタックだけ子のものになってしまいました。

結果として RIP=0 に飛んで page fault します。

そこで、saved_user_rsp の実体は thread ごとの context.rsp3 に持たせるようにしました。

table[current_tid].context.rsp3 = cpu.saved_user_rsp;

次のスレッドを選ぶときは、そのスレッドの rsp3 を CPU 側へ戻します。

cpu.saved_user_rsp = table[next_tid].context.rsp3;

ticks と uptime() の実装

kill() のテストなどで少し待つ処理が欲しかったので、先に uptime() も追加しました。

これまでは PIT を明示的に設定していませんでした。今回は 1秒 = 100 ticks として扱いたかったので、PIT を 100Hz に設定しています。

kernel/src/interrupts.rs

const PIT_BASE_FREQUENCY: u32 = 1_193_182;
pub const TIMER_FREQUENCY_HZ: u32 = 100;

static TICKS: Mutex<u64> = Mutex::new(0);

タイマ割り込みごとに TICKS をインクリメントします。

fn countup_ticks() {
    let mut ticks = TICKS.lock();
    *ticks += 1;
}

pub fn get_ticks() -> u64 {
    x86_64::instructions::interrupts::without_interrupts(|| *TICKS.lock())
}

MutexGuard をそのまま足そうとして少しハマりました。TICKS.lock() の中身を更新するには *ticks += 1 として中身を参照する必要があります。また、get_ticks() 側ではタイマ割り込み中に同じロックを取りに行くと困るので、割り込みを一時的に無効化しています。

kill() の実装

FerriOS の実装の参考にしている xv6 の kill() は、対象プロセスをその場で破壊するわけではありません。対象プロセスの killed フラグを立てておき、対象プロセス自身がユーザ空間へ戻る直前などの安全なタイミングで exit() します。FerriOS でも同じ方針にしました。

pub fn kill(pid: abi::ProcessID) -> Result<(), &'static str> {
    let mut process_table = PROCESS_TABLE.lock();

    for i in 0..NPROCESS-1 {
        let is_target = process_table[i]
            .as_ref()
            .is_some_and(|proc| proc.pid == pid);

        if !is_target {
            continue;
        }

        let mut proc = process_table[i].take().unwrap();
        proc.killed = true;
        process_table[i] = Some(proc);

        return Ok(());
    }

    Err("no process")
}

syscall の入口と出口で現在のプロセスが kill 済みか確認し、kill されたプロセスであれば exit します。そこから先の処理は exit() システムコールと同じです。

pub fn exit_if_current_process_killed() {
    let process = cpu::CPU.lock().current_process().expect("no process");
    if process.killed {
        ksyscall::exit(abi::RET_ERROR);
    }
}

fork() の復帰処理修正

kill() のテスト中、kill() を呼ぶ前に子プロセスが RIP=0 で page fault する問題がありました。

調べてみると原因は kill() ではなく、fork() の子プロセスの復帰処理でした。

これまでは fork した子も、新規プロセス用の trampoline でユーザ空間へ入っていました。しかし fork の子は、新規に main() から始まるわけではありません。親が fork() syscall から戻るのと同じ場所に、レジスタ状態を復元して戻る必要があります。

そこで fork 子専用の fork_return_trampoline() を追加しました。

pub(super) unsafe extern "C" fn fork_return_trampoline() -> ! {
    // コピー済み TrapFrame からレジスタを復元して iretq
}

fork() 側では、親の TrapFrame を子にコピーし、子の rax だけ 0 にします。

*child.tf.expect("no trapframe") = parent_tf;
(*child_tf).rax = 0;
child.context.rip = fork_return_trampoline as u64;

sbrk() の実装

最後に sbrk() です。

sbrk(n) は、プロセスの user memory end を n バイト分増減させ、変更前のアドレスを返すシステムコールです。xv6 の proc->sz に相当する値として、FerriOS では Process.heap_size を使います。

まず、exec() で ELF の LOAD セグメントを読み込んだ後、その末尾を page round up して初期 heap_size にします。

process.heap_size = prepared.heap_size;

次に、ユーザ空間を伸ばす alloc_uvm() と、縮める dealloc_uvm() を実装しました。

pub fn alloc_uvm(
    pml4: &mut PageTable,
    oldsz: usize,
    newsz: usize,
    frame_allocator: &mut (impl FrameAllocator<Size4KiB> + FrameDeallocator<Size4KiB>),
) -> Result<usize, &'static str> {
    // oldsz..newsz を page 単位で map する
}
pub fn dealloc_uvm(
    pml4: &mut PageTable,
    oldsz: usize,
    newsz: usize,
    frame_deallocator: &mut (impl FrameAllocator<Size4KiB> + FrameDeallocator<Size4KiB>),
) -> Result<usize, &'static str> {
    // oldsz..newsz を page 単位で unmap する
}

sbrk() 自体は、現在の heap_size を保存してから grow_process_heap() を呼び、成功したら古いアドレスを返します。

pub fn sbrk(n: isize) -> Result<abi::UserAddress, &'static str> {
    let pid = cpu::CPU.lock().current_pid().expect("no process");
    let mut process_table = PROCESS_TABLE.lock();

    let address = process_table[pid].unwrap().heap_size as u64;
    grow_process_heap(n, &mut process_table[pid].unwrap())?;

    Ok(address)
}

sbrk() の実行フローはこんな感じ。

sbrk-implementation-flow

n>0 の場合は alloc_uvm() を呼び出してフレームを取得し、n<0 の場合は dealloc_uvm() でフレームを返却します。alloc 時にエラーが起きた際にもフレームを返却します。n=0 の場合は何もしません。

ユーザライブラリ実装の改良

ユーザランタイムの実装

…とその前に、userlib 側に main 関数用のランタイムを実装しておきます。

以前は #![no_main]#[unsafe(no_mangle)] を使って main シンボルを直接出していましたが、これらを不要にします。

userlib 側に #[lang = "start"]#[lang = "termination"] を用意しました。

#[lang = "termination"]
pub trait Termination {
    fn report(self) -> RetValue;
}

impl Termination for () {
    fn report(self) -> RetValue {
        RET_SUCCESS
    }
}

impl Termination for RetValue {
    fn report(self) -> RetValue {
        self
    }
}

#[lang = "start"]
fn lang_start<T: Termination + 'static>(
    main: fn() -> T,
    argc: isize,
    argv: *const *const u8,
    sigpipe: u8,
) -> isize {
    let _ = sigpipe;

    unsafe {
        args::ARGC = argc as usize;
        args::ARGV = argv;
    }

    let ret = main().report();
    exit(ret);
}

lang_start() はユーザアプリケーションのエントリポイントを示します。

fn main() -> () 向けの Termination と fn main() -> RetValue (RetValue は u64) 向けの Termination を実装することで、戻り値ありの main() と戻り値なしの main() を実装できるようにしています。

これでユーザアプリは以下のように書けます。

#![no_std]

use userlib::*;

fn main() -> RetValue {
    42
}

main() の戻り値は exit() に渡されます。これでアプリ側の見た目は普通の Rust コードっぽくなりましたね(#![no_std] からは目を背ける)。

引数を渡す

exec() に引数を渡せるようにしたので、ユーザアプリ側から args() で読めるようにしました。とりあえず userlib::args() で引数を取得できる形で実装。

pub fn args() -> Args {
    Args {
        argc: argc(),
        argv: argv(),
        index: 0,
    }
}

Args は iterator として実装しています。

impl Iterator for Args {
    type Item = &'static str;
}

また、現状の exec() システムコールは配列で引数を渡す必要がある(Linux でいうところの execv() みたいな感じ)のですが、execl() のように不特定数の引数を渡せるようにしたかったので、userlib 側にマクロとして execl!() を実装しました。C言語なら不特定数の引数を関数で実装できますが、Rust の関数だと引数の数が厳密にチェックされるのでマクロとして実装する必要があるのですね。

#[macro_export]
macro_rules! execl {
    ($path:expr $(, $arg:expr)* $(,)?) => ;
}

これで親プロセスから子プロセスを起動するときは、以下のように書けます。

execl!("/child", "abc", "def");

動作確認

今回の確認用ユーザアプリは、まず fork() して子プロセスで /childexec() します。子プロセスには引数として “abc”, “def” を渡し、wait() で子プロセスの終了を待って戻り値を受け取ります。

次に sbrk() でメモリを確保しつつ、もう1つ子プロセスを生成して、今度は子プロセスを kill() します。

user/init/src/main.rs

#![no_std]

use userlib::*;

fn main() {
    // 1st child
    let ret = fork();
    if ret == RET_ERROR {
        panic!("failed to call fork()");
    }
    if ret == 0 {
        // on the child process
        let ret = execl!("/child", "abc", "def");
        if ret == RET_ERROR {
            panic!("failed to call exec()");
        }
    }

    // on the parent process
    let pid = getpid();

    print_fmt!("[parent] waiting child process...");
    let mut status: RetValue = RET_SUCCESS;
    let child_pid = wait(Some(&mut status));
    print_fmt!("[parent (pid={})] child process has exited; child's pid is {} and ret value is {}", pid, child_pid, status);

    // 2nd child
    let alloced_memory = sbrk(1234);
    let addr = alloced_memory as *mut u64;
    unsafe {
        *addr = 0;
    }

    let ret = fork();
    let start = uptime();
    if ret == RET_ERROR {
        panic!("failed to call fork()");
    }
    if ret == 0 {
        // on the child process
        unsafe {
            *addr = 0;
        }
        loop {
            unsafe {
                *addr += 1;
                print_fmt!("[child (pid = {})] *addr = {}", pid, *addr);
            }
        }
    }

    // on the parent process
    // wait a moment..
    loop {
        if uptime() - start > 5 {
            break;
        }
    }

    // kill it
    print_fmt!("[parent] pid = {} kill pid {}", pid, ret);
    kill(ret as ProcessID);
    print_fmt!("[parent] pid = {} done.", pid);

    // sbrk test
    if alloced_memory == RET_ERROR {
        print_fmt!("[parent] pid = {} alloc failed", pid);
    }
    else {
        unsafe {
            print_fmt!("[parent (pid = {})] *addr = {}", pid, *addr);
        }
    }

    loop {
    }
}

1つ目の child 側では args() を表示し、ループを回して uptime() で取得される ticks 値を足していきながら、最後に RetValue を返します。

user/child/src/main.rs

fn main() -> RetValue {
    let pid = getpid();

    for arg in args() {
        print_fmt!("[child (pid={})] arg: {}", pid, arg);
    }

    let mut ret = 0;
    for _ in 0..60 {
        ret += uptime();
    }

    ret
}

2つ目の子プロセスでは sbrk() で確保した領域を親子でアクセスつつ(親子間で分離されていることの確認)、子プロセス側では無限ループを実行する中で、親から kill() して止めるテストもしています。

let alloced_memory = sbrk(1234);
let addr = alloced_memory as *mut u64;

let ret = fork();
if ret == 0 {
    loop {
        unsafe {
            *addr += 1;
            print_fmt!("[child] *addr = {}", *addr);
        }
    }
}

kill(ret as ProcessID);

これでプロセスを作って、実行して、終了させて、親プロセスが子プロセスの exit status を受け取るところまで来ました。

実行結果

Welcome to FerriOS!

Initializing..done.
console-mode: Both
initializing memory.. initializing heap memory.. done.
Starting kernel threads.. done.

[parent] waiting child process...
[child (pid=1)] arg: abc
[child (pid=1)] arg: def
[child (pid=1)] ticks = 2 ret = 2
[child (pid=1)] ticks = 2 ret = 4
[child (pid=1)] ticks = 2 ret = 6
[child (pid=1)] ticks = 2 ret = 8
[child (pid=1)] ticks = 2 ret = 10
[child (pid=1)] ticks = 2 ret = 12
[child (pid=1)] ticks = 2 ret = 14
[child (pid=1)] ticks = 2 ret = 16
[child (pid=1)] ticks = 2 ret = 18
[child (pid=1)] ticks = 2 ret = 20
[child (pid=1)] ticks = 2 ret = 22
[child (pid=1)] ticks = 2 ret = 24
[child (pid=1)] ticks = 2 ret = 26
[child (pid=1)] ticks = 2 ret = 28
[child (pid=1)] ticks = 2 ret = 30
[child (pid=1)] ticks = 2 ret = 32
[child (pid=1)] ticks = 2 ret = 34
[child (pid=1)] ticks = 2 ret = 36
[child (pid=1)] ticks = 2 ret = 38
[child (pid=1)] ticks = 2 ret = 40
[child (pid=1)] ticks = 2 ret = 42
[child (pid=1)] ticks = 2 ret = 44
[child (pid=1)] ticks = 2 ret = 46
[child (pid=1)] ticks = 2 ret = 48
[child (pid=1)] ticks = 4 ret = 52
[child (pid=1)] ticks = 4 ret = 56
[child (pid=1)] ticks = 4 ret = 60
[child (pid=1)] ticks = 4 ret = 64
[child (pid=1)] ticks = 4 ret = 68
[child (pid=1)] ticks = 4 ret = 72
[child (pid=1)] ticks = 4 ret = 76
[child (pid=1)] ticks = 4 ret = 80
[child (pid=1)] ticks = 4 ret = 84
[child (pid=1)] ticks = 4 ret = 88
[child (pid=1)] ticks = 4 ret = 92
[child (pid=1)] ticks = 4 ret = 96
[child (pid=1)] ticks = 4 ret = 100
[child (pid=1)] ticks = 4 ret = 104
[child (pid=1)] ticks = 4 ret = 108
[child (pid=1)] ticks = 4 ret = 112
[child (pid=1)] ticks = 4 ret = 116
[child (pid=1)] ticks = 4 ret = 120
[child (pid=1)] ticks = 4 ret = 124
[child (pid=1)] ticks = 4 ret = 128
[child (pid=1)] ticks = 4 ret = 132
[child (pid=1)] ticks = 6 ret = 138
[child (pid=1)] ticks = 6 ret = 144
[child (pid=1)] ticks = 6 ret = 150
[child (pid=1)] ticks = 6 ret = 156
[child (pid=1)] ticks = 6 ret = 162
[child (pid=1)] ticks = 6 ret = 168
[child (pid=1)] ticks = 6 ret = 174
[child (pid=1)] ticks = 6 ret = 180
[child (pid=1)] ticks = 6 ret = 186
[child (pid=1)] ticks = 6 ret = 192
[child (pid=1)] ticks = 8 ret = 200
[child (pid=1)] ticks = 8 ret = 208
[child (pid=1)] ticks = 8 ret = 216
[child (pid=1)] ticks = 8 ret = 224
[child (pid=1)] ticks = 8 ret = 232
[child (pid=1)] exiting..
[parent (pid=0)] child process has exited; child's pid is 1 and ret value is 232
[child (pid = 0)] *addr = 1
[child (pid = 0)] *addr = 2
[child (pid = 0)] *addr = 3
[child (pid = 0)] *addr = 4
[child (pid = 0)] *addr = 5
[child (pid = 0)] *addr = 6
[child (pid = 0)] *addr = 7
[child (pid = 0)] *addr = 8
[child (pid = 0)] *addr = 9
[child (pid = 0)] *addr = 10
[child (pid = 0)] *addr = 11
[child (pid = 0)] *addr = 12
[child (pid = 0)] *addr = 13
[child (pid = 0)] *addr = 14
[child (pid = 0)] *addr = 15
[child (pid = 0)] *addr = 16
[child (pid = 0)] *addr = 17
[child (pid = 0)] *addr = 18
[child (pid = 0)] *addr = 19
[child (pid = 0)] *addr = 20
[child (pid = 0)] *addr = 21
[child (pid = 0)] *addr = 22
[child (pid = 0)] *addr = 23
[child (pid = 0)] *addr = 24
[child (pid = 0)] *addr = 25
[child (pid = 0)] *addr = 26
[child (pid = 0)] *addr = 27
[child (pid = 0)] *addr = 28
[child (pid = 0)] *addr = 29
[parent] pid = 0 kill pid 2
[parent] pid = 0 done.
[parent (pid = 0)] *addr = 0

うん。子プロセスの戻り値を親プロセス側で受け取れていますね。ユーザ空間内の sbrk() で確保した領域も親子間で分離できていて、値の更新ができています。最後に pid 2 の子プロセスを kill() して止めることにも成功しています。

おわりに

今回は exit() / wait() を中心に、kill()sbrk()uptime()、userlib の改善などを実装しました。

これで xv6 基準でいえばファイルシステム以外の部分は出揃ったのかな、と思っています。ということで、次回からはいよいよファイルシステムの実装ですかね。沼りそうな予感しかしないなぁ。