一、信号到底是什么:异步通知,而非“调用”
信号是内核向进程递送的一种异步事件通知。所谓“异步”,是指信号的产生与进程当前的执行流没有因果关系:进程可能在执行任意一条指令时被打断,转去执行信号处理函数,处理完后再回到原位置继续。
一次信号从产生到被处理,经过几个明确的阶段:
信号产生 :用户通过 kill、内核因异常(如段错误、子进程退出)或硬件条件(如 alarm 到期)生成信号。
信号未决 :信号已经产生,但尚未被递达给进程。内核在进程的 PCB 中用 pending 位图记录哪些信号处于未决状态。
信号阻塞 :进程可以通过信号掩码(block 位图)暂时阻止某个信号被递达。被阻塞的信号会保持在未决状态,直到解除阻塞。
信号递达 :内核在合适的时机(通常是从内核态返回用户态之前)检查 pending 且未被阻塞的信号,执行其处理动作。
处理动作 :执行默认动作、忽略信号,或调用用户注册的处理函数。
二、信号的默认动作与不可捕获信号
五种默认动作
| Term | 进程正常终止 |
| Ign | 忽略,不做任何处理 |
| Core | 终止并生成 core 文件,记录进程终止时的内存快照,用于事后调试 |
| Stop | 暂停进程 |
| Cont | 恢复被暂停的进程 |
Core 与 Term 的区别在于前者会保留现场。云服务器通常默认将 core file size 设为 0,需要 ulimit -c unlimited 才能看到 core 文件。
SIGKILL 与 SIGSTOP 为什么“抓不住”
这两个信号的不可捕获性来自内核的刻意设计,而非 API 限制。在内核信号递送路径中,SIGKILL 和 SIGSTOP 被标记为仅内核可屏蔽的信号;它们永远不会被递送到用户空间,因此用户的 signal() 或 sigaction() 对它们无效。这一限制的目的是保证操作系统始终保留对进程生命周期的最终控制权:无论进程做了什么,内核都有手段终止或暂停它。实践中不应尝试捕获这两个信号,而应通过其他信号(如 SIGTERM)实现优雅退出。
三、重点信号速查
笔记中列出的重点信号是日常开发中最常打交道的,这里补充一些容易被忽略的行为细节。
SIGINT(2)与 SIGQUIT(3) 分别对应 Ctrl+C 和 Ctrl+\\。前者默认 Term,后者默认 Core。在终端中,这两个信号的产生由终端的行规程控制;如果进程脱离了控制终端,快捷键就不再产生它们。
SIGTERM(15) 是“礼貌的终止请求”。kill 命令默认发送的就是 15 号信号。进程可以捕获它,在退出前完成清理工作。这是实现优雅关闭的首选信号,而不是 SIGKILL。
SIGSEGV(11) 表示非法内存访问。默认动作是 Core。需要注意,SIGSEGV 也可能出现在栈溢出场景中:如果信号处理函数本身在已溢出的栈上运行,可能会再次触发 SIGSEGV,形成递归。在生产环境中,不建议在 SIGSEGV 处理函数中做复杂操作。
SIGCHLD(17) 的默认动作是 Ign。这意味着如果不主动回收子进程,子进程退出后会变成僵尸进程。正确的做法是在处理函数中用循环加 WNOHANG 的非阻塞方式回收所有已退出的子进程(详见第五节)。
SIGALRM(14) 由 alarm() 或 setitimer() 触发。每个进程同一时刻只有一个“闹钟”,多次调用 alarm() 会覆盖之前的设定。需要周期性通知时,必须在处理函数内部重新调用 alarm()。
SIGUSR1(10)与 SIGUSR2(12) 是留给应用程序自由使用的信号。多个自定义信号可以绑定到同一个处理函数,通过 signo 参数区分来源。但要注意:标准信号不排队。如果两个 SIGUSR1 在阻塞期间先后到达,解除阻塞后处理函数只会被调用一次。
四、注册信号处理:signal 与 sigaction 的选择
signal():能用,但语义不够精确
signal() 的原型是:
c
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
handler 可以取 SIG_IGN(忽略)或 SIG_DFL(恢复默认),也可以传入自定义函数。返回值是旧的处理函数指针,失败时返回 SIG_ERR。
问题在于:signal() 的行为在不同 Unix 实现中并不完全一致,尤其是信号处理函数执行完毕后信号是否自动复位、被中断的系统调用是否自动重启,历史上存在差异。对于需要严格可移植性或精细控制的代码,不推荐使用它。
sigaction():实际开发中的正确选择
sigaction() 提供了对处理行为的完整控制:
c
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
struct sigaction 的关键成员:
c
struct sigaction {
void (*sa_handler)(int); // 简单处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 带上下文信息
sigset_t sa_mask; // 处理期间额外阻塞的信号集
int sa_flags; // 行为标志
void (*sa_restorer)(void); // 已过时
};
几个值得记住的 sa_flags:
-
SA_RESTART:被该信号中断的慢速系统调用(如 read 阻塞在管道上)在信号处理函数返回后自动重启,而不是返回 EINTR。
-
SA_SIGINFO:使用三参数的 sa_sigaction,可从 siginfo_t 中获取发送者 PID、出错地址等信息。
-
SA_NOCLDSTOP:子进程仅暂停/继续时不向父进程发送 SIGCHLD,避免不必要的处理函数调用。
处理函数执行期间,同类型信号会自动阻塞。这是内核的默认行为:当 SIGINT 的处理函数正在运行时,另一个 SIGINT 不会递归进入处理函数,而是保持未决,等当前处理结束后再递达。如果还想在处理某个信号期间顺便屏蔽其他信号,通过 sa_mask 添加即可。
五、信号处理函数的编写铁律:短小且异步信号安全
信号处理函数最隐蔽的错误,是在其中调用了 非异步信号安全 的函数。
POSIX 明确列出了一组保证可以在信号处理函数中安全调用的函数,包括 write、read、_exit、alarm、sigprocmask、waitpid 等。printf、malloc、free 等 不在 这个列表中。原因是这些函数内部可能持有锁,而信号处理函数可能在主程序持有同一把锁时被调用,导致死锁或状态损坏。
因此,处理函数的编写原则是:只做记录、只调用异步信号安全函数、尽快返回。如果必须执行耗时或复杂的任务,常见做法是在处理函数中设置一个 volatile sig_atomic_t 标志,由主循环轮询处理;或者在处理函数中通过 write 向 pipe 写入一个字节,由专门的线程读取并执行实际逻辑。笔记中提到的“创建线程”也是一种思路,但 pthread_create 本身并非异步信号安全函数,需要谨慎对待。
六、pause 与信号唤醒:返回值是 EINTR
pause() 使调用进程主动进入可中断睡眠,直到 任意一个被捕获的信号 递达。如果信号的处理动作是终止进程,pause 不会返回;只有信号被捕获并处理函数返回后,pause 才返回。
关键细节: pause() 在信号处理后返回 -1,并将 errno 设置为 EINTR 。这不是错误,而是“被信号中断”的标准通知方式。
c
#include <unistd.h>
#include <errno.h>
int r = pause();
if (r == -1 && errno == EINTR) {
// 正常情况:被捕获的信号唤醒
}
如果希望暂停后由特定信号(如 SIGCONT)恢复,必须事先为那个信号注册处理函数。否则,若信号默认动作是终止或忽略,进程可能不会按预期恢复。
七、alarm 与 pause 的竞态:为什么需要 sigsuspend
用 alarm + pause 实现一个简单的 sleep 看起来自然:
c
alarm(sec);
pause();
但存在一个经典竞态:如果在 alarm(sec) 返回之后、pause() 被调用之前,进程被调度出去,而闹钟恰好在这段时间内到期,SIGALRM 就会在 pause() 之前被递达。处理函数返回后,进程再调用 pause(),此时闹钟已经“用掉”了,进程将永久挂起。
根本原因是“设置闹钟”和“进入睡眠”不是原子操作。解决方案是使用 sigsuspend:先屏蔽 SIGALRM,设置闹钟,再以原子的方式解除屏蔽并挂起。sigsuspend 将“修改信号掩码”与“挂起等待”合并为一个不可分割的操作,消除了上述窗口。凡是涉及时序严格的“等待某个信号”,都应优先考虑 sigsuspend 而不是裸 pause。
八、SIGCHLD 与子进程回收:循环 + WNOHANG
子进程终止时,内核向父进程发送 SIGCHLD。如果父进程不回收,子进程会变成僵尸进程。由于标准信号不排队,如果多个子进程几乎同时退出,父进程可能只收到一个 SIGCHLD。因此,在处理函数中只调用一次 wait() 是不够的,必须 循环调用 waitpid(-1, &status, WNOHANG),直到返回 0 或 -1 为止。
c
void sigchld_handler(int signo) {
int saved_errno = errno;
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
// 处理已退出的子进程 pid
}
errno = saved_errno;
}
保存并恢复 errno 是信号处理函数的良好习惯:信号处理函数可能在任何时刻打断主流程,不应让主流程看到的 errno 被处理函数内部的调用污染。
九、进阶:阻塞信号集与“先保护临界区,再放行信号”
有时不希望某个信号在主程序修改关键数据结构时被递达,否则处理函数可能看到不一致的中间状态。这不是“忽略信号”,而是“暂缓递达”。
内核为每个进程维护一个 阻塞信号集(信号掩码)。被阻塞的信号产生后保持未决状态,直到解除阻塞才递达。sigprocmask 用于修改这个集合:
c
sigset_t block, old;
sigemptyset(&block);
sigaddset(&block, SIGINT);
sigprocmask(SIG_BLOCK, &block, &old); // 进入临界区,SIGINT 暂不递达
// … 修改共享数据结构 …
sigprocmask(SIG_SETMASK, &old, NULL); // 离开临界区,未决的 SIGINT 立即递达
需要注意的是,SIGKILL 和 SIGSTOP 不可阻塞;即使将它们加入阻塞集,也不会生效。阻塞与忽略有本质区别:忽略是在信号 已经递达 之后不采取动作;阻塞是让信号 根本无法递达 ,保持在未决状态。两者在“信号是否会被记录”上完全不同。
十、共享内存:零拷贝的代价是“没有免费的同步”
为什么共享内存快
共享内存允许多个进程将同一块物理内存映射到各自的虚拟地址空间。一旦映射完成,进程读写这块内存就像读写普通变量一样,数据不需要经过内核缓冲区,也没有管道或消息队列那样的系统调用拷贝开销。因此它是 Unix IPC 中吞吐量最高的一种。
但“快”的代价是:共享内存本身不提供任何同步。如果写进程正在写入一半数据时读进程开始读取,读进程看到的就是不一致的状态。这与管道不同:管道通过阻塞和队列结构隐式地协调了读写节奏;共享内存则要求调用者 自己用信号量、互斥锁或条件变量 来建立读写顺序。
另一个关键差异是:共享内存没有“数据被读走就消失”的语义。写入的数据会一直保留在内存中,读进程可以反复读取同一份数据,直到显式清除或覆盖。因此它更像一块双方共享的白板,而不是一个传送带。
System V 共享内存的五个步骤
生成 Key。ftok(path, proj_id) 将一个存在的路径和一个非零的项目 ID 转换为 key_t。注意:ftok 不保证唯一性;不同路径指向同一 inode 时会产生相同的 key,不同文件系统的设备号也可能导致碰撞。对于有严格隔离需求的场景,可以使用 IPC_PRIVATE 让内核分配一个匿名 key,并通过 fork 继承给子进程。
创建或获取。shmget(key, size, shmflg)。size 会被内核 向上取整到 PAGE_SIZE 的整数倍 ;实际可用的最小单位是页,而不是字节。shmflg 中 IPC_CREAT 表示不存在则创建,IPC_CREAT | IPC_EXCL 表示要求创建新段、已存在则报错,低 9 位是权限位(如 0666)。
映射。shmat(shmid, NULL, 0)。shmaddr 传 NULL 让内核选择未映射的地址是最常见做法。若指定非零地址,必须是页对齐的,否则需要配合 SHM_RND 标志。返回值是进程地址空间中的虚拟地址指针。
读写。直接通过返回的指针对内存进行操作。由于没有任何隐式同步,双方必须约定协议,或借助信号量保证“写完之后再读”。
分离与删除。shmdt(addr) 解除本进程的映射;shmctl(shmid, IPC_RMID, NULL) 标记该共享内存段待删除,当所有进程都分离后内核释放资源。只 shmdt 而不 shmctl(IPC_RMID) 不会删除共享内存对象,它会一直存在于系统中,直到显式删除或重启。
| Linux 信号 | 内核与进程/进程间 | 无(仅传标志) | 强制打断执行流 | 异常处理、定时器、进程回收 |
| 共享内存 | 跨进程 (IPC) | 裸露内存,无队列 | 绝对不阻塞 | 图像、视频等超大数据高速共享 |
| 线程邮箱 | 同进程内的多线程 | 链表队列,有缓冲 | 可选(死等/轮询) | 模块解耦、生产者-消费者流式数据 |


![P11246 [GESP202409 六级] 小杨和整数拆分-171主机测评](https://www.171host.com/wp-content/uploads/2026/11/20261108100631-6af049a75855c-220x150.png)

