Phase 20
Userspace Init Shell
Replace the kernel-resident `init_task` and `shell_task` ring-0 functions with real ring-3 userspace processes. After this phase PID 1 is a `no_std` Rust binary loaded from the ramdisk; the interactive shell is a userspace ELF that init spawns. The kernel is no longer responsible for parsing commands or managing the interactive session.
Learning goal
Understand the Unix PID 1 contract: why init must never exit and must reap orphaned children.
Category
userspace
Components touched
Builds on
Unlocked by this phase
Why this phase matters
Replace the kernel-resident init_task and shell_task ring-0 functions with real ring-3 userspace processes. After this phase PID 1 is a no_std Rust binary loaded from the ramdisk; the interactive shell is a userspace ELF that init spawns. The kernel is no longer responsible for parsing commands or managing the interactive session.
How it works in m3OS
The roadmap entry defines the learning scope for this phase; a deeper implementation doc has not been matched yet.
How it builds on earlier phases
This phase builds on Signal Handlers and turns that foundation into Userspace Init Shell.
Implementation outline
- Uncomment
userspace/initanduserspace/shellin the workspaceCargo.tomland verify each crate has#![no_std], a#[panic_handler], and a_startentry point that calls into Rust (or a thin asm stub that sets uprbpand callsmain). - Write the syscall shim layer shared by both binaries: inline-asm wrappers for
read,write,fork,execve,waitpid,exit,pipe,dup2,open,close,chdir,kill,getpid. - Implement
userspace/init/src/main.rs: open/dev/consoleas fds 0, 1, 2; fork and exec/bin/sh; enter an infinitewaitpid(-1, WNOHANG)+pauseloop. - Implement the line-reader in
userspace/shell: read one byte at a time from fd 0 until\nor\r; handle backspace; cap line length at 256 bytes. - Implement the tokenizer: split on whitespace, respect single-quoted strings, expand nothing else (no glob, no variable expansion in this phase).
- Implement command dispatch: detect
cdandexitbuiltins; otherwisefork+execve; parent callswaitpidfor foreground; print exit status on non-zero. - Implement pipe support: detect
|token; allocate apipefd pair; fork left child withdup2(pipe_write, STDOUT_FILENO); fork right child withdup2(pipe_read, STDIN_FILENO); close both pipe ends in the parent;waitpidboth. - Implement
>and<redirection: open the target file beforefork; afterforkin the child,dup2onto stdout or stdin; close the extra fd; exec. - In
kernel_main, after all kernel-task servers have been started, load/sbin/initfrom the ramdisk via the ELF loader, allocate a new address space, and transfer to ring-3 as PID 1. Remove theinit_task()andshell_task()function calls. - Add the two ELF binaries to the xtask ramdisk assembly step so they appear at
/sbin/initand/bin/shin the filesystem image. - Validate end-to-end: boot in QEMU, observe the shell prompt, run
echo hello, runls | cat, runcat /etc/motd > /tmp/out, runcd /bin && pwd. - Confirm that the
stdin_feeder_taskkernel task (or equivalent) correctly feeds keyboard bytes into the PID 1 process’s stdin fd, so that the shell’sread(0, ...)syscall blocks until a key is pressed rather than spinning. - Run
cargo xtask checkto confirm no new clippy warnings are introduced by the two new workspace members before declaring the phase complete.
Task progress snapshot
73 of 73 tracked tasks are complete.
Documentation targets
- Documentation deliverables have not been listed yet.
Deferred follow-on work
- Moving kernel-task servers (
console_server,kbd_server,vfs_server,fat_server) to ring-3 userspace (requires the capability-grant IPC phase) - PTY / TTY line discipline (
/dev/pts,termios, raw mode, character echo) - Job control:
SIGTSTP,SIGCONT,fg,bg, process groups, sessions - Multi-user login and
/etc/passwd - Shell scripting: loops, conditionals, functions, variable assignment
- Environment variable export and
$VARexpansion - Tab completion and readline-style editing
execbuiltin (replace shell image) andsource/.- Pipelines longer than two stages
- Subshell expansion
$(...)and backtick substitution - Here-documents and here-strings
- Stderr redirection (
2>,2>&1) and fd duplication beyond 0/1/2
Source materials
Important code
Code spotlight cards
Code spotlight
Add raw syscall wrappers (syscall3–syscall6)
Add raw syscall wrappers (syscall3–syscall6). Why it matters: All higher-arity syscalls (fork, exec, pipe, etc.) need more than two arguments passed through registers. Focus on `syscall3` and `syscall4` in `userspace/syscall-lib/src/lib.rs` while you trace the snippet.
/// Raw three-argument syscall.
#[inline(always)]
pub unsafe fn syscall3(num: u64, a0: u64, a1: u64, a2: u64) -> u64 {
let mut rax = num;
asm!(
"syscall",
inlateout("rax") rax,
in("rdi") a0,
in("rsi") a1,
in("rdx") a2,
lateout("rcx") _,
lateout("r11") _,
options(nostack),
);
rax
}
/// Raw four-argument syscall. Note: arg4 uses r10 (not rcx, which is clobbered).
#[inline(always)]
pub unsafe fn syscall4(num: u64, a0: u64, a1: u64, a2: u64, a3: u64) -> u64 { Code spotlight
Create init crate
Create init crate. Why it matters: Init is the first userspace process; without it there is no process tree. Focus on `no_std` and `syscall-lib` in `userspace/init/Cargo.toml` while you trace the snippet.
Code spotlight
Create init entry point
Create init entry point. Why it matters: The kernel hands off control to this function; it must set up fds and never return. Focus on `_start` and `#![no_std]` in `userspace/init/src/main.rs` while you trace the snippet.
#[no_mangle]
pub extern "C" fn _start() -> ! {
// Fds 0/1/2 are pre-opened by the kernel for PID 1.
write_str(STDOUT_FILENO, "\nm3OS init (PID 1)\n");
// Spawn the first shell.
let mut shell_pid = spawn_shell();
if shell_pid < 0 {
write_str(STDOUT_FILENO, "init: failed to spawn shell\n");
exit(1);
}
// Reap loop: wait for any child, re-spawn shell if it exits.
loop {
let mut status: i32 = 0;
let ret = waitpid(-1, &mut status, WNOHANG);
if ret > 0 {
// A child exited. If it was the shell, re-spawn it.
if ret == shell_pid {
write_str(STDOUT_FILENO, "\ninit: shell exited, respawning...\n");
shell_pid = spawn_shell();
if shell_pid < 0 {
write_str(STDOUT_FILENO, "init: failed to respawn shell\n"); Key files
- userspace/init — userspace/init: minimal no_std Rust binary; sets up stdin/stdout/stderr
- userspace/shell — userspace/shell: interactive no_std Rust shell; reads stdin byte by byte
- kernel/src/main.rs — Remove shell_task() and init_task() from kernel/src/main.rs; stdin_feeder_task
- userspace/init/src/main.rs — Implement userspace/init/src/main.rs: open /dev/console as fds 0, 1, 2; fork
How real OSes differ
- Production init systems (systemd, OpenRC, s6) handle service supervision, dependency ordering, socket activation, cgroups, and logging.
- Even BusyBox's `init` handles `/etc/inittab` respawn entries, runlevels, and `sysvinit` compatibility.
- Real shells (dash, bash) implement full POSIX parameter expansion, here-documents, arithmetic, function definitions, and complex job control.
- This phase targets only the subset needed to run interactive commands and pipelines -- enough to feel like a real shell without the decades of accumulated specification compliance.
Success criteria
- The OS boots and presents an interactive shell prompt without any ring-0 command parsing code running.
- `ps` (or `cat /proc/1/status` if available) shows PID 1 is `init` and PID 2 is `sh`.
- `echo hello world` prints correctly.
- `ls | cat` produces the directory listing via a two-stage pipe.
- `cat /sbin/init > /dev/null` exercises I/O redirection without crashing.
- `cd /bin && pwd` prints `/bin`.
- `Ctrl-C` during a long-running child kills the child but leaves the shell running.
- Orphaned grandchildren are reaped by PID 1 (not left as zombies indefinitely).
- `exit` in the shell causes init to re-spawn it (or the boot stops gracefully).
- `kernel/src/main.rs` no longer contains `shell_task` or `init_task` functions.
- A command that does not exist (e.g. `notacommand`) causes `execve` to fail; the shell prints an error and returns to the prompt without crashing.
- Running `true` exits 0; running `false` exits 1; the shell prints nothing extra for `true` and prints a non-zero exit notice for `false`.
Related phases
Keep exploring the roadmap
Phase 19
CompleteSignal Handlers
Enable userspace programs to install and execute signal handlers. Implement the signal trampoline mechanism so ring-3 code can catch `SIGINT`, `SIGSEGV`, `SIGCHLD`, and other signals, run a user-supplied handler, and return cleanly to the interrupted execution without kernel assistance beyond the initial delivery and the `sigreturn` syscall.
Phase 21
CompleteIon Shell
Replace the minimal `no_std` shell from Phase 20 with [ion](https://github.com/redox-os/ion) -- the shell built for Redox OS, another custom Rust OS. After this phase `/bin/ion` is the interactive shell that userspace init spawns. The kernel-level and custom-shell scaffolding from Phase 20 remains as a regression harness; the user-facing shell gains a real scripting language, job control, pipelines, and interactive line editing (the latter pending Phase 22 TTY).