到目前为止,进程只能顺序跑完,没有"被打断"的能力。按 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 之后继续。
用户栈变化:
before: [ ... ]
after: [ 原user_rip ] ← handler ret 弹这里
不需要新的汇编 trampoline,ret 指令本身就完成了返回。
实现
process_t 新增两个字段
uint32_t pending_signals; // 位图:bit N = 信号 N 挂起
uint64_t signal_handlers[NSIG]; // 0=SIG_DFL, 1=SIG_IGN, 其他=函数地址
syscall_entry.asm 在 sysret 前加一次 dispatch
call syscall_handler
; 栈上布局:[rsp+0]=r15 .. [rsp+48]=rflags [rsp+56]=user_rip [rsp+64]=user_rsp
lea rdi, [rsp + 8*7] ; &user_rip
lea rsi, [rsp + 8*8] ; &user_rsp
call signal_dispatch
pop r15 ... pop rcx ... pop rsp
o64 sysret
两个新 syscall
SYS_SIGNAL(11): signal_handlers[sig] = handler_addr
SYS_KILL(12): procs[pid].pending_signals |= (1 << sig)
验证
用户程序:
SYS_SIGNAL(SIGUSR1, sigusr1_handler) ; 注册 handler
SYS_KILL(1, SIGUSR1) ; 向自己(pid=1)发信号
SYS_WRITE("returned from kill") ; kill 返回后继续执行
SYS_EXIT(0)
sigusr1_handler:
SYS_WRITE("caught SIGUSR1!")
ret
输出:
signal test start
handler registered
caught SIGUSR1!
returned from kill
小结
signal 的本质是:内核在用户态恢复点插入一段代码。
不需要新的调度器逻辑,只需要在 syscall 返回前检查 pending 位,修改即将 sysret 到的 RIP 和 RSP,用户栈上保存的原 RIP 当作 handler 的返回地址——就这样,一个完整的用户态信号机制就建立起来了。