从“快递管家”说起:sigaction的故事
<摘要>sigaction是Linux/POSIX标准下的核心信号处理系统调用,主要用于设置或获取进程对特定信号的处理策略,功能远优于简化版的signal函数。其核心价值在于通过struct sigaction结构体实现精细化配置:支持自定义信号处理函数(普通/带附加信息两种模式)、设置信号处理期间的临时屏蔽清单、控制系统调用重启等行为,还能处理实时信号并传递附加数据。常见适用场景包括:捕获Ctr
从“快递管家”说起:sigaction的故事
你有没有过这样的经历?在终端里运行一个程序,随手按了Ctrl+C,程序“嗖”地一下就退出了——这背后其实是Linux的“信号机制”在干活。就像快递员敲门送快递,进程收到“信号”后,要么按默认规则处理(比如Ctrl+C对应“终止进程”),要么忽略,要么执行你指定的操作。
但如果只是简单处理,用signal函数就够了——可它就像个临时快递员,只会说“要么收要么扔”,遇到复杂情况就慌了:比如处理快递时要不要拦住其他快递?收完快递要不要通知邻居?能不能让快递员带点额外东西?这时候,就需要sigaction这位“专业快递管家”出场了——它能把所有规则制定得明明白白,还能应对各种特殊需求。
今天咱们就从“管家的日常工作”入手,一点点揭开sigaction的面纱:先搞懂它是干啥的,再拆明白它的“工作手册”(结构体),然后看它怎么处理不同“快递”(信号),最后上手实操几个例子,让你彻底学会用它管好进程的“信号快递”。
一、sigaction是谁?能干啥?——基本介绍与用途
1. 一句话定位:信号处理的“全能管家”
sigaction本质是Linux内核提供的系统调用(不是库函数),主要干两件事:
- 给某个信号“定规矩”:比如“收到SIGINT(Ctrl+C)时,别终止进程,打印一句提示”;
- 查某个信号的“老规矩”:比如“看看现在SIGCHLD的处理方式是啥,我之后要恢复回来”。
它比signal函数强在哪?举个例子:signal就像你跟快递员说“快递放门口”,而sigaction会跟快递员说“快递放门口,放的时候别让其他快递员来敲门,放完给我发个短信,要是我不在家就明天再来”——细节拉满,稳定性也更高。
2. 常见“工作场景”:这些时候必须找它
sigaction不是“花瓶”,在实际开发中,只要涉及信号的复杂处理,基本都离不开它。比如:
场景1:终端程序“抗造”——捕获Ctrl+C不退出
你写了个命令行工具(比如文件下载器),用户按Ctrl+C可能是误操作,这时候不能直接退出,得提示“确定要退出吗?未完成的任务会丢失”。这时候就用sigaction设置SIGINT的处理函数,替代默认的“终止进程”动作。
场景2:多进程“不闹鬼”——处理子进程退出避免僵尸
父进程创建子进程后,如果子进程先退出,父进程没及时“收尸”,子进程就会变成“僵尸进程”(Z状态),占着PID不释放。这时候子进程退出会发SIGCHLD信号,用sigaction处理这个信号,在处理函数里调用waitpid回收子进程,就能避免僵尸。
场景3:进程间“传小纸条”——用实时信号传递数据
普通信号只能“打个招呼”(比如“我要终止你”),但实时信号(32-64号)能附带数据——比如传感器进程给控制进程发信号时,顺带把“温度=25℃”的整数数据传过去。这时候必须用sigaction的sa_sigaction处理函数,才能接收这些附加信息。
场景4:系统调用“不中断”——让read/write自动续上
比如程序用read从键盘等输入,这时候收到信号,read会被中断,返回-1并设置errno=EINTR。如果用sigaction设置SA_RESTART标志,read会自动“续上”,不用你手动写代码重试——省了不少麻烦。
3. 对比signal:为啥选sigaction?
可能有人会问:“我用signal也能处理信号,为啥非要学sigaction?”咱们用“快递服务”对比一下就懂了:
| 功能点 | signal(临时快递员) | sigaction(专业管家) |
|---|---|---|
| 处理期间拦其他快递? | 只拦当前快递,不能选 | 想拦哪个拦哪个(sa_mask) |
| 快递员中断送货后重试? | 看心情(不同系统行为不同) | 明确设置(SA_RESTART) |
| 带附加东西(数据)? | 不行,只能传个信 | 可以(实时信号+sa_sigaction) |
| 查之前的规则? | 得自己记,麻烦 | 自动存到oldact,随时查 |
| 跨系统能用? | 不一定(Linux和BSD不一样) | 能(符合POSIX标准) |
简单说:signal适合“临时用用”,比如简单忽略某个信号;sigaction适合“正经开发”,尤其是多进程、实时系统、终端工具这些场景——稳定、灵活、兼容性强。
二、找sigaction之前:得知道它在哪——声明与来源
要请“管家”干活,得先知道它的“办公地址”(头文件)和“所属公司”(标准/库),不然编译时会报错。
1. 头文件:必须包含<signal.h>
sigaction的函数声明、struct sigaction结构体定义、还有各种信号宏(比如SIGINT、SIGCHLD),都在<signal.h>这个头文件里。所以写代码时,第一行必须加:
#include <signal.h>
少了这个,编译器会报“undefined reference to sigaction”或者“unknown type name struct sigaction”——相当于没找到管家的办公室,自然没法干活。
2. 标准与库:POSIX亲儿子,glibc自带
- 标准归属:sigaction是POSIX.1-2001标准定义的系统调用,所以在所有符合POSIX的系统上都能用(比如Linux、macOS、FreeBSD),不用担心跨系统兼容性问题。
- 库依赖:在Linux上,sigaction由glibc(GNU C库)封装,编译时不需要额外加链接参数(比如
-lm这种),直接用gcc编译就行。比如:
这里gcc -o sigdemo sigdemo.c -Wall-Wall是显示警告,建议加上,能帮你发现比如“没初始化结构体”这种小问题。
3. 函数原型:三个参数,一个返回值
先看sigaction的“名片”(函数原型):
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
咱们先简单扫一眼:
- 返回值是int:成功返回0,失败返回-1(同时设置errno);
- 三个参数:
- signum:要处理的“快递编号”(信号编号,比如SIGINT=2);
- act:新的“处理规则”(如果非NULL,就是给信号定新规矩);
- oldact:旧的“处理规则”(如果非NULL,就把原来的规矩存这里,方便之后恢复)。
这里最核心的是struct sigaction——这是sigaction的“工作手册”,里面写满了处理信号的各种细节。接下来咱们就拆了这本手册,看看里面都有啥。
三、sigaction的“工作手册”:struct sigaction结构体详解
如果把sigaction比作管家,那struct sigaction就是它的“工作手册”——里面每一项都规定了“怎么处理这个信号”。咱们先看手册的“目录”(结构体定义):
struct sigaction {
// 1. 处理函数:二选一
void (*sa_handler)(int); // 普通处理函数(大部分情况用这个)
void (*sa_sigaction)(int, siginfo_t *, void *); // 带附加信息的处理函数(实时信号用)
// 2. 信号屏蔽清单:处理信号时要拦住的其他信号
sigset_t sa_mask;
// 3. 行为开关:控制处理信号的细节
int sa_flags;
// 4. 已废弃:不用管,glibc自动处理
void (*sa_restorer)(void);
};
注意:sa_handler和sa_sigaction是“二选一”——不能同时用,选哪个由sa_flags里的SA_SIGINFO标志决定。另外,sa_restorer是历史遗留字段,现在不用设置,设了也没用,甚至可能出错,直接忽略就行。
下面咱们逐页拆解这本“工作手册”:
1. 处理函数:sa_handler vs sa_sigaction——选哪个?
这俩是“信号处理的核心动作”,相当于管家说“收到这个快递,我要做什么”。
(1)普通处理函数:sa_handler——日常够用
sa_handler是个函数指针,格式是:
void (*sa_handler)(int sig);
- 参数
sig:收到的信号编号(比如SIGINT=2),方便一个处理函数处理多个信号; - 取值有三种:
① SIG_DFL:用“默认动作”——比如SIGINT默认是“终止进程”,SIGQUIT默认是“终止进程+生成core dump文件”;
② SIG_IGN:“忽略这个信号”——比如设置SIGINT为SIG_IGN,按Ctrl+C就没用了;
③ 自定义函数指针:比如写个void my_handler(int sig),收到信号就执行这个函数。
举个例子:想让程序忽略Ctrl+C,就这么设:
struct sigaction act;
memset(&act, 0, sizeof(act)); // 先清空手册
act.sa_handler = SIG_IGN; // 忽略SIGINT
sigaction(SIGINT, &act, NULL); // 应用规则
(2)带附加信息的处理函数:sa_sigaction——复杂场景用
sa_sigaction也是函数指针,但参数更多,格式是:
void (*sa_sigaction)(int sig, siginfo_t *info, void *ucontext);
比sa_handler多了两个参数,专门用来处理“需要详细信息”的场景(比如实时信号):
sig:还是信号编号;info:指向siginfo_t结构体,里面存满了信号的“附加信息”——比如谁发的信号(info->si_pid)、信号原因(info->si_code)、附带的数据(info->si_value);ucontext:指向进程上下文(ucontext_t结构体),一般用不上,除非要恢复信号中断前的CPU状态(比如做调试工具)。
要想用sa_sigaction,必须在sa_flags里加SA_SIGINFO标志——相当于告诉管家“我要用高级模式,给我更多信息”。
举个简单的sa_sigaction函数:
void my_rt_handler(int sig, siginfo_t *info, void *ucontext) {
printf("收到信号:%d\n", sig);
printf("发送信号的进程PID:%d\n", info->si_pid); // 打印谁发的信号
printf("附带的整数数据:%d\n", info->si_value.sival_int); // 打印附加数据
}
2. 信号屏蔽清单:sa_mask——处理时别被打扰
sa_mask是sigset_t类型(信号集),相当于“处理这个信号时,要拦住的其他快递清单”——避免处理过程中被其他信号打断,导致逻辑错乱。
比如:处理SIGINT时,要是再按一次Ctrl+C,会再次触发处理函数,导致“重入”(函数还没执行完又被调用),可能会修改全局变量出错。这时候就把SIGINT加入sa_mask,处理期间再收到SIGINT,会暂时“存起来”(进入未决信号集),等当前处理完再处理。
怎么操作sa_mask?用这几个函数:
sigemptyset(&sa_mask):清空清单(初始必须做,不然有随机值);sigaddset(&sa_mask, SIGXXX):把SIGXXX加入清单(拦住这个信号);sigdelset(&sa_mask, SIGXXX):把SIGXXX从清单中删除(不拦了);sigfillset(&sa_mask):把所有信号加入清单(拦住所有信号,谨慎用);sigismember(&sa_mask, SIGXXX):检查SIGXXX是否在清单里(返回1是,0不是)。
举个例子:处理SIGINT时,拦住SIGINT和SIGQUIT(避免被Ctrl+\打断):
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = my_handler;
// 初始化屏蔽清单
sigemptyset(&act.sa_mask); // 清空
sigaddset(&act.sa_mask, SIGINT); // 拦住SIGINT(避免重入)
sigaddset(&act.sa_mask, SIGQUIT); // 拦住SIGQUIT(避免干扰)
act.sa_flags = SA_RESTART;
sigaction(SIGINT, &act, NULL);
注意:sa_mask只在“处理当前信号期间”有效——处理完信号后,会自动恢复原来的屏蔽清单,不用你手动改回来。
3. 行为开关:sa_flags——细节控的福音
sa_flags是整数,相当于“管家的行为开关”——每个开关控制一个细节,多个开关用“|”(或运算)组合。比如SA_RESTART | SA_SIGINFO就是同时打开两个开关。
下面是最常用的几个开关,必须记住:
(1)SA_RESTART:系统调用被中断后自动重启
这是最常用的开关之一!比如程序用read(STDIN_FILENO, buf, 1024)从键盘读输入,这时候收到信号,read会被中断,返回-1并设置errno=EINTR。如果没设SA_RESTART,你得手动判断errno,然后重试read:
// 没设SA_RESTART的情况,需要手动重试
ssize_t n;
while ((n = read(STDIN_FILENO, buf, 1024)) == -1 && errno == EINTR) {
// 啥也不用干,继续循环重试
}
但设了SA_RESTART后,read会自动“续上”——被中断后不用你管,内核会帮你重新调用read,直到成功或真的出错(比如文件结束)。省了不少代码!
(2)SA_NOCLDWAIT:处理SIGCHLD时自动回收子进程
父进程处理SIGCHLD时,设了这个开关,子进程退出后会被内核自动回收,不会变成僵尸进程——甚至不用在处理函数里调用waitpid!
举个例子:
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = SIG_IGN; // 甚至可以忽略SIGCHLD
act.sa_flags = SA_NOCLDWAIT; // 关键开关
sigaction(SIGCHLD, &act, NULL);
这样一来,子进程退出后,内核直接回收,父进程连waitpid都不用写——特别适合“父进程不管子进程退出状态”的场景。
注意:设了SA_NOCLDWAIT后,调用waitpid会返回-1,errno=ECHILD(表示没有子进程可回收),这是正常的。
(3)SA_SIGINFO:用sa_sigaction处理函数
前面说过,sa_handler和sa_sigaction二选一——要想用sa_sigaction,必须设这个开关。不然sigaction会默认用sa_handler,哪怕你赋值了sa_sigaction也没用。
(4)SA_NODEFER:处理信号时不屏蔽当前信号
默认情况下,处理某个信号时,内核会自动把这个信号加入sa_mask(哪怕你没加),避免重入。但设了SA_NODEFER后,就不屏蔽当前信号了——处理期间再收到这个信号,会再次触发处理函数。
非常危险! 比如处理SIGINT时设了这个开关,按两次Ctrl+C就会导致处理函数重入,如果函数里修改全局变量,很可能会出问题。除非有特殊需求(比如实时系统需要快速响应),否则别用。
(5)SA_RESETHAND:处理完信号后恢复默认动作
设了这个开关,第一次收到信号时执行自定义函数,处理完后,信号的处理方式会自动恢复成SIG_DFL——第二次收到信号就会执行默认动作。
比如:
act.sa_handler = my_handler;
act.sa_flags = SA_RESETHAND; // 设这个开关
第一次按Ctrl+C,执行my_handler;第二次按Ctrl+C,就会执行默认动作(终止进程)。适合“只需要处理一次信号”的场景。
4. 已废弃的sa_restorer:别碰!
sa_restorer是早期UNIX系统用来恢复栈的字段,现在glibc会自动处理,用户根本不用管。如果你给它赋值,编译器可能会报警告,甚至运行出错。所以写代码时,直接忽略这个字段——初始化结构体时用memset清空就行,不用特意赋值。
四、sigaction的“反馈机制”:返回值与错误码
管家干活完了,会给你一个“反馈”——返回值。成功还是失败,失败的话是啥原因,都能从返回值和errno里看出来。
1. 返回值含义:0成功,-1失败
- 返回0:说明sigaction成功执行了——要么成功设置了新规则,要么成功把旧规则存到了
oldact里。 - 返回-1:说明失败了,这时候必须看
errno才能知道原因(需要包含<errno.h>头文件)。
2. 常见错误码:为啥会失败?
sigaction失败的原因不多,但都很常见,必须记住:
(1)EINVAL:信号编号无效或标志位错误
这是最常见的错误,原因有两种:
- ①
signum无效:比如传了0(信号编号从1开始)、传了大于SIGRTMAX(通常是64)的数,或者传了SIGKILL(9号)、SIGSTOP(19号)——这两个信号是“皇帝的圣旨”,不能修改处理方式,只能执行默认动作; - ②
sa_flags里有无效的标志位:比如自己定义了一个不存在的标志(比如0x1234)。
举个错误例子:
struct sigaction act;
act.sa_handler = SIG_IGN;
// 错误:试图修改SIGKILL的处理方式
if (sigaction(SIGKILL, &act, NULL) == -1) {
perror("sigaction failed"); // 会打印:sigaction failed: Invalid argument
exit(EXIT_FAILURE);
}
(2)EFAULT:act或oldact指针无效
比如act是野指针(没初始化的指针),或者指向只读内存(比如字符串常量),内核没法读写act或oldact的内容,就会报这个错。
错误例子:
struct sigaction *act = NULL; // 野指针
// 错误:act是NULL,内核没法写
if (sigaction(SIGINT, act, NULL) == -1) {
perror("sigaction failed"); // 打印:sigaction failed: Bad address
}
(3)EACCESS:没有权限修改处理方式
除了SIGKILL和SIGSTOP,还有一种情况会报这个错:如果进程在“强制模式”下运行(比如用prctl设置了PR_SET_NO_NEW_PRIVS),可能会没有权限修改某些信号的处理方式。不过这种情况很少见,大部分时候还是因为改了SIGKILL/SIGSTOP。
3. 如何正确处理返回值?
必须检查返回值! 很多新手写代码不检查,出了错都不知道为啥。正确的做法是:
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
act.sa_handler = my_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART;
// 检查返回值
if (sigaction(SIGINT, &act, &oldact) == -1) {
// 用perror打印错误信息(自动拼接errno对应的描述)
perror("Failed to set SIGINT handler");
exit(EXIT_FAILURE); // 退出程序,或做其他错误处理
}
perror会把错误码转换成人类能看懂的字符串,比如Invalid argument(EINVAL)、Bad address(EFAULT),方便排查问题。
五、sigaction的“操作指南”:参数详解
前面咱们看了函数原型,现在再详细拆解三个参数——知道每个参数能传啥、不能传啥,才能避免踩坑。
1. 第一个参数:signum——信号的“身份证号”
signum是要处理的信号编号,相当于快递的“单号”——必须准确,不然管家不知道要处理哪个快递。
(1)信号编号的范围
Linux的信号分两类:
- 普通信号:1~31号,比如SIGINT(2)、SIGTERM(15)、SIGCHLD(17);
- 实时信号:3264号(`SIGRTMIN`
SIGRTMAX),比如SIGRTMIN(32)、SIGRTMIN+1(33)。
注意:不同系统的SIGRTMIN可能不一样(比如Solaris是33),所以别硬编码32,用SIGRTMIN宏更通用。
(2)不能碰的信号:SIGKILL(9)和SIGSTOP(19)
这两个信号是“内核的底线”——无论进程做什么,收到SIGKILL就必须终止,收到SIGSTOP就必须暂停,不能修改处理方式(不能忽略,不能自定义函数)。哪怕你调用sigaction修改,也会返回-1,errno=EINVAL。
记住:“kill -9 进程号”是强制终止进程的终极手段,就是因为SIGKILL不能被拦截。
(3)常见信号编号与含义
为了方便你用,我整理了开发中最常用的几个信号:
| 信号名 | 编号 | 含义与默认动作 | 常用场景 |
|---|---|---|---|
| SIGINT | 2 | 终端中断(Ctrl+C)→ |
更多推荐




所有评论(0)