欢迎光临
我们一直在努力

The Linux Programming interface学习:Linux 信号处理函数设计

一、信号处理函数设计的两大黄金原则

设计信号处理函数时,最重要的一条原则是:尽量写得简单。原因是越复杂的处理函数,越容易引入难以调试的竞态条件(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_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 无效的系统调用


类别系统调用SA_RESTART 有效?
等待子进程 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 内核区分两种(以及一种新增的)睡眠状态:

状态ps 中显示含义信号能打断吗?
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 速查


    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 标识:

    时钟ID特点可设置用途
    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_notify 值通知方式适用场景
    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 特性对比


    特性alarm()setitimer()POSIX timer_*timerfd_*
    定时器数量 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配合使用

    核心知识点:

  • 每个进程只有3种 setitimer() 定时器各一个;POSIX 定时器可以有任意多个
  • nanosleep() 在高频信号下有误差累积问题;用 clock_nanosleep() + TIMER_ABSTIME 解决
  • timerfd 是最现代的接口,可与 epoll 配合实现事件驱动的定时器处理
  • 定时器精度受内核时钟粒度(jiffy)限制,高精度定时器需要 CONFIG_HIGH_RES_TIMERS
  • 周期性定时器不会产生累积漂移
  • 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() 对比


    特性fork()vfork()
    内存复制 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 内核版本fork() 后谁先运行原因
    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

    81=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() 系统调用,或通过 exit() 库函数间接调用
  • _exit() 系统调用

    #include <unistd.h>
    void _exit(int status);

    • status 参数:进程的终止状态码,父进程可以通过 wait() 获取
    • 实际只使用 status 的低8位(即

      0

      255

      0 \\sim 255

      0255

    • 调用后进程一定会终止,永不返回 约定规范:

    状态值含义
    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

      2311),实际上内存先耗尽 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() 的状态码
  • 无法给处理函数传参数 on_exit() 解决了这两个问题,但可移植性差,不建议在跨平台代码中使用。
  • 退出处理函数示例(完整可运行代码)

    // 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() 的行为:

  • 若没有已终止的子进程,阻塞直到某个子进程终止
  • 若已有子进程终止,立即返回
  • 将子进程终止状态写入 status 指向的整数(若 status 非 NULL)
  • 内核将子进程的 CPU 时间、资源使用统计累加到父进程的总计中 等待所有子进程的标准写法:
  • // 循环等待所有子进程
    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 参数的含义:

    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 参数:

    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?

    等待子进程有两种"原始"方式:

  • 阻塞 wait():父进程被卡住无法干其他事
  • 轮询 waitpid(WNOHANG):浪费 CPU,代码复杂 SIGCHLD 提供了异步通知机制:子进程状态改变时,内核向父进程发送 SIGCHLD,父进程在信号处理函数中调用 waitpid() 处理。
  • 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;
    }
    }

    赞(0)
    未经允许不得转载:171主机测评 » The Linux Programming interface学习:Linux 信号处理函数设计
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址