一、信号处理函数设计的两大黄金原则
设计信号处理函数时,最重要的一条原则是:尽量写得简单。原因是越复杂的处理函数,越容易引入难以调试的竞态条件(race condition)。 实践中有两种常见的设计模式: 模式一:设置标志位,主程序检查
信号处理函数只做一件事:把某个全局标志变量设为 1
主程序在循环中定期检查这个标志,再做真正的处理工作
模式二:清理后退出或跳转
信号处理函数做必要的清理(释放资源、关闭文件等)
然后调用 _exit() 终止进程,或用非本地跳转跳回主程序某处
二、可重入函数 vs 不可重入函数
2.1 什么是"可重入"?
想象一个函数正在被主程序执行到一半,这时信号来了,信号处理函数也调用了同一个函数。如果这种"同时执行"不会导致错误,这个函数就是可重入的(reentrant)。 形象比喻:
可重入函数 ≈ 每次调用都用自己独立的"草稿纸"(局部变量)
不可重入函数 ≈ 所有调用共用同一张"草稿纸"(全局/静态变量)
2.2 哪些函数是不可重入的?
不可重入的根本原因是函数内部使用了共享状态:
不可重入的原因
├── 使用了全局/静态数据结构
│ ├── malloc() / free() → 维护共享的空闲链表
│ ├── printf() / scanf() → 维护共享的 stdio 缓冲区
│ └── strtok() → 使用静态内部指针
└── 返回静态分配的内存(每次调用覆盖同一块内存)
├── crypt()
├── getpwnam()
├── gethostbyname()
└── getservbyname()
2.3 不可重入的危险示意
主程序调用 crypt("abc", "xx") ──────→ [crypt 开始写静态缓冲区]
│
← 信号到来,打断!──┘
│
↓
信号处理函数调用 crypt("def", "xx")
[crypt 用新结果覆盖了同一个静态缓冲区]
│
↓
信号处理函数返回
│
主程序继续 ─────→ 读取 crypt() 的返回值
[但缓冲区已被处理函数的结果覆盖!]
← 数据不一致,产生难以复现的 bug
三、异步信号安全函数(Async-Signal-Safe)
异步信号安全函数是指:可以在信号处理函数中安全调用的函数。它要么本身是可重入的,要么在执行期间不会被信号中断。
3.1 常用的安全函数列表(节选)
| 文件 I/O | read(), write(), open(), close(), lseek() |
| 进程控制 | fork(), execve(), _exit(), getpid(), kill() |
| 信号相关 | sigaction(), sigprocmask(), sigpending(), sigsuspend(), raise() |
| 信号集操作 | sigemptyset(), sigfillset(), sigaddset(), sigdelset(), sigismember() |
| 时间 | alarm(), sleep(), time(), clock_gettime() |
| 等待子进程 | wait(), waitpid() |
规则:凡是不在安全列表中的函数,都不应在信号处理函数中调用。 printf() 不在安全列表中,但教学示例中为了方便演示经常使用,实际项目中要避免。
3.2 errno 的保护
安全列表中的函数虽然可以调用,但它们可能会修改 errno,从而覆盖主程序中 errno 的值。解决方法:进入处理函数时保存 errno,退出前恢复。
void handler(int sig) {
int savedErrno = errno; // 保存主程序的 errno
// … 调用可能修改 errno 的安全函数 …
errno = savedErrno; // 恢复,不影响主程序
}
四、全局变量与 sig_atomic_t
4.1 为什么需要 volatile?
编译器为了优化性能,可能把变量缓存在寄存器里,不每次都从内存读取。但信号处理函数对全局变量的修改发生在内存中,如果主程序读的是寄存器里的旧值,就永远感知不到变化。 volatile 关键字告诉编译器:“每次都必须从内存读这个变量,不要缓存到寄存器”。
4.2 为什么需要 sig_atomic_t?
读写一个变量在底层可能需要多条机器指令(比如 64 位值在 32 位机器上)。如果信号在这几条指令中间打断,读到的是半新半旧的值,就会出错。 sig_atomic_t 保证读写操作是原子的(一条机器指令完成,不会被打断)。
4.3 正确的声明方式
// 在主程序和信号处理函数之间共享的标志变量,必须这样声明:
volatile sig_atomic_t flag = 0;
// volatile → 每次从内存读,不用寄存器缓存
// sig_atomic_t → 读写是原子操作,不会读到"中间状态"
注意:++ 和 — 运算符不保证原子性,即使对 sig_atomic_t 类型也不行。 只能做:在处理函数里设置它,在主程序里检查它(或反过来)。
五、从信号处理函数退出的多种方式
#mermaid-svg-9UQG66NcdkTbtdfN{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9UQG66NcdkTbtdfN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9UQG66NcdkTbtdfN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9UQG66NcdkTbtdfN .error-icon{fill:#552222;}#mermaid-svg-9UQG66NcdkTbtdfN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9UQG66NcdkTbtdfN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9UQG66NcdkTbtdfN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9UQG66NcdkTbtdfN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9UQG66NcdkTbtdfN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9UQG66NcdkTbtdfN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9UQG66NcdkTbtdfN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9UQG66NcdkTbtdfN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9UQG66NcdkTbtdfN .marker.cross{stroke:#333333;}#mermaid-svg-9UQG66NcdkTbtdfN svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9UQG66NcdkTbtdfN p{margin:0;}#mermaid-svg-9UQG66NcdkTbtdfN .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-9UQG66NcdkTbtdfN .cluster-label text{fill:#333;}#mermaid-svg-9UQG66NcdkTbtdfN .cluster-label span{color:#333;}#mermaid-svg-9UQG66NcdkTbtdfN .cluster-label span p{background-color:transparent;}#mermaid-svg-9UQG66NcdkTbtdfN .label text,#mermaid-svg-9UQG66NcdkTbtdfN span{fill:#333;color:#333;}#mermaid-svg-9UQG66NcdkTbtdfN .node rect,#mermaid-svg-9UQG66NcdkTbtdfN .node circle,#mermaid-svg-9UQG66NcdkTbtdfN .node ellipse,#mermaid-svg-9UQG66NcdkTbtdfN .node polygon,#mermaid-svg-9UQG66NcdkTbtdfN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9UQG66NcdkTbtdfN .rough-node .label text,#mermaid-svg-9UQG66NcdkTbtdfN .node .label text,#mermaid-svg-9UQG66NcdkTbtdfN .image-shape .label,#mermaid-svg-9UQG66NcdkTbtdfN .icon-shape .label{text-anchor:middle;}#mermaid-svg-9UQG66NcdkTbtdfN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9UQG66NcdkTbtdfN .rough-node .label,#mermaid-svg-9UQG66NcdkTbtdfN .node .label,#mermaid-svg-9UQG66NcdkTbtdfN .image-shape .label,#mermaid-svg-9UQG66NcdkTbtdfN .icon-shape .label{text-align:center;}#mermaid-svg-9UQG66NcdkTbtdfN .node.clickable{cursor:pointer;}#mermaid-svg-9UQG66NcdkTbtdfN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9UQG66NcdkTbtdfN .arrowheadPath{fill:#333333;}#mermaid-svg-9UQG66NcdkTbtdfN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9UQG66NcdkTbtdfN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9UQG66NcdkTbtdfN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9UQG66NcdkTbtdfN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9UQG66NcdkTbtdfN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9UQG66NcdkTbtdfN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9UQG66NcdkTbtdfN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9UQG66NcdkTbtdfN .cluster text{fill:#333;}#mermaid-svg-9UQG66NcdkTbtdfN .cluster span{color:#333;}#mermaid-svg-9UQG66NcdkTbtdfN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9UQG66NcdkTbtdfN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9UQG66NcdkTbtdfN rect.text{fill:none;stroke-width:0;}#mermaid-svg-9UQG66NcdkTbtdfN .icon-shape,#mermaid-svg-9UQG66NcdkTbtdfN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9UQG66NcdkTbtdfN .icon-shape p,#mermaid-svg-9UQG66NcdkTbtdfN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9UQG66NcdkTbtdfN .icon-shape .label rect,#mermaid-svg-9UQG66NcdkTbtdfN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9UQG66NcdkTbtdfN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9UQG66NcdkTbtdfN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9UQG66NcdkTbtdfN :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
正常 return
_exit()
exit()
kill() / raise()
abort()
siglongjmp()
longjmp()
信号处理函数被调用
退出方式?
主程序从中断处继续
进程立即终止(安全,不冲洗 stdio 缓冲)
危险!exit() 不在安全列表会冲洗 stdio 缓冲,可能崩溃
发送终止信号杀死进程
发送 SIGABRT产生 core dump 并终止
非本地跳转跳回主程序某处并正确恢复信号屏蔽字
非本地跳转但不恢复信号屏蔽字(Linux/System V 行为)
六、非本地跳转:sigsetjmp / siglongjmp
6.1 问题背景
信号处理函数执行时,内核会自动把触发该信号加入屏蔽字(防止处理函数被同一信号递归打断)。处理函数正常 return 时,内核会自动恢复屏蔽字。 但如果用 longjmp() 跳出处理函数,屏蔽字就永远不会被恢复,导致该信号从此被永久屏蔽!
6.2 longjmp vs siglongjmp 对比
使用 longjmp() 跳出信号处理函数:
信号处理函数入口 → SIGINT 加入屏蔽字
longjmp() 跳走 → 屏蔽字没有被恢复
结果:SIGINT 永久被屏蔽,再按 Ctrl+C 没有反应!
使用 siglongjmp() 跳出信号处理函数:
sigsetjmp(env, 1) → 保存当时的屏蔽字到 env
信号处理函数入口 → SIGINT 加入屏蔽字
siglongjmp(env,1) → 恢复 env 中保存的屏蔽字(SIGINT 被移除)
结果:一切正常,Ctrl+C 仍然有效
6.3 API
#include <setjmp.h>
// savesigs 非零时:保存当前信号屏蔽字到 env
// 初次调用返回 0;被 siglongjmp 跳回时返回非零值
int sigsetjmp(sigjmp_buf env, int savesigs);
// 跳回 sigsetjmp 调用处,并恢复信号屏蔽字(若 savesigs 非零)
void siglongjmp(sigjmp_buf env, int val);
6.4 守卫变量(canJump)
信号可能在 sigsetjmp() 执行之前就到达。此时 env 还没有初始化,如果处理函数贸然调用 siglongjmp(),会用一个垃圾值跳转,导致崩溃。 解决方案:用一个守卫变量 canJump,只有 sigsetjmp() 成功后才设为 1,处理函数先检查这个变量。
时序图:
主程序 信号处理函数
│
├─ canJump = 0
├─ 安装信号处理函数
├─ sigsetjmp() ← 此时 env 初始化完成
├─ canJump = 1
│
│ ← 信号到来
│ canJump == 1? 是
│ siglongjmp() 安全跳转
│
├─ 跳回 sigsetjmp() 之后继续
七、备用信号栈:sigaltstack()
7.1 为什么需要备用栈?
通常信号处理函数的栈帧建立在进程的主栈上。但如果主栈已经溢出(比如无限递归耗尽栈空间),内核会发送 SIGSEGV。 问题:栈已满 → 无法为 SIGSEGV 处理函数建立栈帧 → 处理函数无法执行 → 进程直接崩溃,无法做任何清理。 解决方案:预先分配一块独立内存作为备用信号栈,SIGSEGV 处理函数在备用栈上执行,不依赖已崩溃的主栈。
7.2 使用步骤
#mermaid-svg-kanJLwoVtI9S1l1t{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kanJLwoVtI9S1l1t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kanJLwoVtI9S1l1t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kanJLwoVtI9S1l1t .error-icon{fill:#552222;}#mermaid-svg-kanJLwoVtI9S1l1t .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kanJLwoVtI9S1l1t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kanJLwoVtI9S1l1t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kanJLwoVtI9S1l1t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kanJLwoVtI9S1l1t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kanJLwoVtI9S1l1t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kanJLwoVtI9S1l1t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kanJLwoVtI9S1l1t .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kanJLwoVtI9S1l1t .marker.cross{stroke:#333333;}#mermaid-svg-kanJLwoVtI9S1l1t svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kanJLwoVtI9S1l1t p{margin:0;}#mermaid-svg-kanJLwoVtI9S1l1t .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-kanJLwoVtI9S1l1t .cluster-label text{fill:#333;}#mermaid-svg-kanJLwoVtI9S1l1t .cluster-label span{color:#333;}#mermaid-svg-kanJLwoVtI9S1l1t .cluster-label span p{background-color:transparent;}#mermaid-svg-kanJLwoVtI9S1l1t .label text,#mermaid-svg-kanJLwoVtI9S1l1t span{fill:#333;color:#333;}#mermaid-svg-kanJLwoVtI9S1l1t .node rect,#mermaid-svg-kanJLwoVtI9S1l1t .node circle,#mermaid-svg-kanJLwoVtI9S1l1t .node ellipse,#mermaid-svg-kanJLwoVtI9S1l1t .node polygon,#mermaid-svg-kanJLwoVtI9S1l1t .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kanJLwoVtI9S1l1t .rough-node .label text,#mermaid-svg-kanJLwoVtI9S1l1t .node .label text,#mermaid-svg-kanJLwoVtI9S1l1t .image-shape .label,#mermaid-svg-kanJLwoVtI9S1l1t .icon-shape .label{text-anchor:middle;}#mermaid-svg-kanJLwoVtI9S1l1t .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kanJLwoVtI9S1l1t .rough-node .label,#mermaid-svg-kanJLwoVtI9S1l1t .node .label,#mermaid-svg-kanJLwoVtI9S1l1t .image-shape .label,#mermaid-svg-kanJLwoVtI9S1l1t .icon-shape .label{text-align:center;}#mermaid-svg-kanJLwoVtI9S1l1t .node.clickable{cursor:pointer;}#mermaid-svg-kanJLwoVtI9S1l1t .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kanJLwoVtI9S1l1t .arrowheadPath{fill:#333333;}#mermaid-svg-kanJLwoVtI9S1l1t .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kanJLwoVtI9S1l1t .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kanJLwoVtI9S1l1t .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kanJLwoVtI9S1l1t .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kanJLwoVtI9S1l1t .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kanJLwoVtI9S1l1t .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kanJLwoVtI9S1l1t .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kanJLwoVtI9S1l1t .cluster text{fill:#333;}#mermaid-svg-kanJLwoVtI9S1l1t .cluster span{color:#333;}#mermaid-svg-kanJLwoVtI9S1l1t div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kanJLwoVtI9S1l1t .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kanJLwoVtI9S1l1t rect.text{fill:none;stroke-width:0;}#mermaid-svg-kanJLwoVtI9S1l1t .icon-shape,#mermaid-svg-kanJLwoVtI9S1l1t .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kanJLwoVtI9S1l1t .icon-shape p,#mermaid-svg-kanJLwoVtI9S1l1t .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kanJLwoVtI9S1l1t .icon-shape .label rect,#mermaid-svg-kanJLwoVtI9S1l1t .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kanJLwoVtI9S1l1t .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kanJLwoVtI9S1l1t .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kanJLwoVtI9S1l1t :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
1. 分配一块内存作为备用栈(malloc 或静态数组)
2. 调用 sigaltstack()告诉内核备用栈的位置
3. 安装信号处理函数时指定 SA_ONSTACK 标志
信号到来时内核自动在备用栈上建立处理函数的栈帧
7.3 stack_t 结构体
typedef struct {
void *ss_sp; // 备用栈的起始地址
int ss_flags; // SS_ONSTACK(正在使用中)或 SS_DISABLE(禁用)
size_t ss_size; // 备用栈的大小(字节)
} stack_t;
备用栈推荐大小:SIGSTKSZ(通常是 8192 字节),最小不能低于 MINSIGSTKSZ(通常是 2048 字节)。
八、SA_SIGINFO:获取信号的详细信息
8.1 普通处理函数 vs SA_SIGINFO 处理函数
普通处理函数:
void handler(int sig)
只知道信号编号,不知道信号是谁发的、为什么发的
SA_SIGINFO 处理函数:
void handler(int sig, siginfo_t *siginfo, void *ucontext)
多了 siginfo_t 结构体,包含信号的详细来源信息
8.2 siginfo_t 结构体关键字段
typedef struct {
int si_signo; // 信号编号(与 sig 参数相同)
int si_code; // 信号来源代码(见下表)
pid_t si_pid; // 发送方的进程 ID(kill()/sigqueue() 发送时有效)
uid_t si_uid; // 发送方的真实用户 ID
void *si_addr; // 导致硬件异常的地址(SIGSEGV/SIGBUS/SIGILL/SIGFPE 有效)
int si_status; // 子进程的退出状态或信号编号(SIGCHLD 有效)
// … 其他字段根据信号类型不同而不同
} siginfo_t;
8.3 si_code 常用值
| SI_USER | 用户进程通过 kill() 或 raise() 发送 |
| SI_QUEUE | 用户进程通过 sigqueue() 发送实时信号 |
| SI_KERNEL | 内核发送(如终端驱动) |
| SI_TIMER | POSIX 定时器到期 |
| SI_ASYNCIO | 异步 I/O 完成 |
| SEGV_MAPERR | 访问未映射的地址(SIGSEGV) |
| SEGV_ACCERR | 访问权限不足的已映射地址(SIGSEGV) |
| CLD_EXITED | 子进程正常退出(SIGCHLD) |
| CLD_KILLED | 子进程被信号杀死,无 core dump(SIGCHLD) |
| FPE_INTDIV | 整数除零(SIGFPE) |
8.4 安装 SA_SIGINFO 处理函数
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO; // 关键:设置此标志
act.sa_sigaction = handler; // 注意:用 sa_sigaction,不是 sa_handler
sigaction(SIGINT, &act, nullptr);
九、系统调用被信号中断:EINTR 与 SA_RESTART
9.1 问题描述
进程正在执行 read(),等待终端输入(阻塞中)
│
← 信号到来,处理函数被调用
│
处理函数执行完毕,返回
│
read() 怎么办?
→ 默认行为:返回 -1,errno = EINTR("被中断")
→ 调用者必须自己处理这个错误
9.2 三种处理方案
方案一:手动重试循环
ssize_t cnt;
// 如果 read() 因信号中断(EINTR),就重新调用
while ((cnt = read(fd, buf, BUF_SIZE)) == –1 && errno == EINTR)
continue; // 循环体为空,直接重试
if (cnt == –1)
perror("read"); // 其他错误才报错
方案二:宏封装
// 定义一个宏,自动处理 EINTR 重试
#define NO_EINTR(stmt) while ((stmt) == –1 && errno == EINTR)
// 使用方式
ssize_t cnt;
NO_EINTR(cnt = read(fd, buf, BUF_SIZE));
if (cnt == –1)
perror("read");
方案三:SA_RESTART(推荐) 安装处理函数时指定 SA_RESTART,被中断的系统调用由内核自动重启,调用者完全不用处理 EINTR。
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 系统调用被中断后自动重启
sigaction(SIGINT, &sa, nullptr);
9.3 SA_RESTART 有效 vs 无效的系统调用
| 等待子进程 | wait(), waitpid() | 是 |
| 慢速设备 I/O | read(), write(), ioctl()(终端/管道/socket) | 是 |
| 打开文件 | open()(可能阻塞的情况) | 是 |
| socket | accept(), connect(), send(), recv() 等 | 是 |
| 文件锁 | flock(), fcntl(), lockf() | 是 |
| I/O 多路复用 | select(), poll(), epoll_wait() | 否 |
| 睡眠 | sleep(), nanosleep() | 否 |
| 等待信号 | pause(), sigsuspend() | 否 |
| System V IPC | semop(), msgrcv(), msgsnd() | 否 |
十、完整示例代码(C++)
示例一:演示不可重入的危险(crypt 版)
// nonreentrant_demo.cpp
// 编译: g++ -o nonreentrant_demo nonreentrant_demo.cpp -lcrypt
// 运行: ./nonreentrant_demo abc def
// 运行后反复按 Ctrl+C,观察不一致输出
#define _XOPEN_SOURCE 600
#include <unistd.h> // crypt(), sleep()
#include <csignal>
#include <cstring>
#include <cstdio>
#include <cstdlib>
// 全局变量,供信号处理函数使用
static const char* str2 = nullptr;
static int handled = 0;
// 信号处理函数:调用 crypt()(不可重入函数)
static void handler(int sig) {
// 这里调用 crypt() 会覆盖主程序 crypt() 使用的同一个静态缓冲区
crypt(str2, "xx");
handled++;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "用法: %s str1 str2\\n", argv[0]);
return EXIT_FAILURE;
}
str2 = argv[2];
// 第一步:用 argv[1] 加密,把结果复制到独立缓冲区
// strdup 内部调用 malloc,动态分配内存,所以不会被覆盖
char* cr1 = strdup(crypt(argv[1], "xx"));
if (cr1 == nullptr) {
perror("strdup");
return EXIT_FAILURE;
}
// 为 SIGINT 安装处理函数
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if (sigaction(SIGINT, &sa, nullptr) == –1) {
perror("sigaction");
return EXIT_FAILURE;
}
printf("开始循环,请反复按 Ctrl+C,按 Ctrl+\\\\ 退出\\n");
int callNum = 0, mismatch = 0;
for (;;) {
callNum++;
// 每次调用 crypt(argv[1]) 应该与 cr1 相同
// 但如果处理函数在此期间被调用,就会覆盖 crypt() 的静态缓冲区
if (strcmp(crypt(argv[1], "xx"), cr1) != 0) {
mismatch++;
printf("第 %d 次调用发现不一致 (mismatch=%d, handled=%d)\\n",
callNum, mismatch, handled);
}
}
}
示例二:正确设计 — 标志位模式
// flag_pattern_demo.cpp
// 编译: g++ -o flag_pattern_demo flag_pattern_demo.cpp
// 运行后按 Ctrl+C,主循环检测到标志后优雅退出
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <cerrno>
// 正确的共享标志变量声明方式
// volatile: 禁止编译器缓存到寄存器
// sig_atomic_t: 读写是原子操作
static volatile sig_atomic_t gotSigint = 0;
// 信号处理函数:只做一件事——设置标志
// 不调用任何不安全的函数
static void sigintHandler(int sig) {
gotSigint = 1;
// 注意:不在这里做复杂操作,也不调用 printf 等不安全函数
}
int main() {
struct sigaction sa;
sa.sa_handler = sigintHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, nullptr) == –1) {
perror("sigaction");
return EXIT_FAILURE;
}
printf("程序运行中,按 Ctrl+C 优雅退出…\\n");
int count = 0;
// 主循环:定期检查标志
while (!gotSigint) {
// 实际业务逻辑
count++;
// 用 sleep 模拟工作,SA_RESTART 未设置,所以 SIGINT 会打断 sleep
// 这里我们检查 EINTR 并继续(因为我们要靠 gotSigint 来退出)
int ret = sleep(1);
(void)ret; // 被信号中断时 sleep 返回剩余秒数,此处忽略
// 检查标志(也可以在此处处理业务)
if (!gotSigint) {
printf("工作循环第 %d 次\\n", count);
}
}
// 收到 SIGINT 后,在主程序中安全地做清理工作
printf("\\n收到 SIGINT,正在清理并退出…\\n");
printf("共执行了 %d 次循环\\n", count);
return EXIT_SUCCESS;
}
示例三:sigsetjmp/siglongjmp 非本地跳转
// siglongjmp_demo.cpp
// 编译: g++ -D_GNU_SOURCE -o siglongjmp_demo siglongjmp_demo.cpp
// 运行后按 Ctrl+C,观察信号屏蔽字的恢复情况
#define _GNU_SOURCE
#include <csignal>
#include <csetjmp>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <cstring>
// 守卫变量:sigsetjmp 执行完才置 1
// 防止 env 未初始化时被 siglongjmp 使用
static volatile sig_atomic_t canJump = 0;
// 保存跳转目标(包含信号屏蔽字信息)
static sigjmp_buf jumpEnv;
// 打印当前信号屏蔽字(教学用,实际不应在处理函数中调用)
static void printMask(const char* msg) {
sigset_t mask;
sigprocmask(SIG_BLOCK, nullptr, &mask);
printf("%s\\n", msg);
// 检查常见信号是否在屏蔽字中
if (sigismember(&mask, SIGINT))
printf(" SIGINT 在屏蔽字中(被屏蔽)\\n");
else
printf(" SIGINT 不在屏蔽字中(未屏蔽)\\n");
}
static void sigintHandler(int sig) {
// UNSAFE: printf 不是异步信号安全函数,仅用于演示
printf("\\n— 进入信号处理函数 —\\n");
printMask("处理函数内的屏蔽字:"); // 此时 SIGINT 被自动加入屏蔽字
if (!canJump) {
// env 还没准备好,直接返回
printf("env 未初始化,直接返回\\n");
return;
}
// 跳回 sigsetjmp 调用处,同时恢复屏蔽字(因为 savesigs=1)
siglongjmp(jumpEnv, 1);
}
int main() {
struct sigaction sa;
sa.sa_handler = sigintHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, nullptr);
printMask("程序启动时的屏蔽字:");
// sigsetjmp 第一次调用返回 0
// 被 siglongjmp 跳回后返回非零值(这里是 1)
if (sigsetjmp(jumpEnv, 1) == 0) {
// 第一次执行:sigsetjmp 保存了当前屏蔽字(SIGINT 未屏蔽)
canJump = 1; // env 已初始化,处理函数可以安全跳转
printf("sigsetjmp() 已设置,等待 Ctrl+C…\\n");
} else {
// 从 siglongjmp 跳回:屏蔽字已恢复(SIGINT 重新未屏蔽)
printf("— 从处理函数跳回主程序 —\\n");
printMask("跳回后的屏蔽字:");
printf("Ctrl+C 仍然有效(信号屏蔽字已正确恢复)\\n\\n");
printf("再次等待 Ctrl+C…\\n");
}
// 无限等待信号
for (;;) {
pause(); // 等待任意信号
}
}
示例四:备用信号栈处理栈溢出
// sigaltstack_demo.cpp
// 编译: g++ -o sigaltstack_demo sigaltstack_demo.cpp
// 运行: ulimit -s unlimited && ./sigaltstack_demo
// 演示栈溢出时在备用栈上捕获 SIGSEGV
#define _GNU_SOURCE
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
// SIGSEGV 处理函数(运行在备用栈上)
static void sigsegvHandler(int sig) {
// 声明一个局部变量,用其地址观察当前栈位置
int localVar;
// UNSAFE: printf 不安全,仅用于演示
printf("\\n捕获到信号 %d (段错误)\\n", sig);
printf("处理函数的栈顶附近地址: %p\\n", (void*)&localVar);
printf("(这是备用信号栈,与已溢出的主栈无关)\\n");
fflush(nullptr);
// 必须用 _exit() 而不是 exit()
// exit() 在安全函数列表之外,且可能尝试冲洗已损坏的 stdio 缓冲区
_exit(EXIT_FAILURE);
}
// 无限递归,故意让主栈溢出
static void overflowStack(int depth) {
// 每个栈帧分配 10000 字节,加速栈溢出
char bigArray[10000];
bigArray[0] = (char)depth; // 防止编译器优化掉这个数组
printf("递归深度 %4d, 栈帧地址: %p\\n", depth, (void*)bigArray);
overflowStack(depth + 1); // 无限递归
}
int main() {
// 步骤1:分配备用信号栈内存
stack_t altStack;
altStack.ss_sp = malloc(SIGSTKSZ); // SIGSTKSZ 通常是 8192 字节
if (altStack.ss_sp == nullptr) {
perror("malloc");
return EXIT_FAILURE;
}
altStack.ss_size = SIGSTKSZ;
altStack.ss_flags = 0; // 不设置任何标志
// 步骤2:告知内核备用栈的位置
if (sigaltstack(&altStack, nullptr) == –1) {
perror("sigaltstack");
return EXIT_FAILURE;
}
printf("备用信号栈已设置: %p – %p\\n",
altStack.ss_sp,
(char*)altStack.ss_sp + SIGSTKSZ – 1);
// 步骤3:安装 SIGSEGV 处理函数,指定使用备用栈
struct sigaction sa;
sa.sa_handler = sigsegvHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK; // 关键:在备用栈上运行处理函数
if (sigaction(SIGSEGV, &sa, nullptr) == –1) {
perror("sigaction");
return EXIT_FAILURE;
}
int localVar;
printf("主栈起始地址附近: %p\\n\\n", (void*)&localVar);
printf("开始无限递归(将导致栈溢出)…\\n\\n");
// 触发栈溢出
overflowStack(1);
return EXIT_SUCCESS; // 不会到达这里
}
示例五:SA_SIGINFO 获取信号详细信息
// sa_siginfo_demo.cpp
// 编译: g++ -o sa_siginfo_demo sa_siginfo_demo.cpp
// 在另一个终端运行: kill -SIGUSR1 <本进程PID>
// 观察发送方的进程 ID 和用户 ID 信息
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
// SA_SIGINFO 处理函数:有三个参数
// sig : 信号编号
// siginfo : 包含信号详细信息的结构体
// ucontext: 进程被中断前的上下文(通常不需要)
static void handler(int sig, siginfo_t* siginfo, void* ucontext) {
// UNSAFE: printf 不安全,仅用于演示
printf("\\n收到信号: %d\\n", sig);
printf("si_code: %d ", siginfo->si_code);
// 根据 si_code 解释信号来源
switch (siginfo->si_code) {
case SI_USER:
printf("(来自用户进程 kill() 或 raise())\\n");
printf("发送方 PID: %ld\\n", (long)siginfo->si_pid);
printf("发送方 UID: %ld\\n", (long)siginfo->si_uid);
break;
case SI_KERNEL:
printf("(来自内核)\\n");
break;
case SI_QUEUE:
printf("(来自 sigqueue())\\n");
printf("发送方 PID: %ld\\n", (long)siginfo->si_pid);
break;
default:
printf("(其他来源)\\n");
break;
}
// 对于 SIGSEGV:打印导致错误的内存地址
if (sig == SIGSEGV) {
printf("导致错误的地址: %p\\n", siginfo->si_addr);
}
}
int main() {
struct sigaction sa;
sigemptyset(&sa.sa_mask);
// 关键:必须设置 SA_SIGINFO,并使用 sa_sigaction(不是 sa_handler)
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = handler; // 注意:用 sa_sigaction 字段
if (sigaction(SIGUSR1, &sa, nullptr) == –1) {
perror("sigaction");
return EXIT_FAILURE;
}
if (sigaction(SIGUSR2, &sa, nullptr) == –1) {
perror("sigaction");
return EXIT_FAILURE;
}
printf("本进程 PID: %ld\\n", (long)getpid());
printf("等待信号中…\\n");
printf("在另一个终端运行: kill -SIGUSR1 %ld\\n", (long)getpid());
for (;;) {
pause(); // 等待信号
}
}
示例六:SA_RESTART 自动重启系统调用
// sa_restart_demo.cpp
// 编译: g++ -o sa_restart_demo sa_restart_demo.cpp
// 运行后按 Ctrl+C,观察 read() 是否被自动重启(等待继续输入)
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <cerrno>
#include <cstring>
static void sigintHandler(int sig) {
// UNSAFE: 仅用于演示
const char msg[] = "\\n[处理函数] 收到 SIGINT,read() 将自动重启\\n";
// 用 write() 而不是 printf()(write 是异步信号安全的)
write(STDOUT_FILENO, msg, sizeof(msg) – 1);
}
int main() {
struct sigaction sa;
sa.sa_handler = sigintHandler;
sigemptyset(&sa.sa_mask);
// SA_RESTART: 被信号中断的系统调用自动重启,无需手动处理 EINTR
sa.sa_flags = SA_RESTART;
if (sigaction(SIGINT, &sa, nullptr) == –1) {
perror("sigaction");
return EXIT_FAILURE;
}
char buf[256];
printf("请输入内容(按 Ctrl+C 测试 SA_RESTART,按 Ctrl+D 结束):\\n");
// 有了 SA_RESTART,不需要处理 EINTR,read() 会自动重启
ssize_t numRead = read(STDIN_FILENO, buf, sizeof(buf) – 1);
if (numRead == –1) {
// 注意:即使设了 SA_RESTART,某些系统调用仍会返回 EINTR
// 如果不设 SA_RESTART,这里几乎必然看到 EINTR
printf("read() 失败: %s\\n", strerror(errno));
return EXIT_FAILURE;
}
if (numRead == 0) {
printf("读到 EOF\\n");
} else {
buf[numRead] = '\\0';
printf("读到 %zd 字节: %s", numRead, buf);
}
return EXIT_SUCCESS;
}
十一、知识点总结脑图
#mermaid-svg-SUKay5SWaFqQd7Rp{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SUKay5SWaFqQd7Rp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SUKay5SWaFqQd7Rp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SUKay5SWaFqQd7Rp .error-icon{fill:#552222;}#mermaid-svg-SUKay5SWaFqQd7Rp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SUKay5SWaFqQd7Rp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SUKay5SWaFqQd7Rp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SUKay5SWaFqQd7Rp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SUKay5SWaFqQd7Rp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SUKay5SWaFqQd7Rp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SUKay5SWaFqQd7Rp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SUKay5SWaFqQd7Rp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SUKay5SWaFqQd7Rp .marker.cross{stroke:#333333;}#mermaid-svg-SUKay5SWaFqQd7Rp svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SUKay5SWaFqQd7Rp p{margin:0;}#mermaid-svg-SUKay5SWaFqQd7Rp .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-SUKay5SWaFqQd7Rp .cluster-label text{fill:#333;}#mermaid-svg-SUKay5SWaFqQd7Rp .cluster-label span{color:#333;}#mermaid-svg-SUKay5SWaFqQd7Rp .cluster-label span p{background-color:transparent;}#mermaid-svg-SUKay5SWaFqQd7Rp .label text,#mermaid-svg-SUKay5SWaFqQd7Rp span{fill:#333;color:#333;}#mermaid-svg-SUKay5SWaFqQd7Rp .node rect,#mermaid-svg-SUKay5SWaFqQd7Rp .node circle,#mermaid-svg-SUKay5SWaFqQd7Rp .node ellipse,#mermaid-svg-SUKay5SWaFqQd7Rp .node polygon,#mermaid-svg-SUKay5SWaFqQd7Rp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SUKay5SWaFqQd7Rp .rough-node .label text,#mermaid-svg-SUKay5SWaFqQd7Rp .node .label text,#mermaid-svg-SUKay5SWaFqQd7Rp .image-shape .label,#mermaid-svg-SUKay5SWaFqQd7Rp .icon-shape .label{text-anchor:middle;}#mermaid-svg-SUKay5SWaFqQd7Rp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SUKay5SWaFqQd7Rp .rough-node .label,#mermaid-svg-SUKay5SWaFqQd7Rp .node .label,#mermaid-svg-SUKay5SWaFqQd7Rp .image-shape .label,#mermaid-svg-SUKay5SWaFqQd7Rp .icon-shape .label{text-align:center;}#mermaid-svg-SUKay5SWaFqQd7Rp .node.clickable{cursor:pointer;}#mermaid-svg-SUKay5SWaFqQd7Rp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SUKay5SWaFqQd7Rp .arrowheadPath{fill:#333333;}#mermaid-svg-SUKay5SWaFqQd7Rp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SUKay5SWaFqQd7Rp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SUKay5SWaFqQd7Rp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SUKay5SWaFqQd7Rp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SUKay5SWaFqQd7Rp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SUKay5SWaFqQd7Rp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SUKay5SWaFqQd7Rp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SUKay5SWaFqQd7Rp .cluster text{fill:#333;}#mermaid-svg-SUKay5SWaFqQd7Rp .cluster span{color:#333;}#mermaid-svg-SUKay5SWaFqQd7Rp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SUKay5SWaFqQd7Rp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SUKay5SWaFqQd7Rp rect.text{fill:none;stroke-width:0;}#mermaid-svg-SUKay5SWaFqQd7Rp .icon-shape,#mermaid-svg-SUKay5SWaFqQd7Rp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SUKay5SWaFqQd7Rp .icon-shape p,#mermaid-svg-SUKay5SWaFqQd7Rp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SUKay5SWaFqQd7Rp .icon-shape .label rect,#mermaid-svg-SUKay5SWaFqQd7Rp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SUKay5SWaFqQd7Rp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SUKay5SWaFqQd7Rp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SUKay5SWaFqQd7Rp :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
信号处理函数设计
安全性问题
退出方式
系统调用中断
高级特性
可重入 vs 不可重入(避免全局/静态状态)
只调用异步信号安全函数(见 Table 21-1)
保存/恢复 errno
全局标志用volatile sig_atomic_t
正常 return(最常见)
_exit()(立即终止,安全)
abort()(产生 core dump)
siglongjmp()(非本地跳转,恢复屏蔽字)
默认: 返回 EINTR
手动重试循环
SA_RESTART内核自动重启
sigaltstack()备用栈处理栈溢出
SA_SIGINFO获取信号详细信息
十二、常见陷阱速查
| 在处理函数中调用 printf() | 用 write() 代替,或只设标志在主程序打印 |
| 在处理函数中调用 malloc()/free() | 不可以,改用预分配的静态缓冲区 |
| 用 longjmp() 跳出处理函数 | 改用 siglongjmp(),正确恢复信号屏蔽字 |
| 共享变量不加 volatile | 必须加,防止编译器缓存到寄存器 |
| 忘记保存/恢复 errno | 进入处理函数先保存,退出前恢复 |
| 用 exit() 终止进程 | 用 _exit(),exit() 不在安全函数列表 |
| 在 sigsetjmp() 之前信号就到来 | 使用守卫变量 canJump,检查后再跳转 |
| 对所有阻塞调用都依赖 SA_RESTART | select()/poll()/sleep() 等不会自动重启 |
Linux 信号高级特性 — 详细中文解析
一、核心转储(Core Dump)
1.1 什么是 core dump?
当某些信号(如 SIGSEGV、SIGQUIT)导致进程终止时,内核会把进程当时的内存镜像写入一个文件,这就是 core dump(核心转储)。调试器(如 gdb)可以加载这个文件,查看程序崩溃时的状态:变量的值、调用栈、寄存器内容等。
触发方式示例:
$ ulimit -c unlimited # 取消 core 文件大小限制
$ cat /proc/sys/kernel/core_pattern # 查看 core 文件会写到哪里
# 临时改回直接生成文件(当前目录)
$ sudo sysctl -w kernel.core_pattern=core
$ sleep 30
按 Ctrl+\\ # 发送 SIGQUIT
Quit (core dumped) # shell 提示发生了 core dump
$ ls -l core # 默认文件名为 "core"
-rw——- 1 user users 57344 Nov 30 13:39 core
1.2 不会产生 core dump 的情况
以下情况下 core dump 不会被生成:
不产生 core dump 的情况:
├── 进程对目标目录无写权限
├── 同名文件已存在且不可写,或不是普通文件
├── 同名文件存在但有多个硬链接
├── 目标目录不存在
├── RLIMIT_CORE 资源限制为 0(ulimit -c 0)
├── RLIMIT_FSIZE 文件大小限制为 0
├── 可执行文件没有读权限(防止逆向)
├── 文件系统只读、已满或 inode 耗尽
└── set-user-ID 程序被非属主用户执行
(防止恶意用户从 core 中提取敏感信息如密码)
1.3 自定义 core dump 文件名
Linux 通过 /proc/sys/kernel/core_pattern 控制 core 文件的命名规则:
# 查看当前规则
cat /proc/sys/kernel/core_pattern
# 默认输出: core
# 自定义示例:包含进程ID和信号编号
echo "core.%e.%p.%s" > /proc/sys/kernel/core_pattern
| %e | 可执行文件名(不含路径) |
| %p | 进程 ID |
| %s | 导致终止的信号编号 |
| %u | 真实用户 ID |
| %g | 真实组 ID |
| %h | 主机名 |
| %t | 转储时间(Unix 时间戳) |
| %c | core 文件大小软限制(字节) |
| %% | 字面量 % |
内核 2.6.19 起:若 core_pattern 以 | 开头,则其后内容被当作程序执行,core 数据写入该程序的标准输入(而非文件),例如 |/usr/lib/systemd/systemd-coredump。
二、特殊信号的投递规则
2.1 SIGKILL 和 SIGSTOP
这两个信号不可屏蔽、不可忽略、不可捕获,设计上如此,以保证系统管理员总能控制失控进程。尝试用 signal() 或 sigaction() 改变它们的处置会返回错误。
2.2 SIGCONT 与停止信号的特殊互动
#mermaid-svg-AIBSwD5ZgpyOjIfJ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .error-icon{fill:#552222;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .marker.cross{stroke:#333333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AIBSwD5ZgpyOjIfJ p{margin:0;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .cluster-label text{fill:#333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .cluster-label span{color:#333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .cluster-label span p{background-color:transparent;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .label text,#mermaid-svg-AIBSwD5ZgpyOjIfJ span{fill:#333;color:#333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .node rect,#mermaid-svg-AIBSwD5ZgpyOjIfJ .node circle,#mermaid-svg-AIBSwD5ZgpyOjIfJ .node ellipse,#mermaid-svg-AIBSwD5ZgpyOjIfJ .node polygon,#mermaid-svg-AIBSwD5ZgpyOjIfJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .rough-node .label text,#mermaid-svg-AIBSwD5ZgpyOjIfJ .node .label text,#mermaid-svg-AIBSwD5ZgpyOjIfJ .image-shape .label,#mermaid-svg-AIBSwD5ZgpyOjIfJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .rough-node .label,#mermaid-svg-AIBSwD5ZgpyOjIfJ .node .label,#mermaid-svg-AIBSwD5ZgpyOjIfJ .image-shape .label,#mermaid-svg-AIBSwD5ZgpyOjIfJ .icon-shape .label{text-align:center;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .node.clickable{cursor:pointer;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .arrowheadPath{fill:#333333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AIBSwD5ZgpyOjIfJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AIBSwD5ZgpyOjIfJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AIBSwD5ZgpyOjIfJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .cluster text{fill:#333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .cluster span{color:#333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AIBSwD5ZgpyOjIfJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .icon-shape,#mermaid-svg-AIBSwD5ZgpyOjIfJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .icon-shape p,#mermaid-svg-AIBSwD5ZgpyOjIfJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .icon-shape .label rect,#mermaid-svg-AIBSwD5ZgpyOjIfJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AIBSwD5ZgpyOjIfJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AIBSwD5ZgpyOjIfJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AIBSwD5ZgpyOjIfJ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
进程处于停止状态
收到 SIGCONT?
立即恢复执行(即使 SIGCONT 被屏蔽或忽略)
若 SIGCONT 被屏蔽:恢复后再投递处理函数
收到其他信号?
信号保持待决直到收到 SIGCONT 恢复后才投递
例外:SIGKILL 立即生效
SIGCONT 与停止信号的消抵规则:
- 投递 SIGCONT 时:丢弃所有待决的停止信号(SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU)
- 投递任何停止信号时:丢弃待决的 SIGCONT 这样做是为了防止"先发的停止信号在 SIGCONT 之后才生效"导致逻辑混乱。
三、进程睡眠状态与信号
Linux 内核区分两种(以及一种新增的)睡眠状态:
| TASK_INTERRUPTIBLE | S | 等待可中断的事件(终端输入、管道数据等) | 能,信号会唤醒进程 |
| TASK_UNINTERRUPTIBLE | D | 等待特殊事件(磁盘 I/O 完成等) | 不能,信号等退出此状态后才投递 |
| TASK_KILLABLE(2.6.25+) | D | 类似不可中断,但致命信号可唤醒 | 仅致命信号可以 |
D 状态的进程如果长时间"挂住"(NFS 问题、内核 bug),即使 SIGKILL 也无效,只能重启系统。TASK_KILLABLE 状态就是为了缓解这个问题。
四、硬件生成信号的特殊性
SIGBUS、SIGFPE、SIGILL、SIGSEGV 可由硬件异常触发。对这类信号,以下行为是未定义的或有害的:
硬件生成信号的三种错误处理方式:
1. 处理函数正常 return:
进程回到出错的那条指令 → 再次触发同一信号 → 无限循环调用处理函数
2. 忽略信号(SIG_IGN):
内核会强制投递,忽略设置无效
程序无法继续正常执行
3. 屏蔽信号:
Linux 2.6+ 中:进程被该信号立即杀死(即使安装了处理函数)
目的:避免死锁和隐藏 bug
正确的做法:
硬件生成信号的正确处理:
├── 接受默认动作(进程终止 + core dump)
├── 处理函数中调用 _exit() 终止进程
└── 处理函数中调用 siglongjmp() 跳到安全位置
(不能正常 return!)
五、同步信号 vs 异步信号
| 异步信号 | 内核或其他进程发送 | 不可预测(下次从内核态切回用户态时) | 不可预测 |
| 同步信号 | 进程自身执行触发(硬件异常、raise()) | 立即投递 | 完全可预测 |
注意:同步/异步是信号产生方式的属性,不是信号本身的属性。同一个信号(如 SIGUSR1)可以被同步(raise())或异步(另一个进程 kill())地产生。 异步信号的投递时机:下次从内核态切回用户态时,包括:
- 进程重新被调度运行(时间片开始)
- 系统调用完成时
六、多个待决信号的投递顺序
当 sigprocmask() 解除屏蔽,多个待决信号同时"放开"时:
- Linux 当前实现:按信号编号升序投递(编号小的先投递)
- SUSv3 规定:多个标准信号的投递顺序由实现决定,不可依赖
- 实时信号:保证编号小的先投递(有优先级) 当多个信号同时解除屏蔽,且在处理第一个信号的处理函数执行期间发生内核态→用户态切换,第二个信号的处理函数会打断第一个处理函数的执行:
时序示意:
主程序
│
├─ 解除屏蔽(SIGINT + SIGQUIT 同时待决)
│
↓ 内核调用 SIGINT 处理函数(编号2,先)
│
SIGINT 处理函数执行中…
│
├─ 做了系统调用(发生内核→用户切换)
│
↓ 内核调用 SIGQUIT 处理函数(编号3,后)
│
SIGQUIT 处理函数执行并返回
│
SIGINT 处理函数继续并返回
│
主程序继续
七、实时信号(Realtime Signals)
7.1 实时信号 vs 标准信号
| 编号范围 | 1~31 | 32~63(Linux),用 SIGRTMIN+n 引用 |
| 是否排队 | 否(同种信号只记录一次) | 是(多次发送多次投递) |
| 投递顺序 | 未定义 | 编号小的先投递;同编号按发送顺序 |
| 携带数据 | 否 | 是(整数或指针) |
| 应用定义 | 仅 SIGUSR1/2 | 整个范围可自由使用 |
不要硬编码实时信号的数字值(如 44),应用 SIGRTMIN+n 表示,因为不同实现的范围不同。
7.2 发送实时信号:sigqueue()
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
// 成功返回 0,失败返回 -1
// 队列满时返回 -1,errno=EAGAIN(需要稍后重试)
union sigval {
int sival_int; // 整数数据
void *sival_ptr; // 指针数据(跨进程几乎无意义)
};
权限要求与 kill() 相同。pid 不能为负(不能发给进程组)。
7.3 接收实时信号
接收方必须用 SA_SIGINFO 标志安装处理函数,才能拿到附带数据:
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_sigaction = handler; // 三参数处理函数
act.sa_flags = SA_RESTART | SA_SIGINFO; // SA_SIGINFO 是关键
sigaction(SIGRTMIN + 5, &act, nullptr);
处理函数中 siginfo_t 的关键字段(实时信号):
| si_signo | 信号编号 |
| si_code | SI_QUEUE(sigqueue 发送)或 SI_USER(kill 发送) |
| si_value.sival_int | sigqueue 发送的整数数据 |
| si_pid | 发送方进程 ID |
| si_uid | 发送方真实用户 ID |
7.4 实时信号排队上限
Linux 2.6.8+ 使用 RLIMIT_SIGPENDING 资源限制,控制每个真实用户 ID 下所有进程可排队的信号总数。队列满时 sigqueue() 返回 EAGAIN。
八、sigsuspend():原子解除屏蔽并等待
8.1 为什么需要 sigsuspend?
朴素写法存在竞态条件(race condition):
错误的写法(有 bug):
sigprocmask(SIG_SETMASK, &prevMask, NULL); // 解除屏蔽
↑
如果信号在这里到来…
处理函数被调用并返回
↓
pause(); // 等待——但信号已经错过了!永久阻塞!
问题:解除屏蔽和 pause() 之间有一个窗口,如果信号恰好在这个窗口里到来,pause() 就会永远等待"下一次"信号。
8.2 sigsuspend() 的原子性
sigsuspend() 把"替换屏蔽字 + 挂起等待"做成原子操作:
#include <signal.h>
int sigsuspend(const sigset_t *mask);
// 正常情况下返回 -1,errno=EINTR
等价于以下三步的原子版本:
sigprocmask(SIG_SETMASK, mask, &prevMask)
→
原子
pause()
→
信号处理函数返回
sigprocmask(SIG_SETMASK, &prevMask, NULL)
\\text{sigprocmask(SIG\\_SETMASK, mask, \\&prevMask)} \\xrightarrow{\\text{原子}} \\text{pause()} \\xrightarrow{\\text{信号处理函数返回}} \\text{sigprocmask(SIG\\_SETMASK, \\&prevMask, NULL)}
sigprocmask(SIG_SETMASK, mask, &prevMask)原子
pause()信号处理函数返回
sigprocmask(SIG_SETMASK, &prevMask, NULL) 即:用 mask 临时替换当前屏蔽字 → 挂起等待信号 → 处理函数返回后自动恢复原屏蔽字。 正确的使用模式:
关键区 → 保持信号屏蔽(阻塞)
↓
sigsuspend(原来的屏蔽字) ← 原子解除屏蔽并等待
↓(处理函数返回后)
屏蔽字自动恢复为阻塞状态 ← 回到关键区保护
九、sigwaitinfo():同步等待信号
9.1 与 sigsuspend 的区别
sigsuspend() 需要编写处理函数,sigwaitinfo() 则完全不需要处理函数,直接同步地"取走"一个信号:
#define _POSIX_C_SOURCE 199309
#include <signal.h>
int sigwaitinfo(const sigset_t *set, siginfo_t *info);
// 成功返回信号编号,失败返回 -1
int sigtimedwait(const sigset_t *set, siginfo_t *info,
const struct timespec *timeout);
// timeout 两字段均为 0:立即轮询(不阻塞)
// timeout 为 NULL:等同于 sigwaitinfo()
// 超时返回 -1,errno=EAGAIN
使用前提:必须先用 sigprocmask() 屏蔽 set 中的信号,否则信号可能在 sigwaitinfo() 调用前就被按默认方式处理了。
9.2 sigwaitinfo 的优势
sigsuspend + 处理函数 vs sigwaitinfo
─────────────────────────────────────
需要编写处理函数 不需要处理函数
处理函数在信号栈执行 直接在主程序流中处理
速度较慢 速度更快(无函数调用开销)
受异步安全函数限制 无此限制
十、signalfd():通过文件描述符读取信号
10.1 概念
Linux 2.6.22 引入的非标准系统调用。把信号"变成"可用 read() 读取的文件描述符,从而可以用 select()、poll()、epoll 来监控信号,与 I/O 事件统一处理。
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
// fd=-1: 创建新 fd
// fd=已有: 更新 mask
// flags: SFD_CLOEXEC, SFD_NONBLOCK(2.6.27+)
// 成功返回文件描述符,失败返回 -1
读取到的数据格式:signalfd_siginfo 结构体,字段与 siginfo_t 类似(字段名以 ssi_ 开头)。
10.2 使用步骤
#mermaid-svg-UmpXqhC343a1TRER{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UmpXqhC343a1TRER .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UmpXqhC343a1TRER .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UmpXqhC343a1TRER .error-icon{fill:#552222;}#mermaid-svg-UmpXqhC343a1TRER .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UmpXqhC343a1TRER .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UmpXqhC343a1TRER .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UmpXqhC343a1TRER .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UmpXqhC343a1TRER .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UmpXqhC343a1TRER .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UmpXqhC343a1TRER .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UmpXqhC343a1TRER .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UmpXqhC343a1TRER .marker.cross{stroke:#333333;}#mermaid-svg-UmpXqhC343a1TRER svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UmpXqhC343a1TRER p{margin:0;}#mermaid-svg-UmpXqhC343a1TRER .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-UmpXqhC343a1TRER .cluster-label text{fill:#333;}#mermaid-svg-UmpXqhC343a1TRER .cluster-label span{color:#333;}#mermaid-svg-UmpXqhC343a1TRER .cluster-label span p{background-color:transparent;}#mermaid-svg-UmpXqhC343a1TRER .label text,#mermaid-svg-UmpXqhC343a1TRER span{fill:#333;color:#333;}#mermaid-svg-UmpXqhC343a1TRER .node rect,#mermaid-svg-UmpXqhC343a1TRER .node circle,#mermaid-svg-UmpXqhC343a1TRER .node ellipse,#mermaid-svg-UmpXqhC343a1TRER .node polygon,#mermaid-svg-UmpXqhC343a1TRER .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UmpXqhC343a1TRER .rough-node .label text,#mermaid-svg-UmpXqhC343a1TRER .node .label text,#mermaid-svg-UmpXqhC343a1TRER .image-shape .label,#mermaid-svg-UmpXqhC343a1TRER .icon-shape .label{text-anchor:middle;}#mermaid-svg-UmpXqhC343a1TRER .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UmpXqhC343a1TRER .rough-node .label,#mermaid-svg-UmpXqhC343a1TRER .node .label,#mermaid-svg-UmpXqhC343a1TRER .image-shape .label,#mermaid-svg-UmpXqhC343a1TRER .icon-shape .label{text-align:center;}#mermaid-svg-UmpXqhC343a1TRER .node.clickable{cursor:pointer;}#mermaid-svg-UmpXqhC343a1TRER .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UmpXqhC343a1TRER .arrowheadPath{fill:#333333;}#mermaid-svg-UmpXqhC343a1TRER .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UmpXqhC343a1TRER .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UmpXqhC343a1TRER .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UmpXqhC343a1TRER .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UmpXqhC343a1TRER .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UmpXqhC343a1TRER .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UmpXqhC343a1TRER .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UmpXqhC343a1TRER .cluster text{fill:#333;}#mermaid-svg-UmpXqhC343a1TRER .cluster span{color:#333;}#mermaid-svg-UmpXqhC343a1TRER div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UmpXqhC343a1TRER .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UmpXqhC343a1TRER rect.text{fill:none;stroke-width:0;}#mermaid-svg-UmpXqhC343a1TRER .icon-shape,#mermaid-svg-UmpXqhC343a1TRER .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UmpXqhC343a1TRER .icon-shape p,#mermaid-svg-UmpXqhC343a1TRER .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UmpXqhC343a1TRER .icon-shape .label rect,#mermaid-svg-UmpXqhC343a1TRER .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UmpXqhC343a1TRER .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UmpXqhC343a1TRER .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UmpXqhC343a1TRER :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
1. 构建信号集 mask(要监控的信号)
2. sigprocmask()屏蔽 mask 中的信号(防止被默认处理)
3. signalfd(-1, mask, 0)创建 signalfd 文件描述符
4. read(sfd, &fdsi, sizeof(fdsi))阻塞读取,直到信号到来可读多个 signalfd_siginfo
5. 处理信号信息close(sfd) 释放资源
10.3 signalfd 的优势场景
signalfd 最大的优势:可以把信号监控统一进 I/O 多路复用:
传统方式(自管道技巧):
信号处理函数 → write 一个字节到管道写端
主循环 select/poll 监控管道读端
signalfd 方式:
signalfd() 创建 fd
主循环 select/poll 直接监控 signalfd fd
(更简洁,无需处理函数)
十一、信号与 IPC 的比较
信号作为 IPC 机制的局限性:
| 异步性 | 需要处理竞态条件、可重入性、全局变量保护 |
| 不排队 | 标准信号多次发送只投递一次,可能丢失 |
| 数据量小 | 标准信号无数据;实时信号只有一个整数/指针 |
| 带宽低 | 相比管道、消息队列等效率低得多 |
结论:信号不适合作为 IPC 机制,更适合用于:进程同步、事件通知、作业控制、定时器到期通知。
十二、signal() 的可移植性问题与实现
12.1 旧版 signal() 的"不可靠"语义
早期 UNIX 的 signal() 存在两个问题(类似 SA_RESETHAND + SA_NODEFER):
12.2 用 sigaction() 实现 signal()
// signal_impl.cpp
// 展示如何用 sigaction 实现 signal(),明确语义
#include <csignal>
typedef void (*sighandler_t)(int);
// 现代语义(默认):SA_RESTART,处理期间屏蔽同一信号,不自动重置
// 旧语义(-DOLD_SIGNAL):SA_RESETHAND | SA_NODEFER,不重启系统调用
sighandler_t my_signal(int sig, sighandler_t handler) {
struct sigaction newDisp, prevDisp;
newDisp.sa_handler = handler;
sigemptyset(&newDisp.sa_mask);
#ifdef OLD_SIGNAL
// 旧(不可靠)语义:
// SA_RESETHAND: 处理函数调用后自动重置为 SIG_DFL
// SA_NODEFER: 处理函数执行期间不屏蔽本信号(可递归)
newDisp.sa_flags = SA_RESETHAND | SA_NODEFER;
#else
// 现代(可靠)语义:
// SA_RESTART: 被信号中断的系统调用自动重启
// 默认:处理期间屏蔽同一信号,处理函数不被重置
newDisp.sa_flags = SA_RESTART;
#endif
if (sigaction(sig, &newDisp, &prevDisp) == –1)
return SIG_ERR;
return prevDisp.sa_handler;
}
十三、完整示例代码(C++)
示例一:发送和接收实时信号(附带数据)
// rtsig_sender.cpp — 发送实时信号
// 编译: g++ -o rtsig_sender rtsig_sender.cpp
// 用法: ./rtsig_sender <目标PID> <信号偏移0~31> <整数数据> [发送次数]
#include <csignal> // sigqueue, union sigval
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
int main(int argc, char* argv[]) {
if (argc < 4) {
fprintf(stderr, "用法: %s pid sig_offset data [count]\\n", argv[0]);
fprintf(stderr, " sig_offset: 0~31,实际信号为 SIGRTMIN+sig_offset\\n");
return EXIT_FAILURE;
}
pid_t targetPid = (pid_t)atol(argv[1]);
int sigOffset = atoi(argv[2]);
int sigData = atoi(argv[3]);
int count = (argc > 4) ? atoi(argv[4]) : 1;
int sig = SIGRTMIN + sigOffset;
if (sig > SIGRTMAX) {
fprintf(stderr, "信号偏移超出范围,SIGRTMAX=%d\\n", SIGRTMAX);
return EXIT_FAILURE;
}
printf("发送方 PID=%ld, UID=%ld\\n", (long)getpid(), (long)getuid());
printf("目标: PID=%ld, 信号=%d (SIGRTMIN+%d), 数据起始=%d, 次数=%d\\n",
(long)targetPid, sig, sigOffset, sigData, count);
for (int i = 0; i < count; i++) {
union sigval sv;
sv.sival_int = sigData + i; // 每次发送的数据递增
// sigqueue() 比 kill() 多一个 sigval 参数,可携带数据
// 实时信号会排队:发几次就能收几次
if (sigqueue(targetPid, sig, sv) == –1) {
if (errno == EAGAIN) {
fprintf(stderr, "信号队列已满(EAGAIN),第 %d 次发送失败\\n", i+1);
} else {
perror("sigqueue");
}
return EXIT_FAILURE;
}
printf("已发送第 %d 次,附带数据: %d\\n", i+1, sigData + i);
}
return EXIT_SUCCESS;
}
// rtsig_receiver.cpp — 接收实时信号
// 编译: g++ -o rtsig_receiver rtsig_receiver.cpp
// 用法: ./rtsig_receiver [屏蔽秒数]
// 先运行接收方,记下 PID,再运行发送方
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <cerrno>
static volatile int totalCaught = 0;
static volatile sig_atomic_t done = 0;
// SA_SIGINFO 三参数处理函数
// sig: 信号编号
// si: siginfo_t 指针,包含发送方信息和附带数据
// uc: 进程上下文(通常不使用)
static void rtSigHandler(int sig, siginfo_t* si, void* uc) {
// UNSAFE: printf 不是异步信号安全函数,仅用于演示
totalCaught++;
printf("收到实时信号 %d\\n", sig);
printf(" si_code=%d (%s)\\n",
si->si_code,
si->si_code == SI_QUEUE ? "SI_QUEUE(sigqueue发送)" :
si->si_code == SI_USER ? "SI_USER(kill发送)" : "其他");
printf(" si_value=%d\\n", si->si_value.sival_int);
printf(" 来自 PID=%ld, UID=%ld\\n",
(long)si->si_pid, (long)si->si_uid);
if (sig == SIGINT || sig == SIGTERM) {
done = 1;
}
}
int main(int argc, char* argv[]) {
printf("接收方 PID: %ld\\n", (long)getpid());
// 为所有实时信号安装处理函数
struct sigaction sa;
sa.sa_sigaction = rtSigHandler;
sa.sa_flags = SA_SIGINFO; // 必须设置,才能收到 siginfo_t
sigfillset(&sa.sa_mask); // 处理函数执行期间屏蔽所有信号
for (int sig = SIGRTMIN; sig <= SIGRTMAX; sig++) {
if (sigaction(sig, &sa, nullptr) == –1) {
// 某些实时信号可能被线程库保留,忽略错误
}
}
// 同样捕获 SIGINT 和 SIGTERM 用于退出
sa.sa_flags = SA_SIGINFO;
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
// 若提供参数,先屏蔽所有信号睡眠,期间发送方可以排队多个信号
if (argc > 1) {
int sleepSecs = atoi(argv[1]);
sigset_t blockAll;
sigfillset(&blockAll);
sigdelset(&blockAll, SIGKILL);
sigdelset(&blockAll, SIGSTOP);
sigprocmask(SIG_SETMASK, &blockAll, nullptr);
printf("屏蔽所有信号,睡眠 %d 秒(此期间请发送实时信号)…\\n", sleepSecs);
sleep(sleepSecs);
printf("睡眠结束,解除屏蔽,开始处理待决信号…\\n\\n");
// 解除屏蔽,待决的实时信号将按编号升序逐一投递
sigset_t emptyMask;
sigemptyset(&emptyMask);
sigprocmask(SIG_SETMASK, &emptyMask, nullptr);
}
// 等待信号到来
while (!done) {
pause();
}
printf("\\n共收到 %d 个实时信号\\n", totalCaught);
return EXIT_SUCCESS;
}
示例二:sigsuspend() 原子等待演示
// sigsuspend_demo.cpp
// 编译: g++ -o sigsuspend_demo sigsuspend_demo.cpp
// 演示:屏蔽 SIGINT,执行"关键区",然后用 sigsuspend 原子等待
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <ctime>
#include <cerrno>
#include <cstring>
static volatile sig_atomic_t gotSigquit = 0;
static void handler(int sig) {
// UNSAFE: 仅演示用
const char* name = (sig == SIGINT) ? "SIGINT(Ctrl+C)" : "SIGQUIT(Ctrl+\\\\)";
// 用 write 代替 printf,write 是异步信号安全的
char buf[64];
int len = snprintf(buf, sizeof(buf), "收到 %s\\n", name);
write(STDOUT_FILENO, buf, len);
if (sig == SIGQUIT) {
gotSigquit = 1;
}
}
int main() {
// 安装 SIGINT 和 SIGQUIT 的处理函数
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGQUIT, &sa, nullptr);
// 构建要屏蔽的信号集:SIGINT + SIGQUIT
sigset_t blockMask;
sigemptyset(&blockMask);
sigaddset(&blockMask, SIGINT);
sigaddset(&blockMask, SIGQUIT);
// 屏蔽信号,保存原屏蔽字
sigset_t origMask;
sigprocmask(SIG_BLOCK, &blockMask, &origMask);
int loop = 0;
while (!gotSigquit) {
loop++;
printf("\\n=== 第 %d 次循环:开始关键区(信号已屏蔽)===\\n", loop);
// 模拟关键区:忙等 3 秒
// 此期间 Ctrl+C 会被屏蔽,信号变为待决
time_t start = time(nullptr);
while (time(nullptr) < start + 3)
; // 忙等
// 查看待决信号
sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGINT))
printf("SIGINT 处于待决状态(关键区期间按了 Ctrl+C)\\n");
if (sigismember(&pending, SIGQUIT))
printf("SIGQUIT 处于待决状态\\n");
printf("关键区结束,调用 sigsuspend() 原子等待信号…\\n");
// sigsuspend(origMask):
// 1. 将屏蔽字替换为 origMask(解除对 SIGINT/SIGQUIT 的屏蔽)
// 2. 挂起进程,等待信号处理函数被调用并返回
// 3. 自动恢复为 blockMask(重新屏蔽 SIGINT/SIGQUIT)
// 全程原子,不存在竞态窗口
if (sigsuspend(&origMask) == –1 && errno != EINTR) {
perror("sigsuspend");
}
// 回到这里时,屏蔽字已自动恢复为 blockMask
}
// 恢复原屏蔽字
sigprocmask(SIG_SETMASK, &origMask, nullptr);
printf("\\n收到 SIGQUIT,退出\\n");
return EXIT_SUCCESS;
}
示例三:sigwaitinfo() 同步接收信号
// sigwaitinfo_demo.cpp
// 编译: g++ -o sigwaitinfo_demo sigwaitinfo_demo.cpp
// 用法: ./sigwaitinfo_demo [等待秒数]
// 等待秒数期间可用另一终端发送信号
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <ctime>
int main(int argc, char* argv[]) {
printf("本进程 PID: %ld\\n", (long)getpid());
// 步骤1:屏蔽所有信号(除 SIGKILL/SIGSTOP)
// 这样信号不会被默认处理,而是积压为待决
sigset_t allSigs;
sigfillset(&allSigs);
if (sigprocmask(SIG_SETMASK, &allSigs, nullptr) == –1) {
perror("sigprocmask");
return EXIT_FAILURE;
}
printf("所有信号已屏蔽\\n");
// 步骤2:可选睡眠,期间可以向本进程发送信号
if (argc > 1) {
int delaySecs = atoi(argv[1]);
printf("睡眠 %d 秒(期间请发送信号)…\\n", delaySecs);
sleep(delaySecs);
printf("睡眠结束,开始处理信号\\n\\n");
}
// 步骤3:循环用 sigwaitinfo() 同步取走信号
// 不需要处理函数!直接在主程序中处理
for (;;) {
siginfo_t si;
// sigwaitinfo() 阻塞,直到 allSigs 中的某个信号到来
// 该信号从待决集中移除,不会再次投递
int sig = sigwaitinfo(&allSigs, &si);
if (sig == –1) {
perror("sigwaitinfo");
return EXIT_FAILURE;
}
// 收到 SIGINT 或 SIGTERM 时退出
if (sig == SIGINT || sig == SIGTERM) {
printf("收到终止信号 %d,退出\\n", sig);
return EXIT_SUCCESS;
}
printf("收到信号: %d (%s)\\n", sig, strsignal(sig));
printf(" si_code=%d (%s)\\n",
si.si_code,
si.si_code == SI_QUEUE ? "SI_QUEUE" :
si.si_code == SI_USER ? "SI_USER" : "其他");
printf(" si_value=%d\\n", si.si_value.sival_int);
printf(" 发送方 PID=%ld, UID=%ld\\n\\n",
(long)si.si_pid, (long)si.si_uid);
}
}
示例四:signalfd() 通过文件描述符读取信号
// signalfd_demo.cpp
// 编译: g++ -o signalfd_demo signalfd_demo.cpp
// 用法: ./signalfd_demo <信号编号> [信号编号2 …]
// 示例: ./signalfd_demo 10 12 (监控 SIGUSR1 和 SIGUSR2)
#include <sys/signalfd.h> // signalfd, signalfd_siginfo
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(stderr, "用法: %s <信号编号> [信号编号2…]\\n", argv[0]);
return EXIT_FAILURE;
}
printf("本进程 PID: %ld\\n", (long)getpid());
// 步骤1:构建要监控的信号集
sigset_t mask;
sigemptyset(&mask);
for (int i = 1; i < argc; i++) {
int sig = atoi(argv[i]);
sigaddset(&mask, sig);
printf("监控信号: %d (%s)\\n", sig, strsignal(sig));
}
// 步骤2:屏蔽这些信号,防止它们被默认处理
// 必须在 signalfd() 之前调用
if (sigprocmask(SIG_BLOCK, &mask, nullptr) == –1) {
perror("sigprocmask");
return EXIT_FAILURE;
}
// 步骤3:创建 signalfd 文件描述符
// 参数 -1 表示创建新 fd;flags=0(或 SFD_NONBLOCK, SFD_CLOEXEC)
int sfd = signalfd(–1, &mask, 0);
if (sfd == –1) {
perror("signalfd");
return EXIT_FAILURE;
}
printf("signalfd 创建成功,fd=%d\\n\\n");
printf("等待信号中(可用 kill -<信号> %ld 发送)…\\n", (long)getpid());
// 步骤4:循环从 signalfd 读取信号
for (;;) {
struct signalfd_siginfo fdsi;
// read() 阻塞,直到有信号到来
// 每次 read 可返回多个 signalfd_siginfo(若缓冲区够大)
ssize_t s = read(sfd, &fdsi, sizeof(fdsi));
if (s != sizeof(fdsi)) {
perror("read");
return EXIT_FAILURE;
}
// 打印信号信息
printf("收到信号: %u (%s)\\n",
fdsi.ssi_signo, strsignal(fdsi.ssi_signo));
printf(" ssi_code=%d (%s)\\n",
fdsi.ssi_code,
fdsi.ssi_code == SI_QUEUE ? "SI_QUEUE" :
fdsi.ssi_code == SI_USER ? "SI_USER" : "其他");
// 仅当 SI_QUEUE 时,ssi_int 才有意义
if (fdsi.ssi_code == SI_QUEUE) {
printf(" ssi_pid=%u, ssi_int=%d\\n",
fdsi.ssi_pid, fdsi.ssi_int);
}
printf("\\n");
// 收到 SIGTERM 或 SIGINT 则退出
if (fdsi.ssi_signo == SIGTERM || fdsi.ssi_signo == SIGINT) {
printf("收到终止信号,关闭 signalfd 并退出\\n");
close(sfd); // 释放内核资源
return EXIT_SUCCESS;
}
}
}
十四、本章知识点总览
#mermaid-svg-IEucuPxdcOWDxkct{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IEucuPxdcOWDxkct .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IEucuPxdcOWDxkct .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IEucuPxdcOWDxkct .error-icon{fill:#552222;}#mermaid-svg-IEucuPxdcOWDxkct .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IEucuPxdcOWDxkct .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IEucuPxdcOWDxkct .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IEucuPxdcOWDxkct .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IEucuPxdcOWDxkct .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IEucuPxdcOWDxkct .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IEucuPxdcOWDxkct .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IEucuPxdcOWDxkct .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IEucuPxdcOWDxkct .marker.cross{stroke:#333333;}#mermaid-svg-IEucuPxdcOWDxkct svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IEucuPxdcOWDxkct p{margin:0;}#mermaid-svg-IEucuPxdcOWDxkct .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-IEucuPxdcOWDxkct .cluster-label text{fill:#333;}#mermaid-svg-IEucuPxdcOWDxkct .cluster-label span{color:#333;}#mermaid-svg-IEucuPxdcOWDxkct .cluster-label span p{background-color:transparent;}#mermaid-svg-IEucuPxdcOWDxkct .label text,#mermaid-svg-IEucuPxdcOWDxkct span{fill:#333;color:#333;}#mermaid-svg-IEucuPxdcOWDxkct .node rect,#mermaid-svg-IEucuPxdcOWDxkct .node circle,#mermaid-svg-IEucuPxdcOWDxkct .node ellipse,#mermaid-svg-IEucuPxdcOWDxkct .node polygon,#mermaid-svg-IEucuPxdcOWDxkct .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IEucuPxdcOWDxkct .rough-node .label text,#mermaid-svg-IEucuPxdcOWDxkct .node .label text,#mermaid-svg-IEucuPxdcOWDxkct .image-shape .label,#mermaid-svg-IEucuPxdcOWDxkct .icon-shape .label{text-anchor:middle;}#mermaid-svg-IEucuPxdcOWDxkct .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IEucuPxdcOWDxkct .rough-node .label,#mermaid-svg-IEucuPxdcOWDxkct .node .label,#mermaid-svg-IEucuPxdcOWDxkct .image-shape .label,#mermaid-svg-IEucuPxdcOWDxkct .icon-shape .label{text-align:center;}#mermaid-svg-IEucuPxdcOWDxkct .node.clickable{cursor:pointer;}#mermaid-svg-IEucuPxdcOWDxkct .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IEucuPxdcOWDxkct .arrowheadPath{fill:#333333;}#mermaid-svg-IEucuPxdcOWDxkct .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IEucuPxdcOWDxkct .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IEucuPxdcOWDxkct .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IEucuPxdcOWDxkct .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IEucuPxdcOWDxkct .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IEucuPxdcOWDxkct .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IEucuPxdcOWDxkct .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IEucuPxdcOWDxkct .cluster text{fill:#333;}#mermaid-svg-IEucuPxdcOWDxkct .cluster span{color:#333;}#mermaid-svg-IEucuPxdcOWDxkct div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IEucuPxdcOWDxkct .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IEucuPxdcOWDxkct rect.text{fill:none;stroke-width:0;}#mermaid-svg-IEucuPxdcOWDxkct .icon-shape,#mermaid-svg-IEucuPxdcOWDxkct .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IEucuPxdcOWDxkct .icon-shape p,#mermaid-svg-IEucuPxdcOWDxkct .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IEucuPxdcOWDxkct .icon-shape .label rect,#mermaid-svg-IEucuPxdcOWDxkct .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IEucuPxdcOWDxkct .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IEucuPxdcOWDxkct .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IEucuPxdcOWDxkct :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
信号高级特性
Core Dumpcore_pattern 自定义命名
特殊信号规则SIGKILL/SIGSTOP 不可屏蔽SIGCONT 与停止信号互消
睡眠状态可中断 S / 不可中断 D / 可杀 K
硬件信号不能正常 return用 siglongjmp 或 _exit
同步 vs 异步信号同步:立即投递异步:下次内核→用户切换时
实时信号排队+携带数据+有序投递sigqueue() 发送
原子等待sigsuspend() 解决竞态
同步接收sigwaitinfo/sigtimedwait无需处理函数
文件描述符接收signalfd()可与 select/poll/epoll 集成
十五、关键 API 速查
| sigqueue(pid, sig, val) | 发送实时信号+数据 | 实时信号排队;队满返回 EAGAIN |
| sigsuspend(&mask) | 原子替换屏蔽字并等待 | 处理函数返回后自动恢复原屏蔽字 |
| sigwaitinfo(&set, &si) | 同步取走一个信号 | 需先屏蔽 set 中的信号 |
| sigtimedwait(&set, &si, &timeout) | 带超时的 sigwaitinfo | timeout 两字段为 0 = 立即轮询 |
| signalfd(fd, &mask, flags) | 信号→文件描述符 | 可与 I/O 多路复用集成 |
| sigaction(…, SA_SIGINFO, …) | 安装三参数处理函数 | 必须用 sa_sigaction 字段 |
Linux 定时器与睡眠机制详解
本文对应《The Linux Programming Interface》第23章:Timers and Sleeping 目标:把每个API讲清楚,配合可运行的C++代码示例。
整体概览
本章涉及的API可以分为四个层次,从最古老到最现代:
+————————————————–+
| 层次1:经典UNIX接口(最古老) |
| alarm() / setitimer() / getitimer() |
+————————————————–+
| 层次2:睡眠接口 |
| sleep() / nanosleep() |
+————————————————–+
| 层次3:POSIX高精度时钟与定时器(POSIX.1b) |
| clock_gettime() / clock_nanosleep() |
| timer_create() / timer_settime() / timer_delete()|
+————————————————–+
| 层次4:Linux专有 timerfd(最现代) |
| timerfd_create() / timerfd_settime() |
| 可与 select/poll/epoll 配合使用 |
+————————————————–+
第一部分:间隔定时器 setitimer() 与 alarm()
1.1 setitimer() — 设置间隔定时器
概念解释
setitimer() 允许进程设置一个"倒计时器",时间到了之后向自己发送一个信号。 它支持三种计时方式:
| ITIMER_REAL | 真实墙钟时间(wall clock) | SIGALRM | 最常用,按真实时间计 |
| ITIMER_VIRTUAL | 进程用户态CPU时间 | SIGVTALRM | 只算程序自己用的CPU时间 |
| ITIMER_PROF | 进程用户态+内核态CPU时间 | SIGPROF | 用于性能剖析 |
数据结构
itimerval 结构(定时器的设置)
┌──────────────────────────────────┐
│ it_interval (重复间隔) │ ← 每次到期后重置为这个值继续倒数
│ tv_sec (秒) │
│ tv_usec (微秒) │
├──────────────────────────────────┤
│ it_value (当前剩余时间) │ ← 多久之后第一次到期
│ tv_sec (秒) │
│ tv_usec (微秒) │
└──────────────────────────────────┘
关键规则:
- 如果 it_interval 全为 0:定时器只触发一次
- 如果 it_interval 非零:定时器周期性触发,每次到期后重置
- 每个进程只能有各一个这三种类型的定时器
定时器的运作示意
时间轴:
0s ────────────────── 1.8s ──── 2.8s ──── 3.8s ──→
↑ 设置定时器 ↑ ↑ ↑
it_value=1.8s SIGALRM SIGALRM SIGALRM
it_interval=1s (重置) (重置) (重置)
函数原型(C++中对应的头文件)
#include <sys/time.h>
// 设置定时器
// which: ITIMER_REAL / ITIMER_VIRTUAL / ITIMER_PROF
// new_value: 新的定时器设置
// old_value: 返回旧的定时器设置(不需要时传 nullptr)
int setitimer(int which,
const struct itimerval *new_value,
struct itimerval *old_value);
// 获取定时器当前状态
int getitimer(int which, struct itimerval *curr_value);
完整可运行示例
// 文件名:real_timer_demo.cpp
// 编译:g++ -o real_timer_demo real_timer_demo.cpp
// 运行:./real_timer_demo
#include <csignal> // signal, sigaction, SIGALRM
#include <sys/time.h> // setitimer, getitimer, itimerval, timeval
#include <cstdio> // printf
#include <cstdlib> // exit
#include <cstring> // memset
#include <unistd.h> // pause
// 全局标志:记录是否收到了 SIGALRM 信号
// 用 volatile sig_atomic_t 保证信号处理器中修改是安全的
static volatile sig_atomic_t g_got_alarm = 0;
// 信号处理函数
// 注意:这里只设置标志,不做复杂操作
// (复杂操作在信号处理函数里不安全,见异步信号安全章节)
static void sigalrm_handler(int sig) {
g_got_alarm = 1;
}
// 辅助函数:显示当前经过时间和定时器状态
static void display_times(const char* label, bool show_timer) {
static struct timeval start_time;
static bool initialized = false;
struct timeval now;
// 第一次调用时记录起始时间
if (!initialized) {
gettimeofday(&start_time, nullptr);
initialized = true;
}
gettimeofday(&now, nullptr);
// 计算经过的秒数(包含小数部分)
double elapsed = (now.tv_sec – start_time.tv_sec)
+ (now.tv_usec – start_time.tv_usec) / 1e6;
printf("%-8s 经过时间: %6.2f秒", label, elapsed);
if (show_timer) {
struct itimerval itv;
getitimer(ITIMER_REAL, &itv);
// 定时器剩余时间和间隔
double remaining = itv.it_value.tv_sec
+ itv.it_value.tv_usec / 1e6;
double interval = itv.it_interval.tv_sec
+ itv.it_interval.tv_usec / 1e6;
printf(" 剩余: %6.2f秒 间隔: %6.2f秒", remaining, interval);
}
printf("\\n");
}
int main() {
// 第一步:设置 SIGALRM 的信号处理函数
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigalrm_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; // 不设 SA_RESTART,让系统调用被信号中断
if (sigaction(SIGALRM, &sa, nullptr) == –1) {
perror("sigaction");
return 1;
}
// 第二步:设置定时器
// 第一次到期:1.8秒后
// 之后每隔:1秒触发一次
struct itimerval itv;
itv.it_value.tv_sec = 1;
itv.it_value.tv_usec = 800000; // 1.8秒
itv.it_interval.tv_sec = 1;
itv.it_interval.tv_usec = 0; // 间隔1秒
display_times("START:", false); // 显示起始状态
if (setitimer(ITIMER_REAL, &itv, nullptr) == –1) {
perror("setitimer");
return 1;
}
// 第三步:主循环,持续消耗CPU时间
// 每消耗约0.5秒CPU时间就检查一次定时器状态
int signal_count = 0;
const int MAX_SIGNALS = 3; // 最多处理3次信号后退出
// 记录上一次检查的CPU时间
clock_t prev_clock = clock();
for (;;) {
// 内层循环:消耗约0.5秒CPU时间
while (((clock() – prev_clock) * 10 / CLOCKS_PER_SEC) < 5) {
// 如果收到了信号
if (g_got_alarm) {
g_got_alarm = 0;
display_times("ALARM:", true);
signal_count++;
if (signal_count >= MAX_SIGNALS) {
printf("收到了 %d 次定时器信号,程序退出\\n", MAX_SIGNALS);
exit(EXIT_SUCCESS);
}
}
}
// 消耗了0.5秒,打印一次状态
prev_clock = clock();
display_times("Main:", true);
}
return 0;
}
https://godbolt.org/z/4svr8o84E 运行输出示例:
START: 经过时间: 0.00秒
Main: 经过时间: 0.50秒 剩余: 1.30秒 间隔: 1.00秒
Main: 经过时间: 1.00秒 剩余: 0.80秒 间隔: 1.00秒
Main: 经过时间: 1.50秒 剩余: 0.30秒 间隔: 1.00秒
ALARM: 经过时间: 1.80秒 剩余: 1.00秒 间隔: 1.00秒
Main: 经过时间: 2.00秒 剩余: 0.80秒 间隔: 1.00秒
…
收到了 3 次定时器信号,程序退出
1.2 alarm() — 更简单的一次性定时器
alarm() 是 setitimer() 的简化版,只能设置一次性的实时定时器(ITIMER_REAL)。
alarm(5)
↓
5秒后 → 发送 SIGALRM → 若没有处理器则进程终止
#include <unistd.h>
// seconds秒后发送 SIGALRM
// 返回:上一个定时器的剩余秒数(没有则返回0)
// alarm(0) 取消已有定时器
unsigned int alarm(unsigned int seconds);
alarm() 最常见的用法:为阻塞操作设置超时
// 使用 alarm() 为 read() 设置超时
// 文件名:timed_read_demo.cpp
// 编译:g++ -o timed_read_demo timed_read_demo.cpp
// 运行:./timed_read_demo 然后等待,或输入内容
#include <csignal>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <cerrno>
// 信号处理函数(什么都不做,只是打断系统调用)
static void handler(int sig) {
// 空函数体:仅仅用于打断 read() 系统调用
// 当信号到来时,read() 会返回 -1,errno = EINTR
}
int main() {
const int TIMEOUT_SECS = 5; // 超时时间:5秒
char buf[200];
// 设置信号处理函数
// 注意:不设 SA_RESTART,这样信号会打断 read()
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; // 关键:不加 SA_RESTART
if (sigaction(SIGALRM, &sa, nullptr) == –1) {
perror("sigaction");
return 1;
}
printf("请在 %d 秒内输入内容(否则超时):\\n", TIMEOUT_SECS);
// 设置定时器
alarm(TIMEOUT_SECS);
// 执行可能阻塞的系统调用
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) – 1);
// 保存 errno(因为 alarm(0) 可能改变它)
int saved_errno = errno;
// 取消定时器(以防 read() 在超时前返回了)
alarm(0);
// 还原 errno
errno = saved_errno;
if (n == –1) {
if (errno == EINTR) {
printf("超时!read() 被信号中断\\n");
} else {
perror("read");
}
} else {
buf[n] = '\\0';
printf("成功读取 %ld 字节:%s\\n", (long)n, buf);
}
return 0;
}
注意竞争条件: 在 alarm() 调用完成之后、read() 开始之前,如果定时器恰好在这极短的时间内触发,read() 就不会被中断。 这种情况极少发生,但理论上存在。更安全的替代方案是使用 select()/poll() 的超时参数。
第二部分:睡眠函数
2.1 sleep() — 低精度睡眠(秒级)
#include <unistd.h>
// 睡眠 seconds 秒
// 返回:0(正常完成)或剩余未睡眠的秒数(被信号打断)
unsigned int sleep(unsigned int seconds);
注意: 在 Linux 上,sleep() 内部用 nanosleep() 实现,不会与 alarm() 冲突。但在其他 UNIX 系统上,sleep() 可能用 alarm() 实现,混用会有问题。
2.2 nanosleep() — 高精度睡眠(纳秒级)
nanosleep() 比 sleep() 更精确,精度可达纳秒(但实际精度受内核时钟粒度限制)。
#define _POSIX_C_SOURCE 199309
#include <time.h>
// request: 想睡多久
// remain: 如果被信号打断,剩余未睡的时间写到这里
// 返回:0(正常完成),-1(出错或被打断,errno=EINTR)
int nanosleep(const struct timespec *request, struct timespec *remain);
timespec 结构体:
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // 纳秒(必须在 0 ~ 999,999,999 之间)
};
精度关系:
1
秒
=
10
3
毫秒
=
10
6
微秒
=
10
9
纳秒
1\\text{秒} = 10^3\\text{毫秒} = 10^6\\text{微秒} = 10^9\\text{纳秒}
1秒=103毫秒=106微秒=109纳秒
完整示例:可被信号打断并重启的睡眠
// 文件名:nanosleep_demo.cpp
// 编译:g++ -o nanosleep_demo nanosleep_demo.cpp
// 运行:./nanosleep_demo 10 0
// (睡10秒,按 Ctrl+C 可打断,会自动重启继续睡)
#define _POSIX_C_SOURCE 199309
#include <ctime> // nanosleep, timespec, clock_gettime
#include <csignal> // sigaction, SIGINT
#include <sys/time.h> // gettimeofday
#include <cstdio> // printf
#include <cstdlib> // atol, exit
#include <cstring> // memset
#include <cerrno> // errno, EINTR
// SIGINT 信号处理函数(什么都不做,只是打断 nanosleep)
static void sigint_handler(int sig) {
// 空函数体:信号到来时 nanosleep() 返回 -1,errno=EINTR
}
int main(int argc, char* argv[]) {
if (argc != 3) {
printf("用法:%s <秒> <纳秒>\\n", argv[0]);
printf("例如:%s 10 0 (睡10秒)\\n", argv[0]);
return 1;
}
// 设置睡眠时间
struct timespec request;
request.tv_sec = atol(argv[1]);
request.tv_nsec = atol(argv[2]);
// 注册 SIGINT 处理函数,使得 Ctrl+C 可以打断 nanosleep
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, nullptr) == –1) {
perror("sigaction");
return 1;
}
// 记录开始时间
struct timeval start;
gettimeofday(&start, nullptr);
printf("开始睡眠 %ld 秒 %ld 纳秒\\n", request.tv_sec, request.tv_nsec);
printf("(按 Ctrl+C 打断,会自动重启继续睡)\\n\\n");
// 循环调用 nanosleep,如果被信号打断就用剩余时间重启
for (;;) {
struct timespec remain;
int ret = nanosleep(&request, &remain);
// 计算已经睡了多久
struct timeval now;
gettimeofday(&now, nullptr);
double elapsed = (now.tv_sec – start.tv_sec)
+ (now.tv_usec – start.tv_usec) / 1e6;
printf("已睡眠: %.6f 秒\\n", elapsed);
if (ret == 0) {
// nanosleep 正常完成
break;
}
if (errno != EINTR) {
// 其他错误
perror("nanosleep");
return 1;
}
// 被信号打断(EINTR),用剩余时间重新睡
printf("被信号打断!剩余: %ld.%09ld 秒,继续睡…\\n",
remain.tv_sec, remain.tv_nsec);
// 用剩余时间作为下次请求
request = remain;
}
printf("睡眠完成!\\n");
return 0;
}
高频信号的问题: 如果信号来得非常频繁(比软件时钟粒度还快),每次重启 nanosleep() 都会有舍入误差(remain 向上取整到时钟粒度的倍数),导致实际睡眠时间越来越长。 解决方案:用 clock_nanosleep() 加 TIMER_ABSTIME(绝对时间),见下文。
第三部分:POSIX 高精度时钟 API
3.1 时钟类型
Linux 支持多种时钟,用 clockid_t 标识:
| CLOCK_REALTIME | 系统挂钟时间(UTC时间) | 是(需要权限) | 获取/设置真实时间 |
| CLOCK_MONOTONIC | 单调递增,从系统启动开始 | 否 | 测量时间间隔(不怕NTP调整) |
| CLOCK_PROCESS_CPUTIME_ID | 进程消耗的CPU时间 | 否 | 性能测量 |
| CLOCK_THREAD_CPUTIME_ID | 线程消耗的CPU时间 | 否 | 线程级性能测量 |
CLOCK_REALTIME vs CLOCK_MONOTONIC 的选择:
CLOCK_REALTIME:
2024-01-01 00:00:00
2024-01-01 00:00:01
(NTP调整) ← 可能突然跳变!
2024-01-01 00:00:00.5 ← 回拨了!
CLOCK_MONOTONIC:
0s
1s
2s
3s ← 永远单调增加,不受NTP影响
3.2 获取和设置时钟
#define _POSIX_C_SOURCE 199309
#include <time.h>
// 获取时钟当前值
// clockid: 时钟类型
// tp: 返回时间值
int clock_gettime(clockid_t clockid, struct timespec *tp);
// 获取时钟分辨率(精度)
int clock_getres(clockid_t clockid, struct timespec *res);
// 设置时钟(需要 CAP_SYS_TIME 权限,只能设置 CLOCK_REALTIME)
int clock_settime(clockid_t clockid, const struct timespec *tp);
3.3 clock_nanosleep() — 高精度睡眠(支持绝对时间)
#define _XOPEN_SOURCE 600
#include <time.h>
// clockid: 使用哪个时钟
// flags: 0(相对时间)或 TIMER_ABSTIME(绝对时间)
// request: 睡到什么时候(相对或绝对)
// remain: 剩余时间(只在相对时间模式下有效)
int clock_nanosleep(clockid_t clockid, int flags,
const struct timespec *request,
struct timespec *remain);
关键优势:TIMER_ABSTIME 模式
相对时间模式的问题:
current_time → +睡眠时间 → 目标时间
但如果中间被打断了,重启时会有误差积累
绝对时间模式的解决方案:
直接指定目标时间点,不管被打断多少次,
重启都用同一个目标时间,不会有误差积累!
完整示例:精确睡到某个绝对时间点
// 文件名:clock_nanosleep_demo.cpp
// 编译:g++ -o clock_nanosleep_demo clock_nanosleep_demo.cpp -lrt
// 运行:./clock_nanosleep_demo
#define _POSIX_C_SOURCE 200112L
#include <ctime> // clock_gettime, clock_nanosleep, CLOCK_REALTIME, TIMER_ABSTIME
#include <cstdio> // printf
#include <cstring> // memset
#include <cerrno> // errno, EINTR
int main() {
struct timespec target;
// 获取当前时间
if (clock_gettime(CLOCK_REALTIME, &target) == –1) {
perror("clock_gettime");
return 1;
}
// 目标时间 = 当前时间 + 20秒
target.tv_sec += 20;
printf("当前时间:%ld.%09ld\\n", target.tv_sec – 20, target.tv_nsec);
printf("目标时间:%ld.%09ld(20秒后)\\n", target.tv_sec, target.tv_nsec);
printf("开始睡眠…\\n");
// 用绝对时间模式睡眠
// 好处:即使被信号打断多次,重启时用同一个目标时间,不会累积误差
for (;;) {
int ret = clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &target, nullptr);
if (ret == 0) {
// 成功睡到目标时间
printf("到达目标时间!\\n");
break;
} else if (ret == EINTR) {
// 被信号打断,重新用同一个 target 继续睡
// 注意:TIMER_ABSTIME 模式下不需要更新 target!
printf("被打断,重新睡到目标时间…\\n");
} else {
// 其他错误
perror("clock_nanosleep");
return 1;
}
}
return 0;
}
第四部分:POSIX 间隔定时器(高级版 setitimer)
4.1 为什么需要 POSIX 定时器?
setitimer() 的局限性:
setitimer 的问题:
1. 每种类型只能有一个定时器(REAL/VIRTUAL/PROF 各一个)
2. 通知方式只有信号
3. 无法检测"超时溢出"(错过了几次?)
4. 精度只到微秒
POSIX timer 的改进:
1. 可以创建任意多个定时器
2. 通知方式可以是:信号、线程函数
3. 提供超时溢出计数
4. 精度到纳秒
4.2 POSIX 定时器的生命周期
#mermaid-svg-GFSLR6DDN2r85rue{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GFSLR6DDN2r85rue .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GFSLR6DDN2r85rue .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GFSLR6DDN2r85rue .error-icon{fill:#552222;}#mermaid-svg-GFSLR6DDN2r85rue .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GFSLR6DDN2r85rue .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GFSLR6DDN2r85rue .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GFSLR6DDN2r85rue .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GFSLR6DDN2r85rue .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GFSLR6DDN2r85rue .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GFSLR6DDN2r85rue .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GFSLR6DDN2r85rue .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GFSLR6DDN2r85rue .marker.cross{stroke:#333333;}#mermaid-svg-GFSLR6DDN2r85rue svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GFSLR6DDN2r85rue p{margin:0;}#mermaid-svg-GFSLR6DDN2r85rue .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-GFSLR6DDN2r85rue .cluster-label text{fill:#333;}#mermaid-svg-GFSLR6DDN2r85rue .cluster-label span{color:#333;}#mermaid-svg-GFSLR6DDN2r85rue .cluster-label span p{background-color:transparent;}#mermaid-svg-GFSLR6DDN2r85rue .label text,#mermaid-svg-GFSLR6DDN2r85rue span{fill:#333;color:#333;}#mermaid-svg-GFSLR6DDN2r85rue .node rect,#mermaid-svg-GFSLR6DDN2r85rue .node circle,#mermaid-svg-GFSLR6DDN2r85rue .node ellipse,#mermaid-svg-GFSLR6DDN2r85rue .node polygon,#mermaid-svg-GFSLR6DDN2r85rue .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GFSLR6DDN2r85rue .rough-node .label text,#mermaid-svg-GFSLR6DDN2r85rue .node .label text,#mermaid-svg-GFSLR6DDN2r85rue .image-shape .label,#mermaid-svg-GFSLR6DDN2r85rue .icon-shape .label{text-anchor:middle;}#mermaid-svg-GFSLR6DDN2r85rue .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GFSLR6DDN2r85rue .rough-node .label,#mermaid-svg-GFSLR6DDN2r85rue .node .label,#mermaid-svg-GFSLR6DDN2r85rue .image-shape .label,#mermaid-svg-GFSLR6DDN2r85rue .icon-shape .label{text-align:center;}#mermaid-svg-GFSLR6DDN2r85rue .node.clickable{cursor:pointer;}#mermaid-svg-GFSLR6DDN2r85rue .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GFSLR6DDN2r85rue .arrowheadPath{fill:#333333;}#mermaid-svg-GFSLR6DDN2r85rue .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GFSLR6DDN2r85rue .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GFSLR6DDN2r85rue .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GFSLR6DDN2r85rue .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GFSLR6DDN2r85rue .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GFSLR6DDN2r85rue .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GFSLR6DDN2r85rue .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GFSLR6DDN2r85rue .cluster text{fill:#333;}#mermaid-svg-GFSLR6DDN2r85rue .cluster span{color:#333;}#mermaid-svg-GFSLR6DDN2r85rue div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GFSLR6DDN2r85rue .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GFSLR6DDN2r85rue rect.text{fill:none;stroke-width:0;}#mermaid-svg-GFSLR6DDN2r85rue .icon-shape,#mermaid-svg-GFSLR6DDN2r85rue .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GFSLR6DDN2r85rue .icon-shape p,#mermaid-svg-GFSLR6DDN2r85rue .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GFSLR6DDN2r85rue .icon-shape .label rect,#mermaid-svg-GFSLR6DDN2r85rue .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GFSLR6DDN2r85rue .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GFSLR6DDN2r85rue .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GFSLR6DDN2r85rue :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
发送信号
调用函数
不通知
timer_create创建定时器
timer_settime启动定时器
定时器到期
SIGEV_SIGNAL信号通知
SIGEV_THREAD线程通知
SIGEV_NONE手动查询
timer_gettime查询状态
timer_settime停止定时器
timer_delete删除定时器
4.3 创建定时器:timer_create()
#include <signal.h>
#include <time.h>
// 创建一个新的 POSIX 定时器
// clockid: 使用哪个时钟(CLOCK_REALTIME 等)
// evp: 定时器到期时如何通知(NULL表示默认发送SIGALRM)
// timerid: 返回定时器的ID(句柄)
int timer_create(clockid_t clockid,
struct sigevent *evp,
timer_t *timerid);
sigevent 结构体详解:
struct sigevent {
int sigev_notify; // 通知方式
int sigev_signo; // 发送哪个信号(SIGEV_SIGNAL时用)
union sigval sigev_value; // 附带数据(传给信号处理函数)
// …(线程通知相关字段)
};
union sigval {
int sival_int; // 整数附带数据
void *sival_ptr; // 指针附带数据(常用于传定时器ID)
};
通知方式对比:
| SIGEV_NONE | 不通知,手动用 timer_gettime() 查询 | 手动轮询 |
| SIGEV_SIGNAL | 发送信号到进程 | 最常用 |
| SIGEV_THREAD | 在新线程中调用函数 | 需要线程知识 |
| SIGEV_THREAD_ID | 发送信号到特定线程(Linux专有) | 线程库内部使用 |
4.4 启动/停止定时器:timer_settime()
#include <time.h>
// 设置定时器的触发时间和间隔
// timerid: timer_create() 返回的ID
// flags: 0(相对时间)或 TIMER_ABSTIME(绝对时间)
// value: 新的定时器设置
// old_value: 返回旧的设置(不需要时传 nullptr)
int timer_settime(timer_t timerid, int flags,
const struct itimerspec *value,
struct itimerspec *old_value);
itimerspec 与之前的 itimerval 类似,只是精度更高:
struct itimerspec {
struct timespec it_interval; // 重复间隔(秒+纳秒)
struct timespec it_value; // 第一次触发时间(秒+纳秒)
};
4.5 通知溢出(Timer Overrun)
什么是溢出?
定时器每秒触发一次,但进程被挂起了3秒:
时刻0:触发 → 排队等待
时刻1:触发 → 信号未处理,溢出!
时刻2:触发 → 信号未处理,溢出!
时刻3:进程恢复,收到信号,overrun_count = 2
获取溢出计数:
#include <time.h>
// 返回从上次信号处理到现在,定时器又额外触发了几次
// 如果只触发了一次(正常情况),返回0
int timer_getoverrun(timer_t timerid);
也可以直接从 siginfo_t 的 si_overrun 字段读取(Linux扩展,无需系统调用)。
4.6 完整示例:使用信号通知的 POSIX 定时器
// 文件名:posix_timer_signal_demo.cpp
// 编译:g++ -o posix_timer_signal_demo posix_timer_signal_demo.cpp -lrt
// 运行:./posix_timer_signal_demo
#define _POSIX_C_SOURCE 199309
#include <csignal> // sigaction, siginfo_t, sigevent, SIGRTMAX
#include <ctime> // timer_create, timer_settime, timer_getoverrun
#include <cstdio> // printf
#include <cstdlib> // exit
#include <cstring> // memset
#include <unistd.h> // pause
// 使用实时信号(SIGRTMAX),避免与 SIGALRM 冲突
// 实时信号比标准信号更可靠(可排队)
#define TIMER_SIG SIGRTMAX
// 信号处理函数
// 参数 si 包含定时器相关的详细信息
static void timer_handler(int sig, siginfo_t *si, void *uc) {
// 从 sigval 中获取定时器ID(我们在创建时设置了 sival_ptr = &timerid)
timer_t *tid_ptr = (timer_t*)si->si_value.sival_ptr;
printf("收到信号 %d\\n", sig);
printf(" 定时器ID = %ld\\n", (long)*tid_ptr);
printf(" 溢出次数 = %d\\n", timer_getoverrun(*tid_ptr));
}
int main() {
timer_t timerid;
// 第一步:设置信号处理函数
// 必须用 SA_SIGINFO,这样才能收到 siginfo_t 参数
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_flags = SA_SIGINFO; // 使用带附加信息的处理函数
sa.sa_sigaction = timer_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(TIMER_SIG, &sa, nullptr) == –1) {
perror("sigaction");
return 1;
}
// 第二步:配置通知方式
struct sigevent sev;
memset(&sev, 0, sizeof(sev));
sev.sigev_notify = SIGEV_SIGNAL; // 使用信号通知
sev.sigev_signo = TIMER_SIG; // 发送这个信号
// sival_ptr 传递定时器ID的地址,这样信号处理函数能知道是哪个定时器触发的
sev.sigev_value.sival_ptr = &timerid;
// 第三步:创建定时器(使用实时时钟)
if (timer_create(CLOCK_REALTIME, &sev, &timerid) == –1) {
perror("timer_create");
return 1;
}
printf("定时器已创建,ID = %ld\\n", (long)timerid);
// 第四步:启动定时器
// 初始值:2秒后第一次触发
// 间隔:之后每5秒触发一次
struct itimerspec ts;
ts.it_value.tv_sec = 2; // 2秒后第一次触发
ts.it_value.tv_nsec = 0;
ts.it_interval.tv_sec = 5; // 之后每5秒触发
ts.it_interval.tv_nsec = 0;
if (timer_settime(timerid, 0, &ts, nullptr) == –1) {
perror("timer_settime");
timer_delete(timerid);
return 1;
}
printf("定时器已启动:2秒后第一次触发,之后每5秒触发\\n");
printf("按 Ctrl+C 退出\\n\\n");
// 第五步:等待信号(pause 会在收到任何信号时返回)
for (;;) {
pause(); // 挂起等待信号
}
// 第六步:清理(实际上不会到这里,因为 Ctrl+C 会终止程序)
timer_delete(timerid);
return 0;
}
4.7 完整示例:使用线程函数通知的 POSIX 定时器
// 文件名:posix_timer_thread_demo.cpp
// 编译:g++ -o posix_timer_thread_demo posix_timer_thread_demo.cpp -lrt -lpthread
// 运行:./posix_timer_thread_demo
#define _POSIX_C_SOURCE 199309
#include <csignal> // sigevent, SIGEV_THREAD
#include <ctime> // timer_create, timer_settime, timer_getoverrun
#include <pthread.h> // pthread_mutex_t, pthread_cond_t
#include <cstdio> // printf
#include <cstdlib> // exit
#include <cstring> // memset
#include <unistd.h> // sleep
// 互斥锁和条件变量,用于线程间同步
static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
// 记录总触发次数(包含溢出)
static int g_expire_count = 0;
// 定时器到期时,系统会在新线程中调用这个函数
// sv: timer_create 时设置的 sigev_value(我们传的是定时器ID的地址)
static void thread_notify_func(union sigval sv) {
// 获取定时器ID
timer_t *tid_ptr = (timer_t*)sv.sival_ptr;
// 获取溢出次数
int overrun = timer_getoverrun(*tid_ptr);
printf("[线程通知] 定时器ID=%ld,溢出次数=%d\\n",
(long)*tid_ptr, overrun);
// 更新全局计数(需要加锁)
pthread_mutex_lock(&g_mtx);
g_expire_count += 1 + overrun; // 这次触发 + 溢出的次数
pthread_mutex_unlock(&g_mtx);
// 通知主线程:有新的触发事件
pthread_cond_signal(&g_cond);
}
int main() {
timer_t timerid;
// 配置线程通知方式
struct sigevent sev;
memset(&sev, 0, sizeof(sev));
sev.sigev_notify = SIGEV_THREAD; // 通过线程函数通知
sev.sigev_notify_function = thread_notify_func; // 要调用的函数
sev.sigev_notify_attributes = nullptr; // 线程属性(nullptr=默认)
sev.sigev_value.sival_ptr = &timerid; // 传递定时器ID给函数
// 创建定时器
if (timer_create(CLOCK_REALTIME, &sev, &timerid) == –1) {
perror("timer_create");
return 1;
}
// 启动定时器:3秒后第一次,之后每3秒触发
struct itimerspec ts;
ts.it_value.tv_sec = 3;
ts.it_value.tv_nsec = 0;
ts.it_interval.tv_sec = 3;
ts.it_interval.tv_nsec = 0;
if (timer_settime(timerid, 0, &ts, nullptr) == –1) {
perror("timer_settime");
timer_delete(timerid);
return 1;
}
printf("定时器已启动,每3秒触发一次(通过线程函数通知)\\n");
printf("运行15秒后自动退出\\n\\n");
// 主线程:等待条件变量,每次触发时打印统计
pthread_mutex_lock(&g_mtx);
int prev_count = 0;
while (g_expire_count < 5) { // 触发5次后退出
// 等待线程通知函数发出的信号
pthread_cond_wait(&g_cond, &g_mtx);
printf("主线程:总触发次数 = %d(本次新增 %d)\\n",
g_expire_count, g_expire_count – prev_count);
prev_count = g_expire_count;
}
pthread_mutex_unlock(&g_mtx);
printf("达到5次触发,程序退出\\n");
// 清理
timer_delete(timerid);
return 0;
}
第五部分:timerfd — 通过文件描述符读取定时器事件
5.1 timerfd 的核心思想
timerfd 是 Linux 专有的 API(从 Linux 2.6.25 开始),把定时器包装成一个文件描述符。 为什么要这样设计? 传统定时器通过信号通知,信号处理麻烦。而文件描述符可以和 select()/poll()/epoll 一起使用,能统一监听多个事件源:
传统方式(信号):
[定时器到期] → 信号处理函数 → 复杂的异步处理
timerfd 方式:
[定时器到期] ─┐
[网络数据到来] ─┤→ epoll_wait() → 统一处理所有事件
[文件可读] ─┘
5.2 timerfd API
#include <sys/timerfd.h>
// 创建 timerfd(返回文件描述符)
// clockid: CLOCK_REALTIME 或 CLOCK_MONOTONIC
// flags: 0,或 TFD_CLOEXEC | TFD_NONBLOCK
int timerfd_create(int clockid, int flags);
// 设置/启动定时器(用法与 timer_settime 相同)
int timerfd_settime(int fd, int flags,
const struct itimerspec *new_value,
struct itimerspec *old_value);
// 查询定时器状态(用法与 timer_gettime 相同)
int timerfd_gettime(int fd, struct itimerspec *curr_value);
// 读取触发次数(必须传入 uint64_t 大小的缓冲区)
// read(fd, &num_exp, sizeof(uint64_t))
// 返回:自上次 read() 后定时器触发了几次
// 如果还没触发:阻塞等待(除非设置了 O_NONBLOCK)
与 fork/exec 的行为:
- fork() 后:子进程继承 timerfd 文件描述符,父子进程都能读取触发事件
- exec() 后:文件描述符保留(除非设置了 TFD_CLOEXEC),定时器继续运行
5.3 完整示例:timerfd 基本使用
// 文件名:timerfd_demo.cpp
// 编译:g++ -o timerfd_demo timerfd_demo.cpp
// 运行:./timerfd_demo 1:1 5
// (1秒间隔,最多读5次触发事件)
#include <sys/timerfd.h>
#include <ctime> // CLOCK_REALTIME, itimerspec, timespec
#include <cstdint> // uint64_t
#include <cstdio> // printf
#include <cstdlib> // atoi, exit
#include <cstring> // memset, strchr
#include <unistd.h> // read, close
// 解析 "秒[:纳秒][/间隔秒[:间隔纳秒]]" 格式的字符串
// 格式示例:
// "2" → 2秒后触发一次
// "2:5" → 2秒后触发,之后每5秒触发
// "1/500000000:2" → 1.5秒后触发,之后每2秒触发
static void parse_timespec_str(const char* str, struct itimerspec* ts) {
memset(ts, 0, sizeof(*ts));
// 格式:初始值 ':' 间隔值
// 每个时间值格式:秒 '/' 纳秒
char buf[256];
strncpy(buf, str, sizeof(buf) – 1);
char* colon = strchr(buf, ':');
char* slash1 = strchr(buf, '/');
if (colon) *colon = '\\0';
if (slash1 && (!colon || slash1 < colon)) *slash1 = '\\0';
// 解析初始值(秒部分)
ts->it_value.tv_sec = atoi(buf);
// 解析初始值(纳秒部分)
if (slash1 && (!colon || slash1 < colon)) {
ts->it_value.tv_nsec = atoi(slash1 + 1);
}
// 解析间隔值
if (colon) {
char* interval_str = colon + 1;
char* slash2 = strchr(interval_str, '/');
if (slash2) *slash2 = '\\0';
ts->it_interval.tv_sec = atoi(interval_str);
if (slash2) {
ts->it_interval.tv_nsec = atoi(slash2 + 1);
}
}
}
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("用法:%s 时间字符串 [最大触发次数]\\n", argv[0]);
printf("例如:%s 1:1 5 (1秒触发,读5次)\\n", argv[0]);
return 1;
}
// 解析定时器参数
struct itimerspec ts;
parse_timespec_str(argv[1], &ts);
// 最大触发次数
int max_exp = (argc > 2) ? atoi(argv[2]) : 1;
// 创建 timerfd
int fd = timerfd_create(CLOCK_REALTIME, 0);
if (fd == –1) {
perror("timerfd_create");
return 1;
}
// 启动定时器(相对时间)
if (timerfd_settime(fd, 0, &ts, nullptr) == –1) {
perror("timerfd_settime");
close(fd);
return 1;
}
// 记录开始时间
struct timespec start;
clock_gettime(CLOCK_MONOTONIC, &start);
printf("定时器已启动,最多等待 %d 次触发\\n\\n", max_exp);
uint64_t total_exp = 0;
for (; total_exp < (uint64_t)max_exp;) {
// 阻塞等待定时器触发
// 读取的值是:自上次读取后定时器触发了多少次
uint64_t num_exp;
ssize_t s = read(fd, &num_exp, sizeof(uint64_t));
if (s != sizeof(uint64_t)) {
perror("read");
close(fd);
return 1;
}
total_exp += num_exp;
// 计算经过时间
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long secs = now.tv_sec – start.tv_sec;
long nsecs = now.tv_nsec – start.tv_nsec;
if (nsecs < 0) {
secs—;
nsecs += 1000000000L;
}
printf("%ld.%03ld 秒:本次读到 %llu 次触发,累计 %llu 次\\n",
secs, nsecs / 1000000L,
(unsigned long long)num_exp,
(unsigned long long)total_exp);
}
printf("\\n共读取 %llu 次触发,程序退出\\n", (unsigned long long)total_exp);
close(fd);
return 0;
}
5.4 timerfd 与 epoll 配合使用
// 文件名:timerfd_epoll_demo.cpp
// 编译:g++ -o timerfd_epoll_demo timerfd_epoll_demo.cpp
// 运行:./timerfd_epoll_demo
#include <sys/timerfd.h>
#include <sys/epoll.h> // epoll_create1, epoll_ctl, epoll_wait
#include <ctime>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <unistd.h>
int main() {
// 创建两个定时器:一个每秒触发,一个每3秒触发
int fd1 = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
int fd2 = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
if (fd1 == –1 || fd2 == –1) {
perror("timerfd_create");
return 1;
}
// 定时器1:1秒间隔
struct itimerspec ts1;
memset(&ts1, 0, sizeof(ts1));
ts1.it_value.tv_sec = 1;
ts1.it_interval.tv_sec = 1;
timerfd_settime(fd1, 0, &ts1, nullptr);
// 定时器2:3秒间隔
struct itimerspec ts2;
memset(&ts2, 0, sizeof(ts2));
ts2.it_value.tv_sec = 3;
ts2.it_interval.tv_sec = 3;
timerfd_settime(fd2, 0, &ts2, nullptr);
// 创建 epoll 实例
int epfd = epoll_create1(EPOLL_CLOEXEC);
if (epfd == –1) {
perror("epoll_create1");
return 1;
}
// 将两个定时器 fd 加入 epoll 监听
struct epoll_event ev;
memset(&ev, 0, sizeof(ev));
ev.events = EPOLLIN; // 监听可读事件(定时器到期时变为可读)
ev.data.fd = fd1;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev);
ev.data.fd = fd2;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev);
printf("开始监听两个定时器(fd1=每1秒,fd2=每3秒)\\n");
printf("运行约9秒后退出\\n\\n");
int total = 0;
// 用 epoll 统一监听多个定时器
while (total < 12) {
struct epoll_event events[2];
// 等待任意一个定时器到期(超时5秒)
int nfds = epoll_wait(epfd, events, 2, 5000);
if (nfds == –1) {
perror("epoll_wait");
break;
}
for (int i = 0; i < nfds; i++) {
uint64_t exp;
read(events[i].data.fd, &exp, sizeof(uint64_t));
if (events[i].data.fd == fd1) {
printf("[1秒定时器] 触发 %llu 次\\n", (unsigned long long)exp);
} else {
printf("[3秒定时器] 触发 %llu 次\\n", (unsigned long long)exp);
}
total++;
}
}
close(fd1);
close(fd2);
close(epfd);
return 0;
}
第六部分:API 对比总结
各 API 特性对比
| 定时器数量 | 1个 | 每类1个(共3个) | 任意多个 | 任意多个 |
| 精度 | 秒 | 微秒 | 纳秒 | 纳秒 |
| 通知方式 | 信号 | 信号 | 信号或线程 | 文件描述符 |
| 溢出检测 | 不支持 | 不支持 | 支持 | 支持(read返回次数) |
| 可与select/poll配合 | 否 | 否 | 否 | 是 |
| 跨 fork 继承 | 否 | 否 | 否 | 是 |
| 跨 exec 保留 | 否 | 否 | 否 | 是(除非CLOEXEC) |
| 标准 | POSIX | POSIX(已废弃) | POSIX.1b | Linux专有 |
如何选择?
需要兼容老代码或简单超时?
→ alarm() 或 setitimer()
需要多个定时器或纳秒精度?
→ POSIX timer_create()
需要通过线程回调而非信号通知?
→ POSIX timer_create() + SIGEV_THREAD
需要与 select/poll/epoll 配合监听多个事件?
→ timerfd_create()(Linux专有,最灵活)
第七部分:定时器精度问题
软件时钟粒度(jiffy)
内核有一个周期性的时钟中断,频率通常是 100Hz 或 1000Hz,每次中断的时间间隔称为 jiffy(节拍):
jiffy
=
1
时钟频率
\\text{jiffy} = \\frac{1}{\\text{时钟频率}}
jiffy=时钟频率1
- 100 Hz 时:
jiffy
=
10
ms
\\text{jiffy} = 10\\text{ms}
jiffy=10ms - 1000 Hz 时:
jiffy
=
1
ms
\\text{jiffy} = 1\\text{ms}
jiffy=1ms 实际效果: 如果你设置了一个 19.1ms 的定时器,实际触发可能在 20ms(向上取整到 jiffy 的倍数):T
实际
=
⌈
T
设置
/
jiffy
⌉
×
jiffy
T_{\\text{实际}} = \\lceil T_{\\text{设置}} / \\text{jiffy} \\rceil \\times \\text{jiffy}
T实际=⌈T设置/jiffy⌉×jiffy 高精度定时器(Linux 2.6.21+): 如果内核配置了 CONFIG_HIGH_RES_TIMERS,精度可以达到硬件支持的最高水平(通常 1 微秒)。
# 检查是否支持高精度定时器
cat /proc/timer_list | head -20
周期性定时器不漂移
setitimer() 和 POSIX 定时器的周期性触发不会产生累积误差。 例如,设置每 2 秒触发一次:
- 即使某次触发因系统繁忙延迟了 50ms,下一次触发仍然在第 4 秒(不是第 4.05 秒)
- 定时器的"下一次触发时间"是根据理论时间计算的,不是基于上一次实际触发时间
练习题解析
练习 23-1:用 setitimer() 实现 alarm()
// alarm_impl.cpp
// 用 setitimer(ITIMER_REAL) 实现 alarm() 的功能
// 编译:g++ -o alarm_impl alarm_impl.cpp
#include <sys/time.h> // setitimer, itimerval
#include <unistd.h> // alarm(原版)
#include <cstdio>
// 自己实现的 alarm()
unsigned int my_alarm(unsigned int seconds) {
struct itimerval new_itv, old_itv;
// 设置新的定时器
new_itv.it_value.tv_sec = seconds;
new_itv.it_value.tv_usec = 0;
// it_interval = 0,表示只触发一次(不重复)
new_itv.it_interval.tv_sec = 0;
new_itv.it_interval.tv_usec = 0;
// 设置定时器,同时获取旧定时器的剩余时间
if (setitimer(ITIMER_REAL, &new_itv, &old_itv) == –1) {
return 0; // 出错时返回0(与标准alarm行为一致)
}
// 返回旧定时器的剩余时间(秒,向上取整)
// 如果旧定时器剩余 0.5 秒,返回 1
unsigned int remaining = (unsigned int)old_itv.it_value.tv_sec;
if (old_itv.it_value.tv_usec > 0) {
remaining++; // 有剩余微秒,向上取整
}
return remaining;
}
int main() {
printf("测试 my_alarm():\\n");
// 测试:设置5秒定时器,然后立刻查询剩余时间
unsigned int prev = my_alarm(5);
printf("设置5秒定时器,之前剩余:%u 秒\\n", prev);
// 等2秒(简单循环消耗时间)
for (volatile long i = 0; i < 200000000L; i++);
// 改为3秒定时器
prev = my_alarm(3);
printf("改为3秒定时器,之前(5秒定时器)剩余:%u 秒\\n", prev);
// 取消定时器
prev = my_alarm(0);
printf("取消定时器,之前(3秒定时器)剩余:%u 秒\\n", prev);
printf("完成\\n");
return 0;
}
练习 23-2:用 clock_nanosleep + TIMER_ABSTIME 避免高频信号导致的过度睡眠 关键思路:
相对时间的问题:
start = now
while (被信号打断) {
remain -= (time used) // 每次有舍入误差
sleep(remain) // 误差累积!
}
绝对时间的解决方案:
target = now + interval // 只算一次目标时间
while (被信号打断) {
sleep_until(target) // 每次都睡到同一个目标时间,不累积误差
}
章节总结
本章介绍了 Linux 中从古到今的定时器和睡眠 API:
#mermaid-svg-5soOZPi8AcuqEfje{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5soOZPi8AcuqEfje .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5soOZPi8AcuqEfje .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5soOZPi8AcuqEfje .error-icon{fill:#552222;}#mermaid-svg-5soOZPi8AcuqEfje .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5soOZPi8AcuqEfje .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5soOZPi8AcuqEfje .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5soOZPi8AcuqEfje .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5soOZPi8AcuqEfje .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5soOZPi8AcuqEfje .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5soOZPi8AcuqEfje .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5soOZPi8AcuqEfje .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5soOZPi8AcuqEfje .marker.cross{stroke:#333333;}#mermaid-svg-5soOZPi8AcuqEfje svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5soOZPi8AcuqEfje p{margin:0;}#mermaid-svg-5soOZPi8AcuqEfje .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-5soOZPi8AcuqEfje .cluster-label text{fill:#333;}#mermaid-svg-5soOZPi8AcuqEfje .cluster-label span{color:#333;}#mermaid-svg-5soOZPi8AcuqEfje .cluster-label span p{background-color:transparent;}#mermaid-svg-5soOZPi8AcuqEfje .label text,#mermaid-svg-5soOZPi8AcuqEfje span{fill:#333;color:#333;}#mermaid-svg-5soOZPi8AcuqEfje .node rect,#mermaid-svg-5soOZPi8AcuqEfje .node circle,#mermaid-svg-5soOZPi8AcuqEfje .node ellipse,#mermaid-svg-5soOZPi8AcuqEfje .node polygon,#mermaid-svg-5soOZPi8AcuqEfje .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5soOZPi8AcuqEfje .rough-node .label text,#mermaid-svg-5soOZPi8AcuqEfje .node .label text,#mermaid-svg-5soOZPi8AcuqEfje .image-shape .label,#mermaid-svg-5soOZPi8AcuqEfje .icon-shape .label{text-anchor:middle;}#mermaid-svg-5soOZPi8AcuqEfje .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5soOZPi8AcuqEfje .rough-node .label,#mermaid-svg-5soOZPi8AcuqEfje .node .label,#mermaid-svg-5soOZPi8AcuqEfje .image-shape .label,#mermaid-svg-5soOZPi8AcuqEfje .icon-shape .label{text-align:center;}#mermaid-svg-5soOZPi8AcuqEfje .node.clickable{cursor:pointer;}#mermaid-svg-5soOZPi8AcuqEfje .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5soOZPi8AcuqEfje .arrowheadPath{fill:#333333;}#mermaid-svg-5soOZPi8AcuqEfje .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5soOZPi8AcuqEfje .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5soOZPi8AcuqEfje .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5soOZPi8AcuqEfje .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5soOZPi8AcuqEfje .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5soOZPi8AcuqEfje .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5soOZPi8AcuqEfje .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5soOZPi8AcuqEfje .cluster text{fill:#333;}#mermaid-svg-5soOZPi8AcuqEfje .cluster span{color:#333;}#mermaid-svg-5soOZPi8AcuqEfje div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5soOZPi8AcuqEfje .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5soOZPi8AcuqEfje rect.text{fill:none;stroke-width:0;}#mermaid-svg-5soOZPi8AcuqEfje .icon-shape,#mermaid-svg-5soOZPi8AcuqEfje .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5soOZPi8AcuqEfje .icon-shape p,#mermaid-svg-5soOZPi8AcuqEfje .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5soOZPi8AcuqEfje .icon-shape .label rect,#mermaid-svg-5soOZPi8AcuqEfje .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5soOZPi8AcuqEfje .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5soOZPi8AcuqEfje .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5soOZPi8AcuqEfje :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
简单一次性超时
秒级精度定时
多个定时器纳秒精度
配合IO多路复用
信号
线程回调
手动查询
定时器需求
需要什么?
alarm
setitimerITIMER_REAL 等
POSIX timer_create
timerfd_create
通知方式
SIGEV_SIGNAL
SIGEV_THREAD
SIGEV_NONE
与 epoll/select配合使用
核心知识点:
Linux 进程创建详解
对应《The Linux Programming Interface》第24章:Process Creation 目标:把 fork()、vfork()、写时复制、竞争条件讲得通俗易懂。
整体概览:四个核心系统调用
这四章(第24~27章)围绕进程的"生老病死"展开,四个核心调用分别负责:
fork() → 创建新进程(复制自己)
exit() → 终止进程(释放资源)
wait() → 父进程等待子进程结束
execve() → 加载并执行新程序
它们通常配合使用,就像 shell 执行一条命令的过程:
#mermaid-svg-nPmnVNwG1MfOXYeI{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nPmnVNwG1MfOXYeI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nPmnVNwG1MfOXYeI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nPmnVNwG1MfOXYeI .error-icon{fill:#552222;}#mermaid-svg-nPmnVNwG1MfOXYeI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nPmnVNwG1MfOXYeI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nPmnVNwG1MfOXYeI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nPmnVNwG1MfOXYeI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nPmnVNwG1MfOXYeI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nPmnVNwG1MfOXYeI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nPmnVNwG1MfOXYeI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nPmnVNwG1MfOXYeI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nPmnVNwG1MfOXYeI .marker.cross{stroke:#333333;}#mermaid-svg-nPmnVNwG1MfOXYeI svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nPmnVNwG1MfOXYeI p{margin:0;}#mermaid-svg-nPmnVNwG1MfOXYeI .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-nPmnVNwG1MfOXYeI .cluster-label text{fill:#333;}#mermaid-svg-nPmnVNwG1MfOXYeI .cluster-label span{color:#333;}#mermaid-svg-nPmnVNwG1MfOXYeI .cluster-label span p{background-color:transparent;}#mermaid-svg-nPmnVNwG1MfOXYeI .label text,#mermaid-svg-nPmnVNwG1MfOXYeI span{fill:#333;color:#333;}#mermaid-svg-nPmnVNwG1MfOXYeI .node rect,#mermaid-svg-nPmnVNwG1MfOXYeI .node circle,#mermaid-svg-nPmnVNwG1MfOXYeI .node ellipse,#mermaid-svg-nPmnVNwG1MfOXYeI .node polygon,#mermaid-svg-nPmnVNwG1MfOXYeI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nPmnVNwG1MfOXYeI .rough-node .label text,#mermaid-svg-nPmnVNwG1MfOXYeI .node .label text,#mermaid-svg-nPmnVNwG1MfOXYeI .image-shape .label,#mermaid-svg-nPmnVNwG1MfOXYeI .icon-shape .label{text-anchor:middle;}#mermaid-svg-nPmnVNwG1MfOXYeI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nPmnVNwG1MfOXYeI .rough-node .label,#mermaid-svg-nPmnVNwG1MfOXYeI .node .label,#mermaid-svg-nPmnVNwG1MfOXYeI .image-shape .label,#mermaid-svg-nPmnVNwG1MfOXYeI .icon-shape .label{text-align:center;}#mermaid-svg-nPmnVNwG1MfOXYeI .node.clickable{cursor:pointer;}#mermaid-svg-nPmnVNwG1MfOXYeI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nPmnVNwG1MfOXYeI .arrowheadPath{fill:#333333;}#mermaid-svg-nPmnVNwG1MfOXYeI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nPmnVNwG1MfOXYeI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nPmnVNwG1MfOXYeI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nPmnVNwG1MfOXYeI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nPmnVNwG1MfOXYeI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nPmnVNwG1MfOXYeI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nPmnVNwG1MfOXYeI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nPmnVNwG1MfOXYeI .cluster text{fill:#333;}#mermaid-svg-nPmnVNwG1MfOXYeI .cluster span{color:#333;}#mermaid-svg-nPmnVNwG1MfOXYeI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nPmnVNwG1MfOXYeI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nPmnVNwG1MfOXYeI rect.text{fill:none;stroke-width:0;}#mermaid-svg-nPmnVNwG1MfOXYeI .icon-shape,#mermaid-svg-nPmnVNwG1MfOXYeI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nPmnVNwG1MfOXYeI .icon-shape p,#mermaid-svg-nPmnVNwG1MfOXYeI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nPmnVNwG1MfOXYeI .icon-shape .label rect,#mermaid-svg-nPmnVNwG1MfOXYeI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nPmnVNwG1MfOXYeI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nPmnVNwG1MfOXYeI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nPmnVNwG1MfOXYeI :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
fork
wait 等待
execve 可选
exit
exit
内核通知
是
父进程运行程序A
子进程复制自父进程
子进程完成?
子进程运行程序B
子进程退出传递状态码
父进程继续执行
为什么 UNIX 把 fork 和 exec 分开? 其他操作系统(如 Windows)往往把"创建进程"和"加载程序"合并成一个 spawn 操作。 UNIX 分开的好处:
- fork() 不需要参数,接口简洁
- 在 fork() 和 exec() 之间可以做很多事(重定向、关闭文件描述符等)
- 有时只需要 fork() 而不需要 exec()(纯粹并行执行相同代码)
第一部分:fork() — 创建子进程
1.1 基本概念
fork() 调用一次,返回两次:
- 在父进程中返回:子进程的 PID(大于0的整数)
- 在子进程中返回:0
- 失败时返回:-1
调用 fork() 之前:
一个进程
调用 fork() 之后:
两个进程,从同一个点继续执行
父进程得到子进程的PID
子进程得到 0
用 ASCII 图示意:
fork()
|
|
——+——
| |
父进程 子进程
返回值=子PID 返回值=0
继续向下执行 继续向下执行
(同一份代码) (同一份代码)
1.2 内存关系
子进程拿到的是父进程栈、数据段、堆的独立副本,但代码段(text)共享:
fork() 之后的内存布局:
父进程 子进程
+———-+ +———-+
| text段 |—共享—–>| text段 | ← 代码只读,直接共享
+———-+ +———-+
| data段 | 独立副本 | data段 | ← 初始值相同,之后互不影响
+———-+ +———-+
| heap段 | 独立副本 | heap段 |
+———-+ +———-+
| stack段 | 独立副本 | stack段 |
+———-+ +———-+
关键点: 修改子进程的变量不影响父进程,反之亦然。
1.3 fork() 的惯用写法
pid_t child_pid;
switch (child_pid = fork()) {
case –1:
// fork 失败(资源不足,或达到进程数上限)
perror("fork");
exit(1);
case 0:
// 子进程执行这里
// child_pid == 0
break;
default:
// 父进程执行这里
// child_pid == 子进程的PID(正整数)
break;
}
1.4 完整示例:验证父子进程内存独立
// 文件名:fork_demo.cpp
// 编译:g++ -o fork_demo fork_demo.cpp
// 运行:./fork_demo
#include <unistd.h> // fork, getpid, sleep
#include <sys/wait.h> // wait
#include <cstdio> // printf
#include <cstdlib> // exit, EXIT_SUCCESS
// 全局变量(数据段)
static int g_data = 111;
int main() {
// 局部变量(栈)
int stack_var = 222;
pid_t child_pid;
printf("fork() 之前:g_data=%d, stack_var=%d, PID=%ld\\n",
g_data, stack_var, (long)getpid());
switch (child_pid = fork()) {
case –1:
perror("fork");
return 1;
case 0:
// —- 子进程 —-
// 修改自己的副本,不影响父进程
g_data *= 3; // 111 * 3 = 333
stack_var *= 3; // 222 * 3 = 666
printf("子进程(PID=%ld):g_data=%d, stack_var=%d\\n",
(long)getpid(), g_data, stack_var);
// 子进程用 _exit() 退出,不刷 stdio 缓冲区
_exit(EXIT_SUCCESS);
default:
// —- 父进程 —-
// 让子进程先执行(不可靠,只是演示用)
sleep(1);
printf("父进程(PID=%ld):g_data=%d, stack_var=%d\\n",
(long)getpid(), g_data, stack_var);
// 等待子进程结束,避免产生僵尸进程
wait(nullptr);
exit(EXIT_SUCCESS);
}
return 0;
}
预期输出:
fork() 之前:g_data=111, stack_var=222, PID=12345
子进程(PID=12346):g_data=333, stack_var=666
父进程(PID=12345):g_data=111, stack_var=222
第二部分:fork() 之后的文件描述符共享
2.1 文件描述符的继承规则
fork() 后,子进程拿到父进程所有文件描述符的副本,但这些副本指向同一个内核文件描述结构(open file description):
父进程 内核
fd[3] ─────────────────────→ 文件描述结构(OFD)
包含:文件偏移、打开标志
子进程 ↑
fd[3] ─────────────────────┘ 共享!
后果:
- 子进程移动文件偏移(lseek),父进程也能看到变化
- 子进程设置 O_APPEND 标志,父进程也能看到变化
2.2 文件共享的示意图
fork() 之前:
父进程
+——+ +——————+
| fd 3 |——->| OFT entry |
+——+ | offset: 0 |
| flags: 无O_APPEND|
+——————+
fork() 之后:
父进程 子进程
+——+ +——————+ +——+
| fd 3 |——->| OFT entry |<——-| fd 3 |
+——+ | offset: 0 | +——+
| flags: 无O_APPEND|
+——————+
子进程执行 lseek(fd, 1000, SEEK_SET) 和设置 O_APPEND 之后:
父进程 子进程
+——+ +——————+ +——+
| fd 3 |——->| OFT entry |<——-| fd 3 |
+——+ | offset: 1000 | +——+
| flags: O_APPEND |
+——————+
↑ 父进程也能看到这些变化!
2.3 完整示例:验证文件描述符共享
// 文件名:fork_file_sharing.cpp
// 编译:g++ -o fork_file_sharing fork_file_sharing.cpp
// 运行:./fork_file_sharing
#include <sys/stat.h> // mkstemp 需要
#include <fcntl.h> // O_APPEND, F_GETFL, F_SETFL
#include <sys/wait.h> // wait
#include <unistd.h> // fork, lseek, close, unlink
#include <cstdio> // printf, setbuf
#include <cstdlib> // exit, EXIT_SUCCESS
#include <cstring> // strcpy
int main() {
// 关闭 stdout 缓冲,确保输出顺序正确
setbuf(stdout, nullptr);
// 创建一个临时文件
char tmpl[] = "/tmp/fork_test_XXXXXX";
int fd = mkstemp(tmpl);
if (fd == –1) {
perror("mkstemp");
return 1;
}
// 删除文件名(文件本身还在,通过fd访问)
unlink(tmpl);
// 记录 fork 之前的状态
printf("fork() 之前:\\n");
printf(" 文件偏移 = %lld\\n", (long long)lseek(fd, 0, SEEK_CUR));
int flags = fcntl(fd, F_GETFL);
printf(" O_APPEND 标志 = %s\\n", (flags & O_APPEND) ? "开" : "关");
switch (fork()) {
case –1:
perror("fork");
return 1;
case 0:
// —- 子进程:修改文件偏移和标志 —-
// 移动文件偏移到1000字节处
if (lseek(fd, 1000, SEEK_SET) == –1) {
perror("lseek");
_exit(1);
}
// 获取当前标志,设置 O_APPEND
flags = fcntl(fd, F_GETFL);
flags |= O_APPEND;
if (fcntl(fd, F_SETFL, flags) == –1) {
perror("fcntl – F_SETFL");
_exit(1);
}
printf("子进程:已将偏移设为1000,已开启O_APPEND\\n");
_exit(EXIT_SUCCESS);
default:
// —- 父进程:等子进程结束后检查 —-
wait(nullptr);
printf("\\n子进程已退出,父进程检查文件状态:\\n");
// 父进程的文件描述符也反映了子进程的修改
printf(" 文件偏移 = %lld(子进程改的,父进程可见)\\n",
(long long)lseek(fd, 0, SEEK_CUR));
flags = fcntl(fd, F_GETFL);
printf(" O_APPEND 标志 = %s(子进程改的,父进程可见)\\n",
(flags & O_APPEND) ? "开" : "关");
close(fd);
exit(EXIT_SUCCESS);
}
return 0;
}
输出:
fork() 之前:
文件偏移 = 0
O_APPEND 标志 = 关
子进程:已将偏移设为1000,已开启O_APPEND
子进程已退出,父进程检查文件状态:
文件偏移 = 1000(子进程改的,父进程可见)
O_APPEND 标志 = 开(子进程改的,父进程可见)
2.4 如何避免不需要的文件共享
如果父子进程需要独立操作文件,应该在 fork() 后各自关闭不需要的文件描述符:
fork() 之后:
父进程关闭子进程专用的 fd_y: 子进程关闭父进程专用的 fd_x:
+——+ +——+
| fd_x |→ OFT_m (父进程专用) | fd_y |→ OFT_n (子进程专用)
+——+ +——+
(fd_y 已关闭) (fd_x 已关闭)
第三部分:写时复制(Copy-on-Write,CoW)
3.1 为什么需要 CoW?
早期 UNIX 的 fork() 会真正复制父进程的全部内存——即使子进程马上就 exec() 替换掉所有内存,复制也是浪费的。 现代 Linux 用两个技巧避免: 技巧1:代码段共享 代码段只读,父子进程直接共享同一份物理页,无需复制。 技巧2:写时复制(CoW) 数据段、堆、栈的页表项一开始也指向同一份物理内存,并标记为只读。 只有当某一方真正写这些内存时,内核才复制那一页,给写入方一个独立的副本。
3.2 CoW 过程图解
fork() 之后,修改之前:
父进程页表 物理内存 子进程页表
+———-+ +———-+ +———-+
| 页211 |─────────>| 帧 1998 |<────────| 页211 |
| (只读) | | 数据… | | (只读) |
+———-+ +———-+ +———-+
↑ 两个页表指向同一物理帧,标记只读
某一方(假设子进程)尝试写页211时:
内核捕获写操作(page fault)
复制帧1998到新帧2038
将子进程的页211指向新帧2038
两个帧都标记为可读写
修改之后:
父进程页表 物理内存 子进程页表
+———-+ +———-+ +———-+
| 页211 |─────────>| 帧 1998 | | 页211 |─────────>| 帧 2038 |
| (可读写) | | 原始数据 | | (可读写) | | 修改后 |
+———-+ +———-+ +———-+ +———-+
↑ 父进程仍用原帧 ↑ 子进程用新帧
3.3 CoW 的性能含义
实际复制的页数
=
fork后被修改的页数
\\text{实际复制的页数} = \\text{fork后被修改的页数}
实际复制的页数=fork后被修改的页数 如果 fork() 之后马上 exec()(替换整个程序),几乎不会触发 CoW,非常高效。 如果父子进程都大量修改内存,则复制量接近完全复制,性能下降。
3.4 利用 fork() 控制内存占用
一个实用技巧:在子进程中调用可能泄漏内存的函数,父进程的内存不受影响:
// 在子进程中调用 func(),父进程内存足迹不变
pid_t child_pid = fork();
if (child_pid == –1) { perror("fork"); exit(1); }
if (child_pid == 0) {
// 子进程:调用 func,结果通过退出码传回
exit(func(arg)); // func 里的内存泄漏只影响子进程
}
// 父进程:等待子进程,查看退出码
int status;
wait(&status);
// 通过 WEXITSTATUS(status) 获取 func 的返回值
注意:退出码只有 8 位(0~255),更复杂的结果需要用管道或共享内存传递。
第四部分:vfork() — 比 fork() 更快但更危险
4.1 vfork() 的设计背景
历史上,fork() 真正复制内存,很慢。BSD 发明了 vfork() 来加速"fork后立即exec"这种常见模式。 现代 Linux 的 fork() 已经用 CoW 优化,速度接近 vfork(),所以 vfork() 已经过时,SUSv4 甚至删除了它的规范。
4.2 vfork() 的两个特殊语义
vfork() 的两条特殊规则:
规则1:子进程直接使用父进程的内存(不复制,不CoW)
→ 子进程修改变量,父进程能看到!
规则2:父进程被挂起,直到子进程调用 exec() 或 _exit()
→ 保证子进程先运行
4.3 vfork() vs fork() 对比
| 内存复制 | CoW(懒复制) | 完全共享,不复制 |
| 父进程执行 | 立即继续(顺序不定) | 挂起,等子进程 exec/_exit |
| 子进程修改变量 | 只影响自己 | 会影响父进程! |
| 安全性 | 高 | 低(容易出bug) |
| 标准状态 | 标准 | SUSv4已删除 |
| 建议 | 优先使用 | 避免使用 |
4.4 完整示例:演示 vfork() 的特殊行为
// 文件名:vfork_demo.cpp
// 编译:g++ -o vfork_demo vfork_demo.cpp
// 运行:./vfork_demo
// 注意:vfork 是危险的,这里只做演示,实际项目避免使用
#include <unistd.h> // vfork, _exit, write, STDOUT_FILENO
#include <cstdio> // printf
#include <cstdlib> // exit, EXIT_SUCCESS
int main() {
int istack = 222; // 局部变量
printf("vfork() 之前:istack = %d\\n", istack);
switch (vfork()) {
case –1:
perror("vfork");
return 1;
case 0:
// —- 子进程 —-
// 注意:子进程直接用的是父进程的内存空间!
// 即使睡3秒,父进程也不会被调度(被挂起了)
sleep(3);
// 用 write 而不是 printf(避免 stdio 缓冲区问题)
write(STDOUT_FILENO, "子进程正在执行\\n", 22);
// 修改变量——这实际上修改的是父进程的内存!
istack *= 3; // 222 * 3 = 666
// 子进程必须用 _exit(),不能用 exit()
// exit() 会刷新并关闭 stdio 缓冲区,影响父进程
_exit(EXIT_SUCCESS);
default:
// —- 父进程 —-
// 父进程在这里继续,子进程已经结束
write(STDOUT_FILENO, "父进程正在执行\\n", 22);
// istack 被子进程改成了666!
printf("istack = %d(子进程改的,父进程看到了)\\n", istack);
exit(EXIT_SUCCESS);
}
return 0;
}
输出:
vfork() 之前:istack = 222
子进程正在执行
父进程正在执行
istack = 666(子进程改的,父进程看到了)
注意事项:
- vfork() 的子进程绝对不能 return(从 main 返回),否则会破坏父进程的栈帧,导致段错误
- 必须用 _exit() 而不是 exit()(exit() 会清理 stdio 缓冲区,影响父进程)
- 子进程中只能做有限的操作(主要是文件描述符操作和 exec())
第五部分:fork() 之后的竞争条件
5.1 什么是竞争条件?
fork() 之后,父进程和子进程谁先执行是不确定的,取决于:
- 调度器的策略
- 系统负载
- CPU 数量
- 内核版本 如果程序的正确性依赖于"父进程先执行"或"子进程先执行"这种假设,就会产生竞争条件(race condition)。
5.2 各版本内核的行为变化
| Linux 2.2 | 父进程(99.97%情况) | 父进程的CPU状态已热身 |
| Linux 2.6(早期) | 子进程(99.98%情况) | 子进程立即exec可减少CoW复制 |
| Linux 2.6.32+ | 父进程(默认) | 父进程TLB缓存热,性能更好 |
结论:不要假设任何固定的执行顺序! 可以通过 /proc/sys/kernel/sched_child_runs_first 控制(Linux专有):
- 设为 0:父进程先运行(默认)
- 设为非0:子进程先运行
5.3 竞争条件的演示
下面用一个程序来统计父子进程谁先打印消息:
// 文件名:fork_race_demo.cpp
// 编译:g++ -o fork_race_demo fork_race_demo.cpp
// 运行:./fork_race_demo 10
#include <unistd.h> // fork
#include <sys/wait.h> // wait
#include <cstdio> // printf, setbuf
#include <cstdlib> // exit, atoi, EXIT_SUCCESS
#include <cstring> // strcmp
int main(int argc, char* argv[]) {
int num_children = (argc > 1) ? atoi(argv[1]) : 5;
// 关闭缓冲,确保每次 printf 立即输出
setbuf(stdout, nullptr);
int parent_first = 0;
int child_first = 0;
for (int j = 0; j < num_children; j++) {
pid_t child_pid;
switch (child_pid = fork()) {
case –1:
perror("fork");
return 1;
case 0:
// 子进程:打印消息后退出
printf("%d child\\n", j);
_exit(EXIT_SUCCESS);
default:
// 父进程:打印消息后等待子进程
printf("%d parent\\n", j);
wait(nullptr);
break;
}
}
// 实际测试时需要分析输出,统计谁先打印
// 这里只打印提示
printf("\\n运行完成。分析输出可知父子进程的调度顺序。\\n");
printf("注意:不同内核版本结果不同,不可依赖。\\n");
exit(EXIT_SUCCESS);
}
第六部分:用信号同步父子进程
6.1 问题:如何让父进程等子进程完成某个操作?
场景:父进程需要等子进程完成某些初始化(如建立数据库连接)后才能继续。
6.2 解决方案:信号同步
基本流程:
1. fork() 之前,父进程先屏蔽同步信号(SIGUSR1)
→ 防止信号在 fork() 之后、父进程调用 sigsuspend() 之前就到达
2. fork() 之后:
– 子进程:完成工作后,向父进程发送 SIGUSR1
– 父进程:调用 sigsuspend() 等待 SIGUSR1
3. 父进程收到信号后,恢复信号屏蔽,继续执行
关键点:为什么要在 fork() 之前就屏蔽信号?
时间线(有问题的版本,信号在fork后才屏蔽):
父进程 子进程
fork()
完成工作
发送 SIGUSR1
↑ 信号到达,但父进程还没屏蔽!
↑ 信号被默认处理(终止进程)或被丢弃
sigprocmask(屏蔽SIGUSR1) ← 已经太晚了
sigsuspend() 永远等待… ← 竞争条件!
正确的版本(fork前屏蔽):
父进程(fork前先屏蔽)
sigprocmask(屏蔽SIGUSR1)
fork()
完成工作
发送 SIGUSR1
↑ 信号被加入待处理队列
sigsuspend(空掩码) ← 解除屏蔽,立即收到信号
收到信号,继续执行 ← 正确!
6.3 完整示例:信号同步父子进程
// 文件名:fork_signal_sync.cpp
// 编译:g++ -o fork_signal_sync fork_signal_sync.cpp
// 运行:./fork_signal_sync
#include <csignal> // sigaction, sigprocmask, sigsuspend, SIGUSR1
#include <unistd.h> // fork, getpid, getppid, sleep, kill
#include <sys/wait.h> // wait
#include <cstdio> // printf, setbuf
#include <cstdlib> // exit, EXIT_SUCCESS
#include <cstring> // memset
#include <cerrno> // errno, EINTR
#include <ctime> // time, strftime, localtime
// 获取当前时间字符串(格式:HH:MM:SS)
static const char* current_time() {
static char buf[20];
time_t t = time(nullptr);
strftime(buf, sizeof(buf), "%H:%M:%S", localtime(&t));
return buf;
}
// 同步信号
#define SYNC_SIG SIGUSR1
// 信号处理函数:什么都不做,只是用于打断 sigsuspend
static void sync_handler(int sig) {
// 空函数体:sigsuspend 会在收到信号后返回
}
int main() {
// 关闭输出缓冲
setbuf(stdout, nullptr);
// 第一步:在 fork() 之前屏蔽同步信号
// 这是关键!避免信号在 fork() 之后、sigsuspend() 之前到达
sigset_t block_mask, orig_mask, empty_mask;
sigemptyset(&block_mask);
sigaddset(&block_mask, SYNC_SIG);
if (sigprocmask(SIG_BLOCK, &block_mask, &orig_mask) == –1) {
perror("sigprocmask");
return 1;
}
// 第二步:设置信号处理函数
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sync_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 让被信号打断的系统调用自动重启
if (sigaction(SYNC_SIG, &sa, nullptr) == –1) {
perror("sigaction");
return 1;
}
// 第三步:fork()
pid_t child_pid;
switch (child_pid = fork()) {
case –1:
perror("fork");
return 1;
case 0:
// —- 子进程 —-
printf("[%s 子进程PID=%ld] 开始执行某些工作…\\n",
current_time(), (long)getpid());
// 模拟做一些需要时间的工作(如初始化数据库连接)
sleep(2);
printf("[%s 子进程PID=%ld] 工作完成,向父进程发送信号\\n",
current_time(), (long)getpid());
// 向父进程发送同步信号
if (kill(getppid(), SYNC_SIG) == –1) {
perror("kill");
_exit(1);
}
// 子进程继续做其他事…
printf("[%s 子进程PID=%ld] 子进程继续运行\\n",
current_time(), (long)getpid());
_exit(EXIT_SUCCESS);
default:
// —- 父进程 —-
// 父进程可以先做一些不依赖子进程结果的工作
printf("[%s 父进程PID=%ld] 等待子进程完成初始化…\\n",
current_time(), (long)getpid());
// 用 sigsuspend 等待信号
// sigsuspend 会临时替换信号掩码为 empty_mask(空),
// 这样 SYNC_SIG 就能到达
// 当信号到达时,sigsuspend 返回(errno = EINTR)
sigemptyset(&empty_mask);
if (sigsuspend(&empty_mask) == –1 && errno != EINTR) {
perror("sigsuspend");
return 1;
}
printf("[%s 父进程PID=%ld] 收到信号!子进程已完成初始化\\n",
current_time(), (long)getpid());
// 恢复原来的信号掩码
if (sigprocmask(SIG_SETMASK, &orig_mask, nullptr) == –1) {
perror("sigprocmask");
return 1;
}
// 父进程继续执行(现在可以安全使用子进程初始化的资源了)
printf("[%s 父进程PID=%ld] 父进程继续执行其他工作\\n",
current_time(), (long)getpid());
// 等待子进程结束,防止产生僵尸进程
wait(nullptr);
exit(EXIT_SUCCESS);
}
return 0;
}
输出示例:
[17:30:00 父进程PID=12345] 等待子进程完成初始化…
[17:30:00 子进程PID=12346] 开始执行某些工作…
[17:30:02 子进程PID=12346] 工作完成,向父进程发送信号
[17:30:02 父进程PID=12345] 收到信号!子进程已完成初始化
[17:30:02 父进程PID=12345] 父进程继续执行其他工作
[17:30:02 子进程PID=12346] 子进程继续运行
第七部分:练习题解析
练习 24-1:三次连续 fork() 创建几个进程?
fork(); // 第1次
fork(); // 第2次
fork(); // 第3次
用树来分析:
#mermaid-svg-ORdwvvpFsg8OrQII{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ORdwvvpFsg8OrQII .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ORdwvvpFsg8OrQII .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ORdwvvpFsg8OrQII .error-icon{fill:#552222;}#mermaid-svg-ORdwvvpFsg8OrQII .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ORdwvvpFsg8OrQII .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ORdwvvpFsg8OrQII .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ORdwvvpFsg8OrQII .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ORdwvvpFsg8OrQII .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ORdwvvpFsg8OrQII .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ORdwvvpFsg8OrQII .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ORdwvvpFsg8OrQII .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ORdwvvpFsg8OrQII .marker.cross{stroke:#333333;}#mermaid-svg-ORdwvvpFsg8OrQII svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ORdwvvpFsg8OrQII p{margin:0;}#mermaid-svg-ORdwvvpFsg8OrQII .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ORdwvvpFsg8OrQII .cluster-label text{fill:#333;}#mermaid-svg-ORdwvvpFsg8OrQII .cluster-label span{color:#333;}#mermaid-svg-ORdwvvpFsg8OrQII .cluster-label span p{background-color:transparent;}#mermaid-svg-ORdwvvpFsg8OrQII .label text,#mermaid-svg-ORdwvvpFsg8OrQII span{fill:#333;color:#333;}#mermaid-svg-ORdwvvpFsg8OrQII .node rect,#mermaid-svg-ORdwvvpFsg8OrQII .node circle,#mermaid-svg-ORdwvvpFsg8OrQII .node ellipse,#mermaid-svg-ORdwvvpFsg8OrQII .node polygon,#mermaid-svg-ORdwvvpFsg8OrQII .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ORdwvvpFsg8OrQII .rough-node .label text,#mermaid-svg-ORdwvvpFsg8OrQII .node .label text,#mermaid-svg-ORdwvvpFsg8OrQII .image-shape .label,#mermaid-svg-ORdwvvpFsg8OrQII .icon-shape .label{text-anchor:middle;}#mermaid-svg-ORdwvvpFsg8OrQII .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ORdwvvpFsg8OrQII .rough-node .label,#mermaid-svg-ORdwvvpFsg8OrQII .node .label,#mermaid-svg-ORdwvvpFsg8OrQII .image-shape .label,#mermaid-svg-ORdwvvpFsg8OrQII .icon-shape .label{text-align:center;}#mermaid-svg-ORdwvvpFsg8OrQII .node.clickable{cursor:pointer;}#mermaid-svg-ORdwvvpFsg8OrQII .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ORdwvvpFsg8OrQII .arrowheadPath{fill:#333333;}#mermaid-svg-ORdwvvpFsg8OrQII .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ORdwvvpFsg8OrQII .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ORdwvvpFsg8OrQII .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ORdwvvpFsg8OrQII .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ORdwvvpFsg8OrQII .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ORdwvvpFsg8OrQII .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ORdwvvpFsg8OrQII .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ORdwvvpFsg8OrQII .cluster text{fill:#333;}#mermaid-svg-ORdwvvpFsg8OrQII .cluster span{color:#333;}#mermaid-svg-ORdwvvpFsg8OrQII div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ORdwvvpFsg8OrQII .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ORdwvvpFsg8OrQII rect.text{fill:none;stroke-width:0;}#mermaid-svg-ORdwvvpFsg8OrQII .icon-shape,#mermaid-svg-ORdwvvpFsg8OrQII .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ORdwvvpFsg8OrQII .icon-shape p,#mermaid-svg-ORdwvvpFsg8OrQII .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ORdwvvpFsg8OrQII .icon-shape .label rect,#mermaid-svg-ORdwvvpFsg8OrQII .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ORdwvvpFsg8OrQII .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ORdwvvpFsg8OrQII .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ORdwvvpFsg8OrQII :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
fork1
fork2
fork3
fork2
fork3
fork3
fork3
原始进程 P0
子进程 P1
子进程 P2
子进程 P3
子进程 P4
子进程 P5
子进程 P6
子进程 P7
计算方式:每次 fork() 都让当前存在的所有进程数量翻倍:
进程总数
=
2
3
=
8
\\text{进程总数} = 2^3 = 8
进程总数=23=8 其中新创建的子进程数为
8
−
1
=
7
8 – 1 = 7
8−1=7。 执行过程:
初始:1个进程(P0)
第1次 fork():
P0 执行 fork() → 产生 P1
现在有:P0, P1(共2个)
第2次 fork():
P0 和 P1 都执行 fork()
P0 → 产生 P2
P1 → 产生 P4
现在有:P0, P1, P2, P4(共4个)
第3次 fork():
P0, P1, P2, P4 都执行 fork()
P0 → 产生 P3
P1 → 产生 P5
P2 → 产生 P6
P4 → 产生 P7
现在有:P0, P1, P2, P3, P4, P5, P6, P7(共8个)
新创建的子进程:P1, P2, P3, P4, P5, P6, P7(共7个)
练习 24-3:如何在不终止进程的情况下获取 core dump?
思路:在子进程中让其崩溃产生 core dump,父进程继续运行。
// 文件名:core_dump_demo.cpp
// 编译:g++ -o core_dump_demo core_dump_demo.cpp
// 运行:./core_dump_demo(需要系统允许 core dump)
#include <unistd.h> // fork
#include <sys/wait.h> // wait, WCOREDUMP
#include <csignal> // SIGABRT, raise
#include <cstdio> // printf
#include <cstdlib> // exit, EXIT_SUCCESS
int main() {
printf("主进程(PID=%ld):准备产生 core dump 快照\\n", (long)getpid());
pid_t child_pid = fork();
if (child_pid == –1) {
perror("fork");
return 1;
}
if (child_pid == 0) {
// 子进程:通过 abort() 产生 core dump
// abort() 发送 SIGABRT,默认行为是终止并产生 core dump
// core 文件记录了此时刻的进程状态(内存、寄存器等)
abort(); // 等价于 raise(SIGABRT)
}
// 父进程:等待子进程
int status;
wait(&status);
// 检查子进程是否真的产生了 core dump
if (WIFSIGNALED(status) && WCOREDUMP(status)) {
printf("主进程(PID=%ld):core dump 已生成\\n", (long)getpid());
} else {
printf("主进程(PID=%ld):core dump 可能未生成(检查系统设置)\\n",
(long)getpid());
}
printf("主进程(PID=%ld):继续正常运行\\n", (long)getpid());
exit(EXIT_SUCCESS);
}
注意: 系统默认可能禁止 core dump,需要先执行 ulimit -c unlimited。
总结
本章核心知识点
fork()
→ 创建子进程,返回两次
→ 子进程获得父进程栈/数据/堆的独立副本(CoW实现)
→ 代码段共享
→ 文件描述符共享(指向同一OFD)
→ fork后谁先运行不确定(取决于内核版本和调度器)
写时复制(CoW)
→ fork后内存页初始共享,标记只读
→ 任一方写入时才复制那一页
→ 使得 fork+exec 非常高效(exec前几乎不触发复制)
vfork()
→ 子进程直接用父进程内存(不复制)
→ 父进程挂起直到子进程exec/_exit
→ 危险,已过时,避免使用
竞争条件
→ fork后执行顺序不确定
→ 不要假设父进程或子进程先运行
→ 用信号(sigsuspend + kill)实现同步
→ 关键:在fork前就屏蔽同步信号
各知识点关系图
#mermaid-svg-MS5Nax3VMc5PqvTZ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MS5Nax3VMc5PqvTZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MS5Nax3VMc5PqvTZ .error-icon{fill:#552222;}#mermaid-svg-MS5Nax3VMc5PqvTZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MS5Nax3VMc5PqvTZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MS5Nax3VMc5PqvTZ .marker.cross{stroke:#333333;}#mermaid-svg-MS5Nax3VMc5PqvTZ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MS5Nax3VMc5PqvTZ p{margin:0;}#mermaid-svg-MS5Nax3VMc5PqvTZ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-MS5Nax3VMc5PqvTZ .cluster-label text{fill:#333;}#mermaid-svg-MS5Nax3VMc5PqvTZ .cluster-label span{color:#333;}#mermaid-svg-MS5Nax3VMc5PqvTZ .cluster-label span p{background-color:transparent;}#mermaid-svg-MS5Nax3VMc5PqvTZ .label text,#mermaid-svg-MS5Nax3VMc5PqvTZ span{fill:#333;color:#333;}#mermaid-svg-MS5Nax3VMc5PqvTZ .node rect,#mermaid-svg-MS5Nax3VMc5PqvTZ .node circle,#mermaid-svg-MS5Nax3VMc5PqvTZ .node ellipse,#mermaid-svg-MS5Nax3VMc5PqvTZ .node polygon,#mermaid-svg-MS5Nax3VMc5PqvTZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MS5Nax3VMc5PqvTZ .rough-node .label text,#mermaid-svg-MS5Nax3VMc5PqvTZ .node .label text,#mermaid-svg-MS5Nax3VMc5PqvTZ .image-shape .label,#mermaid-svg-MS5Nax3VMc5PqvTZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-MS5Nax3VMc5PqvTZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MS5Nax3VMc5PqvTZ .rough-node .label,#mermaid-svg-MS5Nax3VMc5PqvTZ .node .label,#mermaid-svg-MS5Nax3VMc5PqvTZ .image-shape .label,#mermaid-svg-MS5Nax3VMc5PqvTZ .icon-shape .label{text-align:center;}#mermaid-svg-MS5Nax3VMc5PqvTZ .node.clickable{cursor:pointer;}#mermaid-svg-MS5Nax3VMc5PqvTZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MS5Nax3VMc5PqvTZ .arrowheadPath{fill:#333333;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MS5Nax3VMc5PqvTZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MS5Nax3VMc5PqvTZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MS5Nax3VMc5PqvTZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MS5Nax3VMc5PqvTZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MS5Nax3VMc5PqvTZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MS5Nax3VMc5PqvTZ .cluster text{fill:#333;}#mermaid-svg-MS5Nax3VMc5PqvTZ .cluster span{color:#333;}#mermaid-svg-MS5Nax3VMc5PqvTZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MS5Nax3VMc5PqvTZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MS5Nax3VMc5PqvTZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-MS5Nax3VMc5PqvTZ .icon-shape,#mermaid-svg-MS5Nax3VMc5PqvTZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MS5Nax3VMc5PqvTZ .icon-shape p,#mermaid-svg-MS5Nax3VMc5PqvTZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MS5Nax3VMc5PqvTZ .icon-shape .label rect,#mermaid-svg-MS5Nax3VMc5PqvTZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MS5Nax3VMc5PqvTZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MS5Nax3VMc5PqvTZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MS5Nax3VMc5PqvTZ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
fork
内存复制
写时复制CoW
只在真正写入时才复制页面
文件描述符
共享同一OFD偏移和标志共享
执行顺序
不确定取决于调度器
竞争条件风险
用信号同步解决
vfork
共享内存父进程挂起
危险已过时避免使用
进程终止与子进程监控
本文对应《Linux/UNIX系统编程手册》第25章(进程终止)和第26章(监控子进程)
第25章:进程终止
25.1 进程的两种终止方式
进程终止有两大类:
_exit() 系统调用
#include <unistd.h>
void _exit(int status);
- status 参数:进程的终止状态码,父进程可以通过 wait() 获取
- 实际只使用 status 的低8位(即
0
∼
255
0 \\sim 255
0∼255) - 调用后进程一定会终止,永不返回 约定规范:
| 0 / EXIT_SUCCESS | 成功退出 |
| 非零 / EXIT_FAILURE | 失败退出 |
注意陷阱: 如果传入大于128的值,在 shell 脚本中会产生歧义。因为当进程被信号杀死时,shell 会将 $? 设置为
128
+
信号编号
128 + \\text{信号编号}
128+信号编号,与主动调用 _exit(128+N) 无法区分。
exit() 库函数
#include <stdlib.h>
void exit(int status);
exit() 是对 _exit() 的封装,在调用 _exit() 之前,还会额外做以下三件事:
调用 exit() 时的执行顺序:
┌──────────────────────────────────────────────┐
│ 1. 按注册的逆序调用所有退出处理函数 │
│ (atexit / on_exit 注册的函数) │
│ 2. 刷新所有 stdio 缓冲区 │
│ 3. 调用 _exit(status) 真正终止进程 │
└──────────────────────────────────────────────┘
从 main() 返回等价于调用 exit():
- return n; 等价于 exit(n);(运行时库会用 main 的返回值调用 exit)
- 例外:如果退出处理函数中访问了 main 的局部变量,return 会导致未定义行为 C标准版本差异(从 main 末尾"掉出来"):
- C89:行为未定义,退出状态是随机值(栈上的垃圾)
- C99:等价于 exit(0),即成功退出
25.2 进程终止时内核做的清理工作
无论正常还是异常终止,内核都会自动做以下清理:
进程终止时内核自动清理项目:
├── 关闭所有打开的文件描述符、目录流、消息目录描述符、转换描述符
├── 释放该进程持有的所有文件锁(文件描述符关闭时自动释放)
├── 分离所有附加的 System V 共享内存段,对应 shm_nattch 计数减1
├── 对每个设置了 semadj 的 System V 信号量,将 semadj 值加回信号量
├── 如果是控制终端的控制进程:
│ ├── 向前台进程组发送 SIGHUP 信号
│ └── 终端与会话解除关联
├── 关闭所有打开的 POSIX 命名信号量(如同调用 sem_close())
├── 关闭所有打开的 POSIX 消息队列(如同调用 mq_close())
├── 如果某进程组因此变成孤儿组且有停止的进程:发送 SIGHUP + SIGCONT
├── 移除该进程通过 mlock()/mlockall() 建立的所有内存锁
└── 解除该进程通过 mmap() 建立的所有内存映射
25.3 退出处理函数(Exit Handlers)
什么是退出处理函数?
退出处理函数是程序员注册的一个函数,在进程通过 exit() 正常退出时自动被调用。 重要限制:
- 直接调用 _exit() 时,不会调用退出处理函数
- 进程被信号异常终止时,不会调用退出处理函数
用 atexit() 注册(标准方法)
#include <stdlib.h>
int atexit(void (*func)(void));
// 成功返回 0,失败返回非零值(注意:不一定是 -1)
注册的函数格式:
void my_cleanup(void)
{
/* 执行清理工作 */
}
调用顺序:按注册的逆序调用(后注册的先调用,类似栈)
注册顺序: func1 → func2 → func3
调用顺序: func3 → func2 → func1
Linux 实现细节:
- glibc 用动态分配的链表存储,支持几乎无限数量的退出处理函数
- sysconf(_SC_ATEXIT_MAX) 返回
2,147,482,647
2{,}147{,}482{,}647
2,147,482,647(2
31
−
1
2^{31}-1
231−1),实际上内存先耗尽 fork() 和 exec() 对退出处理函数的影响: - fork() 子进程继承父进程的退出处理函数列表
- exec() 执行后清除所有退出处理函数(因为代码段被替换了)
用 on_exit() 注册(GNU 扩展,非标准)
#define _BSD_SOURCE
#include <stdlib.h>
int on_exit(void (*func)(int, void *), void *arg);
// 成功返回 0,失败返回非零值
on_exit() 注册的函数可以接收两个参数:
void my_cleanup(int exit_status, void *arg)
{
// exit_status: 传给 exit() 的状态码
// arg: 注册时传入的自定义参数
}
atexit() 的两个局限:
退出处理函数示例(完整可运行代码)
// exit_handlers_demo.cpp
// 演示 atexit() 和 on_exit() 的使用
// 编译: g++ -o exit_handlers_demo exit_handlers_demo.cpp
#include <cstdio>
#include <cstdlib>
// atexit 注册的函数:无参数,无返回值
static void atexitFunc1(void)
{
printf("atexit 函数1 被调用\\n");
}
static void atexitFunc2(void)
{
printf("atexit 函数2 被调用\\n");
}
// on_exit 注册的函数:接收退出状态码和自定义参数
static void onexitFunc(int exitStatus, void *arg)
{
printf("on_exit 函数被调用: status=%d, arg=%ld\\n",
exitStatus, (long)arg);
}
int main(void)
{
// 注册顺序:onexitFunc(10) → atexitFunc1 → atexitFunc2 → onexitFunc(20)
// 调用顺序(逆序):onexitFunc(20) → atexitFunc2 → atexitFunc1 → onexitFunc(10)
if (on_exit(onexitFunc, (void *)10) != 0) {
fprintf(stderr, "on_exit 1 失败\\n");
return 1;
}
if (atexit(atexitFunc1) != 0) {
fprintf(stderr, "atexit 1 失败\\n");
return 1;
}
if (atexit(atexitFunc2) != 0) {
fprintf(stderr, "atexit 2 失败\\n");
return 1;
}
if (on_exit(onexitFunc, (void *)20) != 0) {
fprintf(stderr, "on_exit 2 失败\\n");
return 1;
}
exit(2); // 触发所有退出处理函数
}
https://godbolt.org/z/x3M1s6E5z 预期输出:
on_exit 函数被调用: status=2, arg=20
atexit 函数2 被调用
atexit 函数1 被调用
on_exit 函数被调用: status=2, arg=10
25.4 fork()、stdio 缓冲区与 _exit() 的相互影响
这是一个经典的"坑",理解它需要知道 stdio 缓冲区的工作方式。
现象
// fork_stdio_demo.cpp
// 演示 fork 与 stdio 缓冲区的交互问题
// 编译: g++ -o fork_stdio_demo fork_stdio_demo.cpp
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
int main(void)
{
// printf 输出进入 stdio 缓冲区(重定向到文件时是块缓冲)
printf("Hello world\\n");
// write 直接写入内核缓冲区,不经过 stdio 缓冲区
write(STDOUT_FILENO, "Ciao\\n", 5);
if (fork() == –1) {
perror("fork");
return 1;
}
// 父进程和子进程都会执行到这里
exit(EXIT_SUCCESS);
}
https://godbolt.org/z/9WaGvaMaW 输出到终端(行缓冲):
Hello world
Ciao
正常,因为终端是行缓冲,\\n 会立即刷新缓冲区。 重定向到文件(块缓冲):
Ciao
Hello world
Hello world
奇怪现象解析:
为什么 "Hello world" 出现两次?
终端模式(行缓冲):
printf("Hello world\\n")
│ 遇到 \\n 立即刷新
▼
[内核缓冲区] ← 已输出
fork() 时 stdio 缓冲区已空
父、子各调用 exit() 时刷新空缓冲区
→ 只输出一次 ✓
文件重定向模式(块缓冲):
printf("Hello world\\n")
│ 未满不刷新,留在用户空间 stdio 缓冲区
▼
[stdio缓冲区: "Hello world\\n"]
fork() 发生,缓冲区被复制!
┌─────────────────────┐
│父进程 stdio 缓冲区 │ → exit() → 刷新 → 输出"Hello world"
│"Hello world\\n" │
└─────────────────────┘
┌─────────────────────┐
│子进程 stdio 缓冲区 │ → exit() → 刷新 → 输出"Hello world"
│"Hello world\\n" │ (复制自父进程!)
└─────────────────────┘
→ 输出两次!
为什么 "Ciao" 在前面?
write() 直接写内核缓冲区,不受 stdio 影响,立即可见
printf 的内容在 exit() 时才刷出来
所以 Ciao 先出现
解决方案
方案一:fork 之前手动刷新缓冲区
fflush(stdout); // 或 fflush(NULL) 刷新所有流
pid_t pid = fork();
方案二:子进程用 _exit() 代替 exit()(推荐)
pid_t pid = fork();
if (pid == 0) {
// 子进程:用 _exit() 退出,不刷新 stdio 缓冲区
_exit(EXIT_SUCCESS);
} else {
// 父进程:用 exit() 退出,负责刷新缓冲区
exit(EXIT_SUCCESS);
}
方案三:禁用缓冲区
setbuf(stdout, NULL); // 完全无缓冲
第26章:监控子进程
26.1 等待子进程
26.1.1 wait() 系统调用
#include <sys/wait.h>
pid_t wait(int *status);
// 返回已终止子进程的 PID,出错返回 -1
wait() 的行为:
// 循环等待所有子进程
while ((childPid = wait(NULL)) != –1)
continue;
if (errno != ECHILD) // ECHILD 表示没有子进程了,是正常结束
perror("wait");
wait() 完整示例(多子进程)
// multi_wait_demo.cpp
// 创建多个子进程,每个睡眠不同时间后退出,父进程等待它们
// 编译: g++ -o multi_wait_demo multi_wait_demo.cpp
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <ctime>
// 获取当前时间字符串(HH:MM:SS)
static const char* currTime(void)
{
static char buf[64];
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
strftime(buf, sizeof(buf), "%H:%M:%S", tm_info);
return buf;
}
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "用法: %s 睡眠时间1 [睡眠时间2 …]\\n", argv[0]);
return 1;
}
// 禁用 stdout 缓冲,避免 fork 后重复输出
setbuf(stdout, NULL);
// 为每个命令行参数创建一个子进程
for (int j = 1; j < argc; j++) {
switch (fork()) {
case –1:
perror("fork");
return 1;
case 0: // 子进程:睡眠指定秒数后退出
printf("[%s] 子进程 %d 启动, PID=%ld, 睡眠 %s 秒\\n",
currTime(), j, (long)getpid(), argv[j]);
sleep((unsigned int)atoi(argv[j]));
_exit(EXIT_SUCCESS); // 用 _exit 避免刷新父进程的缓冲区
default: // 父进程:继续创建下一个子进程
break;
}
}
// 父进程循环等待所有子进程
int numDead = 0;
for (;;) {
pid_t childPid = wait(NULL);
if (childPid == –1) {
if (errno == ECHILD) {
printf("所有子进程已终止,再见!\\n");
break;
} else {
perror("wait");
return 1;
}
}
numDead++;
printf("[%s] wait() 返回子进程 PID=%ld (numDead=%d)\\n",
currTime(), (long)childPid, numDead);
}
return 0;
}
26.1.2 waitpid() 系统调用
wait() 的局限性:
- 不能等待特定的子进程
- 只能阻塞等待,无法非阻塞轮询
- 无法得知子进程被停止(不只是终止)的状态 waitpid() 解决了这些问题:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
// 返回子进程 PID、0(WNOHANG 且无变化时)或 -1(出错)
pid 参数的含义:
| > 0 | 等待 PID 等于该值的子进程 |
| == 0 | 等待与调用进程同一进程组的任意子进程 |
| == -1 | 等待任意子进程(等价于 wait()) |
| < -1 | 等待进程组 ID 等于 ` |
options 标志:
| WUNTRACED | 也报告被信号停止的子进程状态 |
| WCONTINUED | 也报告被 SIGCONT 恢复的子进程状态(Linux 2.6.10+) |
| WNOHANG | 非阻塞,若无子进程状态变化则立即返回 0 |
26.1.3 wait 状态值的解析
wait()/waitpid() 返回的 status 值包含了子进程的状态信息,其布局如下:
status 低16位的布局(Linux/x86):
正常退出(exit status 在高8位):
┌────────────────────┬────────────────────┐
│ 退出状态码(bits15-8) │ 0x00 (bits7-0) │
└────────────────────┴────────────────────┘
被信号杀死(信号编号在低7位,bit7是核心转储标志):
┌────────────────────┬──────┬─────────────────┐
│ 未使用(bits15-8) │core │ 终止信号(bits6-0)│
└────────────────────┴──────┴─────────────────┘
被信号停止(信号编号在高8位,低8位为0x7F):
┌────────────────────┬────────────────────┐
│ 停止信号(bits15-8) │ 0x7F (bits7-0) │
└────────────────────┴────────────────────┘
被 SIGCONT 恢复(低16位为0xFFFF):
┌──────────────────────────────────────────┐
│ 0xFFFF │
└──────────────────────────────────────────┘
解析宏(不要直接操作位,用这些宏):
| WIFEXITED(status) | 子进程正常退出则为真 |
| WEXITSTATUS(status) | 获取退出状态码(仅当 WIFEXITED 为真时有效) |
| WIFSIGNALED(status) | 子进程被信号杀死则为真 |
| WTERMSIG(status) | 获取终止信号编号 |
| WCOREDUMP(status) | 子进程产生了核心转储则为真(非标准但广泛支持) |
| WIFSTOPPED(status) | 子进程被停止则为真 |
| WSTOPSIG(status) | 获取停止信号编号 |
| WIFCONTINUED(status) | 子进程被 SIGCONT 恢复则为真(Linux 2.6.10+) |
waitpid() 完整示例
// child_status_demo.cpp
// 创建子进程,父进程用 waitpid 监控其状态变化
// 编译: g++ -o child_status_demo child_status_demo.cpp
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <csignal>
// 打印 wait 状态的含义
static void printWaitStatus(int status)
{
if (WIFEXITED(status)) {
// 正常退出:打印退出码
printf("子进程正常退出,退出码=%d\\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
// 被信号杀死:打印信号编号和名称
printf("子进程被信号 %d (%s) 杀死",
WTERMSIG(status), strsignal(WTERMSIG(status)));
#ifdef WCOREDUMP
if (WCOREDUMP(status))
printf("(产生了核心转储)");
#endif
printf("\\n");
} else if (WIFSTOPPED(status)) {
// 被信号停止
printf("子进程被信号 %d (%s) 停止\\n",
WSTOPSIG(status), strsignal(WSTOPSIG(status)));
} else if (WIFCONTINUED(status)) {
// 被 SIGCONT 恢复
printf("子进程被 SIGCONT 恢复\\n");
} else {
printf("未知状态: 0x%x\\n", (unsigned int)status);
}
}
int main(int argc, char *argv[])
{
int status;
switch (fork()) {
case –1:
perror("fork");
return 1;
case 0: // 子进程
printf("子进程启动,PID=%ld\\n", (long)getpid());
if (argc > 1) {
// 如果提供了命令行参数,以该整数为状态码退出
exit(atoi(argv[1]));
} else {
// 否则循环等待信号
for (;;)
pause();
}
return 1; // 不会到达这里
default: // 父进程:循环等待子进程状态变化
for (;;) {
pid_t childPid = waitpid(–1, &status,
WUNTRACED
#ifdef WCONTINUED
| WCONTINUED
#endif
);
if (childPid == –1) {
perror("waitpid");
return 1;
}
// 打印16进制原始状态值和解析结果
printf("waitpid() 返回: PID=%ld; status=0x%04x (%d,%d)\\n",
(long)childPid,
(unsigned int)status,
status >> 8,
status & 0xff);
printWaitStatus(status);
// 如果子进程已终止(正常或被信号),退出循环
if (WIFEXITED(status) || WIFSIGNALED(status))
break;
}
}
return 0;
}
26.1.4 在信号处理函数中终止进程
如果我们在信号处理函数中捕获了某个终止信号,做完清理后想让父进程知道子进程是被信号杀死的(而不是正常退出),应该这样做:
void handler(int sig)
{
/* 执行清理步骤 */
signal(sig, SIG_DFL); // 重置信号为默认动作(终止进程)
raise(sig); // 再次发送同一信号,这次会真正终止进程
}
为什么不直接 _exit()? 因为 _exit(0) 会让父进程以为子进程成功退出,而不知道是被信号杀死的。
26.1.5 waitid() 系统调用
waitid() 是 System V 衍生的接口,提供比 waitpid() 更精细的控制:
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
// 成功返回 0,出错返回 -1
idtype 参数:
| P_ALL | 等待任意子进程,id 被忽略 |
| P_PID | 等待 PID 等于 id 的子进程 |
| P_PGID | 等待进程组 ID 等于 id 的子进程 |
options 比 waitpid 更细粒度:
| WEXITED | 等待已终止(正常或异常)的子进程 |
| WSTOPPED | 等待被信号停止的子进程 |
| WCONTINUED | 等待被 SIGCONT 恢复的子进程 |
| WNOHANG | 非阻塞 |
| WNOWAIT | 返回信息但不"消费"状态,子进程仍可再次被等待 |
成功时,siginfo_t 结构体中填入以下字段:
| si_code | CLD_EXITED/CLD_KILLED/CLD_STOPPED/CLD_CONTINUED |
| si_pid | 子进程 PID |
| si_signo | 始终为 SIGCHLD |
| si_status | 退出状态码或导致状态改变的信号编号 |
| si_uid | 子进程的真实用户 ID |
可移植地使用 WNOHANG 的写法:
siginfo_t info;
memset(&info, 0, sizeof(siginfo_t)); // 先清零,避免平台差异
if (waitid(P_ALL, 0, &info, WEXITED | WNOHANG) == –1)
perror("waitid");
if (info.si_pid == 0) {
// 没有子进程状态改变
} else {
// 有子进程状态改变,查看 info 中的字段
}
26.1.6 wait3() 和 wait4()
这两个来自 BSD 的函数在 waitpid() 基础上增加了资源使用信息的返回:
#define _BSD_SOURCE
#include <sys/resource.h>
#include <sys/wait.h>
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
等价关系:
wait3
(
status, options, rusage
)
≡
waitpid
(
−
1
,
status, options
)
\\text{wait3}(\\text{status, options, rusage}) \\equiv \\text{waitpid}(-1, \\text{status, options})
wait3(status, options, rusage)≡waitpid(−1,status, options)
wait4
(
pid, status, options, rusage
)
≡
waitpid
(
pid, status, options
)
\\text{wait4}(\\text{pid, status, options, rusage}) \\equiv \\text{waitpid}(\\text{pid, status, options})
wait4(pid, status, options, rusage)≡waitpid(pid, status, options) 额外的 rusage 参数返回子进程使用的 CPU 时间、内存统计等信息。这两个函数不在 SUSv3 标准中,可移植性差,通常避免使用。
26.2 孤儿进程与僵尸进程
孤儿进程(Orphan Process)
父进程先于子进程终止,子进程就成了孤儿。 内核处理方式: 孤儿进程被 init 进程(PID=1)收养
父进程死亡前:
init(1) ─── 父进程(100) ─── 子进程(200)
父进程死亡后:
init(1) ─┬── 父进程(100) [已死]
└── 子进程(200) ← 被 init 收养
子进程中 getppid() 变为返回 1,可以此判断父进程是否还活着。
僵尸进程(Zombie Process)
子进程先于父进程终止,但父进程还没有调用 wait() 来"收尸"。 为什么需要僵尸状态? 内核需要保留子进程的退出状态,以便父进程之后调用 wait() 时能获取到。 僵尸进程的特点:
- 大部分资源已释放,只保留内核进程表中的一条记录(PID、终止状态、资源使用统计)
- 不能被任何信号杀死,包括 SIGKILL(因为它已经"死了")
- 只有父进程调用 wait() 后才真正从系统中消失
- 如果父进程先退出,init 会收养僵尸并自动调用 wait(),清理僵尸
僵尸进程生命周期:
子进程调用 exit()
│
▼
[进入僵尸状态]
大部分资源释放,保留进程表条目
│
├── 父进程调用 wait() ──→ 僵尸被清除,子进程彻底消失
│
└── 父进程先于僵尸退出 ──→ init 收养 ──→ init 调用 wait() ──→ 清除
僵尸进程的危害: 若大量子进程成为僵尸而不被回收,最终填满内核进程表,导致无法创建新进程。
僵尸进程演示代码
// make_zombie_demo.cpp
// 演示僵尸进程的创建和 SIGKILL 无法杀死僵尸
// 编译: g++ -o make_zombie_demo make_zombie_demo.cpp
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <csignal>
#include <cstring>
#include <libgen.h>
int main(int argc, char *argv[])
{
// 禁用 stdout 缓冲
setbuf(stdout, NULL);
printf("父进程 PID=%ld\\n", (long)getpid());
pid_t childPid = fork();
switch (childPid) {
case –1:
perror("fork");
return 1;
case 0: // 子进程:立即退出,成为僵尸
printf("子进程 (PID=%ld) 正在退出…\\n", (long)getpid());
_exit(EXIT_SUCCESS);
default: // 父进程
// 等待3秒,让子进程有时间退出并成为僵尸
sleep(3);
// 用 ps 查看僵尸进程(<defunct> 标记)
printf("\\n— 查看僵尸进程 —\\n");
{
char cmd[256];
// 获取程序名(去掉路径)
char *name = basename(argv[0]);
snprintf(cmd, sizeof(cmd), "ps -o pid,stat,comm | grep %s", name);
system(cmd);
}
// 尝试用 SIGKILL 杀死僵尸(会失败)
printf("\\n向僵尸进程发送 SIGKILL…\\n");
if (kill(childPid, SIGKILL) == –1)
perror("kill");
// 等待信号生效
sleep(3);
printf("\\n发送 SIGKILL 后再次查看:\\n");
{
char cmd[256];
char *name = basename(argv[0]);
snprintf(cmd, sizeof(cmd), "ps -o pid,stat,comm | grep %s", name);
system(cmd);
}
// 现在才调用 wait() 清理僵尸
printf("\\n调用 wait() 清理僵尸…\\n");
pid_t waited = wait(NULL);
printf("wait() 返回 PID=%ld,僵尸已清除\\n", (long)waited);
return 0;
}
}
26.3 SIGCHLD 信号
为什么需要 SIGCHLD?
等待子进程有两种"原始"方式:
26.3.1 SIGCHLD 处理函数
关键陷阱: SIGCHLD 是不排队的标准信号。如果多个子进程快速连续终止,父进程可能只收到一次 SIGCHLD。 错误写法(可能漏掉子进程):
// 错误!每次只处理一个子进程,其他的可能被漏掉
void sigchld_handler_bad(int sig) {
wait(NULL); // 只等一个
}
正确写法(循环等待直到没有僵尸为止):
void sigchld_handler(int sig) {
int saved_errno = errno; // 保存 errno,因为 waitpid 可能修改它
// 循环直到没有更多已终止的子进程
while (waitpid(–1, NULL, WNOHANG) > 0)
continue;
// waitpid 返回 0(没有僵尸了)或 -1(ECHILD,没有子进程了)时退出循环
errno = saved_errno; // 恢复 errno
}
26.3.2 SIGCHLD 用于停止的子进程
设置 SA_NOCLDSTOP 标志时:子进程被信号停止不发送 SIGCHLD 不设置时:子进程停止也发送 SIGCHLD
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_NOCLDSTOP; // 只关心终止,不关心停止
sigaction(SIGCHLD, &sa, NULL);
26.3.3 忽略已死子进程
将 SIGCHLD 显式设置为 SIG_IGN,子进程终止时直接被系统清除,不变成僵尸:
signal(SIGCHLD, SIG_IGN);
重要区别:
- SIGCHLD 的默认动作是忽略(SIG_DFL),不会自动清除僵尸
- 显式设置为 SIG_IGN,才会自动清除僵尸 代价: 父进程无法再通过 wait() 获取子进程的退出状态(因为状态已被丢弃)。
SIGCHLD 完整示例
// multi_sigchld_demo.cpp
// 演示正确处理 SIGCHLD,包括多个子进程快速终止时只收到一次信号的情况
// 编译: g++ -o multi_sigchld_demo multi_sigchld_demo.cpp
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <ctime>
// 全局变量:当前存活的子进程数
static volatile int numLiveChildren = 0;
// 获取时间字符串
static const char *currTime(void)
{
static char buf[64];
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
strftime(buf, sizeof(buf), "%H:%M:%S", tm_info);
return buf;
}
// SIGCHLD 信号处理函数
static void sigchldHandler(int sig)
{
int savedErrno = errno; // 保存 errno
int status;
pid_t childPid;
printf("%s 处理函数:收到 SIGCHLD\\n", currTime());
// 循环等待所有已终止的子进程(WNOHANG = 非阻塞)
while ((childPid = waitpid(–1, &status, WNOHANG)) > 0) {
printf("%s 处理函数:回收子进程 PID=%ld", currTime(), (long)childPid);
if (WIFEXITED(status))
printf(",退出码=%d\\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf(",被信号 %d 杀死\\n", WTERMSIG(status));
else
printf(",状态=0x%x\\n", (unsigned int)status);
numLiveChildren—;
}
if (childPid == –1 && errno != ECHILD)
perror("waitpid");
// 人为延长处理函数执行时间,演示信号不排队的效果
sleep(5);
printf("%s 处理函数:返回\\n", currTime());
errno = savedErrno; // 恢复 errno
}
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "用法: %s 睡眠时间1 [睡眠时间2 …]\\n", argv[0]);
return 1;
}
setbuf(stdout, NULL);
int sigCnt = 0;
numLiveChildren = argc – 1;
// 设置 SIGCHLD 处理函数
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = sigchldHandler;
if (sigaction(SIGCHLD, &sa, NULL) == –1) {
perror("sigaction");
return 1;
}
// 阻塞 SIGCHLD,防止在创建子进程期间就收到信号
// (避免竞态条件:子进程在 sigsuspend 调用前就终止了)
sigset_t blockMask, emptyMask;
sigemptyset(&blockMask);
sigaddset(&blockMask, SIGCHLD);
if (sigprocmask(SIG_SETMASK, &blockMask, NULL) == –1) {
perror("sigprocmask");
return 1;
}
// 创建子进程
for (int j = 1; j < argc; j++) {
switch (fork()) {
case –1:
perror("fork");
return 1;
case 0: // 子进程:睡眠后退出
sleep((unsigned int)atoi(argv[j]));
printf("%s 子进程 %d (PID=%ld) 退出\\n",
currTime(), j, (long)getpid());
_exit(EXIT_SUCCESS);
default: // 父进程:继续创建
break;
}
}
// 父进程:等待所有子进程终止
sigemptyset(&emptyMask);
while (numLiveChildren > 0) {
// sigsuspend:原子地解除 SIGCHLD 阻塞并等待信号
// 解决竞态:确保不会错过在解除阻塞和睡眠之间到达的 SIGCHLD
if (sigsuspend(&emptyMask) == –1 && errno != EINTR) {
perror("sigsuspend");
return 1;
}
sigCnt++;
}
printf("%s 所有 %d 个子进程已终止;SIGCHLD 被捕获 %d 次\\n",
currTime(), argc – 1, sigCnt);
return 0;
}
26.4 本章总结
进程终止和子进程监控知识体系:
进程终止
├── 正常终止
│ ├── _exit(status) ← 直接终止,不做 stdio 清理
│ └── exit(status) ← 先调用退出处理函数、刷新 stdio,再调 _exit
│
└── 异常终止
└── 被信号杀死(可能产生核心转储)
退出处理函数
├── atexit(func) ← 标准,无参数
└── on_exit(func, arg) ← GNU 扩展,可传参数和接收状态码
监控子进程
├── wait() ← 等待任意子进程,阻塞
├── waitpid() ← 等待特定子进程,可非阻塞,可监控停止/继续
├── waitid() ← 更精细的控制(System V 风格)
└── wait3()/wait4() ← 额外返回资源使用信息(BSD 风格,非标准)
特殊情况
├── 孤儿进程 ← 父进程先死,被 init 收养
└── 僵尸进程 ← 子进程先死,父进程未 wait()
└── 危害:填满进程表,无法用信号杀死
└── 解决:
├── 父进程调用 wait()/waitpid()
├── 建立 SIGCHLD 处理函数,循环调用 waitpid(WNOHANG)
└── 将 SIGCHLD 显式设为 SIG_IGN(子进程自动清除,但无法获取状态)
习题解答
25-1: 若子进程调用 exit(-1),父进程通过 WEXITSTATUS() 看到的退出状态是多少?
exit(-1) 中,-1 作为 int 传入,但最终只有低8位传给父进程。-1 的二进制补码低8位是 0xFF,即十进制 255。所以父进程看到的是 255。 26-1: 验证当子进程的父进程终止后,getppid() 返回 1。
// orphan_demo.cpp
// 验证孤儿进程被 init 收养后 getppid() 返回 1
// 编译: g++ -o orphan_demo orphan_demo.cpp
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
int main(void)
{
pid_t childPid = fork();
switch (childPid) {
case –1:
perror("fork");
return 1;
case 0: // 子进程
// 先睡一秒,等父进程先退出
sleep(1);
printf("子进程 PID=%ld, 父进程 PID=%ld\\n",
(long)getpid(), (long)getppid());
// 此时父进程已退出,getppid() 应返回 1(init 的 PID)
if (getppid() == 1)
printf("已被 init 收养(getppid() == 1)\\n");
return 0;
default: // 父进程:立即退出
printf("父进程 PID=%ld 即将退出\\n", (long)getpid());
return 0;
}
}






