这章修了三个基础性的问题,每一个都影响着内核能否正确支持 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 阻塞、信号、大文件写入都正常了。