从零写OS(五十四):完整信号系统——sigaction、mask 与 sigreturn

跑 busybox sh 时发现一个诡异的问题:Ctrl+C 能杀掉进程,但 handler 执行完之后程序直接崩了——寄存器全乱了。调查下去,发现 ch53 的信号系统有三个根本缺陷。 问题一:寄存器没有保存 ch53 的 signal_dispatch 只保存了 user_rip 和 user_rsp: uint64_t new_rsp = *user_rsp - 8; *(uint64_t *)(phys + ...) = *user_rip; // 仅压返回地址 *user_rsp = new_rsp; *user_rip = handler; handler 执行期间会用到 r15, r14, …, rbx, rbp——这些全都没有保存。handler 返回时弹出的 rip 确实是原来的位置,但所有调用者保存的寄存器都被 handler 破坏了,接下来的代码跑的是垃圾数据。 问题二:sigaction 是空 stub case SYS_SIGACTION: case SYS_SIGPROCMASK: case SYS_SIGRETURN: return 0; // 什么都没做 musl 的 signal() 底层调用 sigaction,返回 0 让它以为设置成功了,实际上 handler 根本没有注册进去。 问题三:没有 signal mask handler 执行期间,如果同一个信号再次到来,会再次触发 dispatch,handler 递归执行,栈很快就溢出了。 解法:signal_frame_t + 真正的 sigaction 核心数据结构 把所有寄存器打包成一个结构体,压到用户栈: ...

June 8, 2026 · 3 min · 大飞

从零写OS(五十二):waitpid 阻塞、信号投递修复、ext2 间接块

这章修了三个基础性的问题,每一个都影响着内核能否正确支持 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)。原来直接写: ...

June 4, 2026 · 2 min · 大飞

从零写OS(二十四):signal 信号机制

到目前为止,进程只能顺序跑完,没有"被打断"的能力。按 Ctrl+C 没有反应,子进程崩溃父进程不知道,kill 命令更无从谈起。 这一章实现 signal——内核向进程发送异步通知的机制。 信号是什么 信号是一个整数编号,内核用它告诉进程"发生了某件事": 信号 编号 默认行为 SIGUSR1 10 终止 SIGKILL 9 终止(不可捕获) SIGTERM 15 终止 SIGCHLD 17 忽略 进程可以用 signal(sig, handler) 注册自定义处理函数,也可以接受默认行为。 关键:信号不立刻打断进程 信号发送时只是设置一个 pending 位,不立刻跳转。等进程下次从内核态返回用户态时,才检查并派发: SYS_KILL → pending_signals |= (1 << sig) syscall 处理完 → 准备 sysret ↓ 检查 pending_signals ↓ 有信号 → 操纵 user_rip/user_rsp → sysret 跳到 handler handler 执行完 ret → 回到原来被中断的位置 这和硬件中断不同——硬件中断可以在任意时刻打断 CPU,信号只在内核→用户的切换点才生效。 派发机制(trampoline) signal_dispatch 收到两个指针:内核栈上保存的 user_rip(原来准备 sysret 到的地址)和 user_rsp。它做的事: 1. user_rsp -= 8 2. *(user_rsp) = user_rip // 原返回地址压到用户栈 3. user_rip = handler_addr // sysret 改跳到 handler sysret 跳到 handler,handler 执行完 ret,弹出用户栈上的原 rip,回到 kill syscall 之后继续。 ...

May 14, 2026 · 2 min · 大飞
京ICP备14031575号-3