从“快递签收规则”看 sigaction:信号处理的“总开关”
一、从“快递签收规则”看 sigaction:信号处理的“总开关”
生活中,我们总会收到各种通知——快递到了、电话来了、闹钟响了。为了应对这些通知,我们会提前制定规则:快递放驿站、陌生电话拒接、闹钟响后起床。在 Linux 系统中,“信号”就像这些通知(比如程序出错时的 SIGSEGV、用户按 Ctrl+C 发送的 SIGINT),而 sigaction 结构体就是用来定义“信号处理规则”的核心工具。
简单说,sigaction 结构体是 Linux 系统中描述信号处理方式的“数据模板”,它包含了四个关键信息:用什么函数处理信号(处理者)、处理期间要屏蔽哪些其他信号(免打扰设置)、处理时的特殊行为(附加规则)、以及一个遗留的恢复函数(历史产物)。通过这个结构体,我们能精确控制程序对每个信号的响应方式,比早期的 signal() 函数更灵活、更强大。
比如,当程序收到 SIGINT 信号(Ctrl+C)时,你可以通过 sigaction 定义:调用自定义函数打印“收到中断信号”,处理期间不响应 SIGQUIT 信号(避免被打断),并且处理完后自动恢复默认行为——这些细致的控制都离不开 sigaction 结构体的设计。
二、sigaction 结构体的“家庭成员”:各成员详解
sigaction 结构体定义在 <signal.h>
头文件中,其原型如下(不同系统可能略有差异,但核心成员一致):
struct sigaction {void (*sa_handler)(int); // 信号处理函数(或特殊值)sigset_t sa_mask; // 信号屏蔽集(处理期间屏蔽的信号)int sa_flags; // 标志位(控制处理行为的选项)void (*sa_restorer)(void); // 恢复函数(已废弃,不建议使用)
};
这四个成员就像“处理规则表”的四个栏目,共同决定了信号的处理逻辑。
1. sa_handler:信号的“处理者”
sa_handler 是一个函数指针,指向信号发生时要执行的函数,原型为 void (*)(int)
,参数是信号编号(比如 SIGINT 是 2,SIGTERM 是 15)。除了自定义函数,它还可以取两个特殊值:
- SIG_DFL:使用默认处理方式。不同信号的默认行为不同,比如 SIGINT 默认是终止程序,SIGQUIT 默认是终止程序并生成核心转储文件。
- SIG_IGN:忽略该信号。程序收到后不做任何处理,就像没收到一样(但有两个信号无法忽略:SIGKILL(9)和 SIGSTOP(19),这是系统保留的“终极控制信号”)。
举个例子:如果 sa_handler = SIG_IGN
,那么程序会忽略对应的信号;如果 sa_handler = my_handler
,则会调用 void my_handler(int signo)
函数处理。
2. sa_mask:处理期间的“免打扰名单”
sa_mask 是一个 sigset_t 类型的信号集(可以理解为一个“信号黑名单”),用于指定在处理当前信号时,哪些信号需要被暂时屏蔽(阻塞)。
比如,假设程序正在处理 SIGINT 信号,而 sa_mask 中包含 SIGQUIT,那么在处理 SIGINT 的过程中,如果收到 SIGQUIT 信号,系统会暂时把它“存起来”,等 SIGINT 处理完再处理(除非被忽略或设置了其他规则)。
需要注意的是,当前正在处理的信号会被自动加入 sa_mask 中(即使不手动添加),避免同一信号被嵌套处理。比如处理 SIGINT 时,再次收到 SIGINT 会被阻塞,直到当前处理完成。
3. sa_flags:处理行为的“附加规则”
sa_flags 是一个整数,通过设置不同的标志位(用 | 组合),可以改变信号处理的行为。常用的标志有:
- SA_RESTART:使被信号中断的系统调用自动重启。比如程序正在调用 read() 等待输入,此时收到信号,处理完后 read() 会继续等待,而不是返回 -1 并设置 errno=EINTR。
- SA_NOCLDSTOP:针对 SIGCHLD 信号(子进程状态变化时发送),当子进程暂停或继续时,不发送 SIGCHLD 信号(只在子进程终止时发送)。
- SA_NOCLDWAIT:子进程终止时不成为僵尸进程,系统会自动回收其资源(此时无法用 wait() 获取子进程状态)。
- SA_NODEFER:默认情况下,处理信号时会屏蔽该信号本身(避免嵌套),设置此标志后,不屏蔽当前信号(可能导致同一信号被多次嵌套处理,慎用)。
- SA_ONSTACK:使用替代栈(通过 sigaltstack() 设置)处理信号,避免因栈溢出导致程序崩溃。
- SA_SIGINFO:使用 sa_sigaction 成员作为处理函数(而不是 sa_handler),该函数能接收更详细的信号信息(比如信号来源、附加数据等)。此时需要将结构体视为:
struct sigaction {void (*sa_sigaction)(int, siginfo_t *, void *); // 带详细信息的处理函数sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void);
};
(注:sa_handler 和 sa_sigaction 共用同一块内存,设置 SA_SIGINFO 后,应使用 sa_sigaction)
4. sa_restorer:被遗忘的“历史遗留者”
sa_restorer 是一个函数指针,用于在信号处理完成后恢复某些系统状态。这个成员是早期 libc 实现的遗留物,现在已经被废弃,在现代 Linux 系统中不需要设置(设置了也会被忽略),因此实际编程中通常将其设为 NULL 或忽略。
三、“激活”规则:sigaction() 系统调用
定义好 sigaction 结构体后,需要通过 sigaction() 系统调用来“激活”这个规则,让系统知道如何处理指定信号。该函数原型如下:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- signum:要设置处理规则的信号编号(如 SIGINT、SIGTERM 等,不能是 SIGKILL 或 SIGSTOP,因为它们无法被捕获或修改处理方式)。
- act:指向新的 sigaction 结构体,包含要设置的处理规则。如果为 NULL,则只获取当前规则,不修改。
- oldact:用于保存原来的 sigaction 结构体(以便后续恢复),如果为 NULL,则不保存。
- 返回值:成功返回 0,失败返回 -1 并设置 errno(如 EINVAL 表示信号编号无效)。
比如,要为 SIGINT 信号设置处理规则,步骤是:
- 初始化 sigaction 结构体,设置 sa_handler、sa_mask、sa_flags 等。
- 调用 sigaction(SIGINT, &new_act, &old_act) 激活新规则,同时保存旧规则。
- (可选)需要时,调用 sigaction(SIGINT, &old_act, NULL) 恢复旧规则。
四、使用示例:sigaction 结构体的实战场景
示例 1:基础用法——自定义处理 SIGINT 信号
这个示例展示如何用 sigaction 结构体自定义处理 SIGINT 信号(Ctrl+C),替代默认的终止程序行为。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>// 自定义信号处理函数
void sigint_handler(int signo) {printf("\n收到 SIGINT 信号(编号:%d),我被中断了,但不会退出!\n", signo);printf("再按 Ctrl+C 试试...\n");
}int main() {struct sigaction act;// 1. 设置处理函数act.sa_handler = sigint_handler; // 指向自定义函数// 2. 初始化信号屏蔽集(处理期间屏蔽 SIGQUIT)sigemptyset(&act.sa_mask); // 先清空sigaddset(&act.sa_mask, SIGQUIT); // 加入 SIGQUIT(Ctrl+\)// 3. 设置标志位(无特殊行为)act.sa_flags = 0;// 4. 激活规则(处理 SIGINT 信号)if (sigaction(SIGINT, &act, NULL) == -1) {perror("sigaction 设置失败");exit(EXIT_FAILURE);}printf("程序启动,按 Ctrl+C 发送 SIGINT 信号,按 Ctrl+\\ 发送 SIGQUIT 信号,按 Ctrl+Z 退出...\n");// 无限循环,等待信号while (1) {pause(); // 暂停进程,等待信号printf("信号处理完成,继续等待...\n");}return 0;
}
代码说明:
- 定义
sigint_handler
函数,收到 SIGINT 时打印提示信息,不终止程序。 - 初始化 sigaction 结构体:sa_handler 设为自定义函数,sa_mask 加入 SIGQUIT(处理 SIGINT 时屏蔽 Ctrl+\),sa_flags 为 0(默认行为)。
- 用 sigaction() 激活规则,然后进入循环等待信号(pause() 会暂停进程,直到收到信号)。
示例 2:SA_RESTART 标志——让系统调用自动重启
这个示例展示 SA_RESTART 标志的作用:当系统调用(如 read())被信号中断时,会自动重启,而不是返回错误。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>// 信号处理函数
void sigusr1_handler(int signo) {printf("\n收到 SIGUSR1 信号(编号:%d),正在处理...\n", signo);sleep(2); // 模拟处理耗时printf("SIGUSR1 处理完成\n");
}int main() {struct sigaction act;char buf[1024];// 设置处理函数act.sa_handler = sigusr1_handler;sigemptyset(&act.sa_mask);act.sa_flags = SA_RESTART; // 关键:设置 SA_RESTART 标志// 激活 SIGUSR1 信号的处理规则if (sigaction(SIGUSR1, &act, NULL) == -1) {perror("sigaction 设置失败");exit(EXIT_FAILURE);}printf("程序启动,PID = %d\n", getpid());printf("请输入字符串(输入后按回车):\n");// 调用 read() 读取标准输入(可能被信号中断)ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1);if (n == -1) {perror("read 失败");exit(EXIT_FAILURE);}buf[n] = '\0';printf("你输入了:%s\n", buf);return 0;
}
代码说明:
- 定义
sigusr1_handler
函数,处理 SIGUSR1 信号(用户自定义信号),模拟耗时 2 秒。 - 初始化 sigaction 结构体时设置
sa_flags = SA_RESTART
,表示被信号中断的系统调用会自动重启。 - 程序启动后,调用 read() 等待用户输入。此时如果另一个终端发送 SIGUSR1 信号(
kill -USR1 程序PID
),程序会先处理信号,处理完成后 read() 会继续等待输入,而不是返回错误。
如果去掉 SA_RESTART 标志,信号处理完成后 read() 会返回 -1 并设置 errno=EINTR,程序会打印“read 失败”。
示例 3:SA_SIGINFO 标志——获取详细信号信息
这个示例展示 SA_SIGINFO 标志的用法:使用 sa_sigaction 作为处理函数,获取信号的详细信息(如发送者 PID、附加数据等)。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>// 带详细信息的信号处理函数(SA_SIGINFO 模式)
void siginfo_handler(int signo, siginfo_t *info, void *context) {printf("\n收到信号:%d\n", signo);printf("信号来源 PID:%d\n", info->si_pid); // 发送信号的进程 PIDprintf("发送者 UID:%d\n", info->si_uid); // 发送者的用户 IDif (signo == SIGUSR1) {printf("这是 SIGUSR1 信号\n");} else if (signo == SIGUSR2) {printf("这是 SIGUSR2 信号\n");}
}int main() {struct sigaction act;// 设置 SA_SIGINFO 模式:使用 sa_sigaction 作为处理函数act.sa_sigaction = siginfo_handler;sigemptyset(&act.sa_mask);act.sa_flags = SA_SIGINFO; // 关键:启用详细信息模式// 为 SIGUSR1 和 SIGUSR2 设置相同的处理规则if (sigaction(SIGUSR1, &act, NULL) == -1 || sigaction(SIGUSR2, &act, NULL) == -1) {perror("sigaction 设置失败");exit(EXIT_FAILURE);}printf("程序启动,PID = %d\n", getpid());printf("发送 SIGUSR1:kill -USR1 %d\n", getpid());printf("发送 SIGUSR2:kill -USR2 %d\n", getpid());printf("等待信号...\n");// 无限循环等待信号while (1) {pause();}return 0;
}
代码说明:
- 定义
siginfo_handler
函数,参数包括信号编号(signo)、信号信息结构体(siginfo_t *info)、上下文(void *context),能获取比 sa_handler 更详细的信息。 - 初始化 sigaction 结构体时,
sa_sigaction
设为自定义函数,sa_flags
设为 SA_SIGINFO,启用详细信息模式。 - 为 SIGUSR1 和 SIGUSR2 信号设置处理规则,启动后在另一个终端发送这两个信号,程序会打印信号来源的 PID、UID 等信息。
siginfo_t 结构体包含多种信号相关信息,比如对于 SIGSEGV(段错误),可以通过 info->si_addr
获取导致错误的内存地址。
五、编译与运行:让示例代码跑起来
三个示例均为 C 代码,用 gcc 编译即可,无需链接额外库(信号处理函数属于标准 C 库)。
编译命令
- 示例 1:
gcc -o sigaction_basic sigaction_basic.c -Wall
- 示例 2:
gcc -o sigaction_restart sigaction_restart.c -Wall
- 示例 3:
gcc -o sigaction_siginfo sigaction_siginfo.c -Wall
运行方法
-
示例 1:
./sigaction_basic
运行后按 Ctrl+C(发送 SIGINT),会触发自定义处理函数;按 Ctrl+\(发送 SIGQUIT),如果此时正在处理 SIGINT,SIGQUIT 会被屏蔽,直到 SIGINT 处理完成才会执行默认行为(终止程序并生成核心转储)。
-
示例 2:
./sigaction_restart
程序启动后,在另一个终端执行
kill -USR1 程序PID
(PID 会在程序启动时打印),程序会处理 SIGUSR1 信号,2 秒后 read() 继续等待输入,输入内容后程序会正常打印。如果注释掉act.sa_flags = SA_RESTART
,信号处理后 read() 会返回错误。 -
示例 3:
./sigaction_siginfo
在另一个终端分别执行
kill -USR1 程序PID
和kill -USR2 程序PID
,程序会打印信号编号、发送者 PID 和 UID 等信息。
注意事项
- 信号屏蔽的范围:sa_mask 中设置的屏蔽信号仅在当前信号处理期间有效,处理完成后会自动恢复原来的屏蔽集,不会影响其他时候的信号处理。
- SA_RESTART 的局限性:并非所有系统调用都支持自动重启(如 select()、poll() 等 I/O 多路复用函数就不支持),具体可参考 man 手册。
- 信号处理函数的安全性:信号处理函数应尽量简单,避免调用不可重入函数(如 printf() 虽然常用,但严格来说在信号处理中不保证安全,实际开发中需谨慎)。
- 核心信号不可修改:SIGKILL(9)和 SIGSTOP(19)无法通过 sigaction 修改处理方式,系统会忽略对它们的设置请求。
六、执行结果分析:理解信号处理的逻辑
示例 1 结果分析
运行程序后输出:
程序启动,按 Ctrl+C 发送 SIGINT 信号,按 Ctrl+\ 发送 SIGQUIT 信号,按 Ctrl+Z 退出...
按 Ctrl+C 后,触发 sigint_handler:
收到 SIGINT 信号(编号:2),我被中断了,但不会退出!
再按 Ctrl+C 试试...
信号处理完成,继续等待...
此时如果按 Ctrl+\(发送 SIGQUIT),由于 sa_mask 中包含 SIGQUIT,信号会被屏蔽,直到当前处理完成。再次按 Ctrl+C,会重复上述过程;如果在处理 SIGINT 时按 Ctrl+\,SIGQUIT 会在 SIGINT 处理完成后立即执行默认行为(终止程序)。
这说明 sa_mask 成功在信号处理期间屏蔽了指定信号,避免了处理过程被干扰。
示例 2 结果分析
带 SA_RESTART 标志时,程序启动后输出:
程序启动,PID = 12345
请输入字符串(输入后按回车):
在另一个终端执行 kill -USR1 12345
后,程序输出:
收到 SIGUSR1 信号(编号:10),正在处理...
SIGUSR1 处理完成
之后 read() 继续等待输入,输入“hello”并回车后:
你输入了:hello
程序正常结束,说明 SA_RESTART 让被中断的 read() 自动重启。
如果去掉 SA_RESTART 标志,信号处理完成后 read() 会返回 -1,输出:
read 失败: Interrupted system call
示例 3 结果分析
程序启动后输出:
程序启动,PID = 67890
发送 SIGUSR1:kill -USR1 67890
发送 SIGUSR2:kill -USR2 67890
等待信号...
在另一个终端执行 kill -USR1 67890
后,程序输出:
收到信号:10
信号来源 PID:54321(发送 kill 命令的终端进程 PID)
发送者 UID:1000(当前用户 UID)
这是 SIGUSR1 信号
执行 kill -USR2 67890
后,输出类似,仅信号编号和描述不同。
这说明 SA_SIGINFO 模式下,处理函数能获取到信号的详细来源信息,比 sa_handler 更强大。
七、设计理念:sigaction 结构体为何这样设计?
sigaction 结构体的设计体现了 Linux 信号处理的灵活性和安全性,其核心设计理念包括:
-
分离处理逻辑与控制选项:将处理函数(sa_handler)、屏蔽集(sa_mask)、行为标志(sa_flags)分离,让用户能独立配置每个部分,满足不同场景需求。比如既可以自定义处理函数,又能控制处理期间的信号屏蔽,还能设置系统调用重启等附加行为。
-
最小权限原则:默认情况下,处理信号时会自动屏蔽当前信号(避免嵌套处理),sa_mask 允许用户添加更多屏蔽信号,确保处理过程不受干扰,这是一种“安全默认”设计。
-
向后兼容与扩展:保留 sa_restorer 成员以兼容旧系统,同时通过 sa_flags 提供扩展功能(如 SA_SIGINFO、SA_RESTART),既保证了历史代码的可用性,又能支持新需求。
-
细粒度控制:相比早期的 signal() 函数(只能设置处理函数),sigaction 提供了更细的控制能力。比如 signal() 无法指定信号屏蔽集,也不能设置系统调用重启,而 sigaction 可以做到。
用 Mermaid 图展示 sigaction 结构体的设计逻辑:
graph TDA[“sigaction 结构体”] --> B[“处理函数sa_handler / sa_sigaction”]A --> C[“屏蔽集sa_mask(处理期间阻塞的信号)”]A --> D[“标志位sa_flags(控制处理行为)”]A --> E[“历史遗留sa_restorer(废弃)”]B --> F[“自定义函数 / SIG_DFL(默认) / SIG_IGN(忽略)”]C --> G[“自动包含当前信号 + 用户添加的其他信号”]D --> H[“SA_RESTART(系统调用重启)”]D --> I[“SA_SIGINFO(详细信息模式)”]D --> J[“SA_NOCLDSTOP(控制 SIGCHLD)等其他标志”]style A fill:#e1f5festyle B fill:#e8f5e8style C fill:#fff3e0style D fill:#ffebeestyle E fill:#f5f5f5style F fill:#c8e6c9style G fill:#fff3e0style H fill:#e1f5festyle I fill:#e1f5festyle J fill:#e1f5fe
八、常见问题与避坑指南
-
忘记初始化 sa_mask:如果不初始化 sa_mask(比如未调用 sigemptyset()),其值是不确定的,可能会意外屏蔽某些信号。正确做法是先用 sigemptyset() 清空,再用 sigaddset() 添加需要屏蔽的信号。
-
混淆 sa_handler 和 sa_sigaction:设置 SA_SIGINFO 标志后,必须使用 sa_sigaction 成员,而不是 sa_handler,否则会导致未定义行为(可能程序崩溃)。
-
在信号处理函数中做太多事情:信号处理函数应尽量简短,避免调用复杂函数或进行长时间操作。因为信号处理可能打断程序的正常执行流程,长时间处理会影响程序稳定性。
-
认为 SA_RESTART 能解决所有中断问题:SA_RESTART 只对部分系统调用有效(如 read()、write() 等),对 select()、poll() 等则无效,这些函数被信号中断后仍会返回 -1,需要手动处理 EINTR 错误。
-
忽略信号的默认行为:如果自定义处理函数中没有显式处理信号(比如只是打印信息),程序会继续执行原来的流程。但有些信号(如 SIGSEGV)的默认行为是终止程序,即使自定义处理函数,处理完成后程序仍可能崩溃(因为内存错误已经发生)。
九、总结:sigaction 结构体的核心价值
sigaction 结构体是 Linux 信号处理的“瑞士军刀”,它通过四个成员(sa_handler/sa_sigaction、sa_mask、sa_flags、sa_restorer)提供了对信号处理的全方位控制,既可以简单地忽略或默认处理信号,也能自定义复杂的处理逻辑,还能控制处理期间的信号屏蔽和系统调用行为。
相比早期的 signal() 函数,sigaction 更灵活、更可靠,是编写健壮程序的首选。其设计理念体现了 Linux 系统“按需配置”的思想,让开发者能根据实际需求定制信号处理规则,在应对各种异常和用户交互时游刃有余。
最后,用一张 Mermaid 图总结 sigaction 结构体的核心要点:
通过本文的讲解,相信你对 sigaction 结构体的设计和使用已经有了深入的理解。在实际开发中,合理运用 sigaction 能让你的程序更好地应对各种信号事件,提升稳定性和可靠性。记住,信号处理的核心是“规则明确、处理简洁”,sigaction 正是为此提供了强大的工具支持。