这章修了三个基础性的问题,每一个都影响着内核能否正确支持 busybox 的日常使用。

waitpid:从轮询到真正阻塞

sh 执行命令时需要等子进程退出:

int pid = fork();
if (pid == 0) { exec(...); }
waitpid(pid, &status, 0);   // 等子进程

原来的实现是 sti + hlt 轮询:

while (1) {
    code = proc_wait(&child_pid);
    if (code != -2) break;
    sti(); hlt(); cli();   // 等定时器唤醒,再试
}

这有两个问题:1. 浪费 CPU,定时器每 10ms 唤醒一次,立刻又继续轮询;2. 多核下 AP 的调度时钟可能不触发 BSP 的 hlt,等待时间不稳定。

改为真正的阻塞:父进程把自己设为 PROC_BLOCKED,调度器不再调度它,直到子进程退出时主动唤醒:

// SYS_WAIT4
int32_t code = proc_wait(&child_pid);
if (code == -2) {
    current->wait_wstatus_va = a2;     // 记住要写 wstatus 的用户地址
    current->state = PROC_BLOCKED;     // 挂起
    return -EINTR;                     // 返回调度器
}

子进程退出时(proc_exit)唤醒父进程:

if (ppid < MAX_PROCS && procs[ppid].state == PROC_BLOCKED) {
    procs[ppid].state = PROC_READY;    // 唤醒
}

信号投递:不能直接写用户虚拟地址

信号处理时,内核需要把返回地址压到用户栈(user_rsp - 8)。原来直接写:

*(uint64_t *)(*user_rsp - 8) = *user_rip;   // 直接写虚拟地址

但内核运行在 kernel_pml4,用户进程的虚拟地址在内核页表里没有映射,这行代码直接 Page Fault。

必须先把用户虚拟地址转成物理地址,再写:

uint64_t new_rsp = *user_rsp - 8;
uint64_t phys = vmm_virt_to_phys(current->pml4, new_rsp & ~0xFFFULL);
if (!phys) { proc_exit(-sig); return; }   // 用户栈不存在,直接退出
*(uint64_t *)(phys + (new_rsp & 0xFFF)) = *user_rip;
*user_rsp = new_rsp;

这个 bug 之前没暴露是因为单核下 QEMU 的内存布局比较宽松,物理地址和虚拟地址的低位偶尔相同,歪打正着写到了正确位置。SMP 下地址偏移变了就崩了。

ext2 写入:支持间接块

原来 ext2_write 只支持直接块(最多 12 个),文件超过 12 × block_size = 12KB 就写不下了:

if (blk_idx >= 12) return (int)written;   // 截断

busybox sh 执行 echo 等命令会产生临时文件,有些超过 12KB。

新增 get_or_alloc_block 函数,统一处理直接块/一级间接块/二级间接块:

直接块:     i_block[0..11]            → 最多 12 块
一级间接块:i_block[12] → 指针块      → 最多 256 块(1KB block)
二级间接块:i_block[13] → 指针块 → 指针块 → 最多 65536 块

支持的最大文件大小从 12KB 提升到约 64MB。

顺便把 MAX_PROCS 从 8 扩到 32——busybox 的 pipe 场景需要同时运行多个进程,8 个槽位不够用。

验收

/ # echo hello > /tmp/test.txt
/ # cat /tmp/test.txt
hello
/ # sleep 1 && echo done &
/ # done     ← 后台进程正常唤醒

waitpid 阻塞、信号、大文件写入都正常了。