文章目录
-
- 一、程序概述
- 二、完整程序源码
- 三、程序详解
-
- [3.1 头文件和宏定义](#3.1 头文件和宏定义)
- [3.2 pr_mask 函数 - 打印当前被阻塞的信号](#3.2 pr_mask 函数 - 打印当前被阻塞的信号)
- [3.3 sig_int 函数 - SIGINT信号处理函数](#3.3 sig_int 函数 - SIGINT信号处理函数)
- [3.4 信号处理函数安装](#3.4 信号处理函数安装)
- [3.5 信号集初始化](#3.5 信号集初始化)
- [3.6 进入临界区域](#3.6 进入临界区域)
- [3.7 使用sigsuspend等待信号](#3.7 使用sigsuspend等待信号)
- [3.8 恢复原始信号掩码并退出](#3.8 恢复原始信号掩码并退出)
- 四、信号处理的关键技术点
-
- [4.1 信号掩码管理](#4.1 信号掩码管理)
- [4.2 sigsuspend函数的作用](#4.2 sigsuspend函数的作用)
- [4.3 信号处理函数的注意事项](#4.3 信号处理函数的注意事项)
- 五、程序执行流程和预期行为
-
- [5.1 执行结果](#5.1 执行结果)
- [5.2 执行流程](#5.2 执行流程)
- [5.3 信号掩码变化表](#5.3 信号掩码变化表)
- [5.4 信号处理函数执行时系统自动阻塞当前信号的机制详解](#5.4 信号处理函数执行时系统自动阻塞当前信号的机制详解)
- 六、总结
一、程序概述
这是一个Linux环境下的信号处理演示程序,主要展示了如何使用信号掩码(signal mask)、信号处理函数和sigsuspend函数来安全地管理程序中的信号,特别是在临界区域(critical region)的信号处理。
二、完整程序源码
c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#ifndef NSIG
#define NSIG 65 // Linux系统上通常的最大信号数,可根据具体系统调整
#endif
/* 简易 pr_mask 实现:打印当前被阻塞的信号 */
void pr_mask(const char *msg) {
sigset_t set;
int i;
if (sigprocmask(0, NULL, &set) == -1) {
perror("sigprocmask");
return;
}
printf("%s", msg);
int first = 1;
for (i = 1; i < NSIG; i++) {
if (sigismember(&set, i)) {
if (!first) printf(" ");
printf("%s", strsignal(i));
first = 0;
}
}
if (first) printf("(none)");
printf("\n");
}
/* SIGINT 信号处理函数 */
static void sig_int(int signo) {
pr_mask("\nin sig_int: ");
}
int main(void) {
sigset_t newmask, oldmask, waitmask;
/* 使用 sigaction 安装信号处理函数(推荐方式) */
struct sigaction sa;
sa.sa_handler = sig_int;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; // 不设置 SA_RESTART,以便系统调用可被中断
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction(SIGINT)");
exit(EXIT_FAILURE);
}
/* 初始化信号集 */
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1); // 在 sigsuspend 期间阻塞 SIGUSR1
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT); // 要阻塞的信号:SIGINT
/* 阻塞 SIGINT,并保存旧的信号掩码 */
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) == -1) {
perror("sigprocmask(SIG_BLOCK)");
exit(EXIT_FAILURE);
}
pr_mask("in critical region: ");
/* 进入等待:临时使用 waitmask 作为屏蔽字 */
/* 注意:sigsuspend 总是返回 -1,正常情况 errno 为 EINTR */
if (sigsuspend(&waitmask) == -1 && errno != EINTR) {
perror("sigsuspend");
exit(EXIT_FAILURE);
}
pr_mask("after return from sigsuspend: ");
/* 恢复原始信号掩码(解除对 SIGINT 的阻塞) */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
perror("sigprocmask(SIG_SETMASK)");
exit(EXIT_FAILURE);
}
pr_mask("program exit: ");
exit(EXIT_SUCCESS);
}
三、程序详解
3.1 头文件和宏定义
c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#ifndef NSIG
#define NSIG 65 // Linux系统上通常的最大信号数,可根据具体系统调整
#endif
_POSIX_C_SOURCE 200809L:定义POSIX标准版本,确保使用符合POSIX 2008标准的函数和特性。
头文件:
<stdio.h>:标准输入输出函数
<stdlib.h>:通用工具函数(如exit)
<unistd.h>:Unix标准函数(如sleep)
<signal.h>:信号处理相关函数和数据结构
<errno.h>:错误码定义
<string.h>:字符串处理函数(如strsignal)
NSIG:信号最大数量,Linux系统通常为65,预定义该宏以确保程序在不同系统上的兼容性。
3.2 pr_mask 函数 - 打印当前被阻塞的信号
c
void pr_mask(const char *msg) {
sigset_t set;
int i;
if (sigprocmask(0, NULL, &set) == -1) {
perror("sigprocmask");
return;
}
printf("%s", msg);
int first = 1;
for (i = 1; i < NSIG; i++) {
if (sigismember(&set, i)) {
if (!first) printf(" ");
printf("%s", strsignal(i));
first = 0;
}
}
if (first) printf("(none)");
printf("\n");
}
功能:打印当前进程的信号掩码(即被阻塞的信号列表)
参数:msg - 前缀信息,用于标识打印内容的上下文
实现细节:
使用sigprocmask(0, NULL, &set)获取当前信号掩码(第一个参数为0表示不修改,仅获取)
遍历1到NSIG-1的所有信号编号,使用sigismember检查是否在信号集中
使用strsignal将信号编号转换为可读的信号名称
格式化输出结果,第一个信号前不加空格,其他信号前加空格分隔
3.3 sig_int 函数 - SIGINT信号处理函数
c
static void sig_int(int signo) {
pr_mask("\nin sig_int: ");
}
功能:处理SIGINT信号(通常由Ctrl+C产生)
参数:signo - 收到的信号编号
实现:在信号处理函数中调用pr_mask打印当前的信号掩码
3.4 信号处理函数安装
c
struct sigaction sa;
sa.sa_handler = sig_int;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; // 不设置 SA_RESTART,以便系统调用可被中断
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction(SIGINT)");
exit(EXIT_FAILURE);
}
使用sigaction函数安装SIGINT信号处理函数(推荐的信号处理安装方式,比signal函数更可靠)
sa.sa_handler:指定信号处理函数为sig_int
sa.sa_mask:在信号处理函数执行期间要额外阻塞的信号集(此处为空,即不额外阻塞其他信号)
sa.sa_flags:设置为0,表示不使用任何特殊标志(不设置SA_RESTART,意味着被中断的系统调用不会自动重启)
3.5 信号集初始化
c
/* 初始化信号集 */
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1); // 在 sigsuspend 期间阻塞 SIGUSR1
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT); // 要阻塞的信号:SIGINT
waitmask:用于sigsuspend的临时信号掩码,仅包含SIGUSR1(表示在等待期间只阻塞SIGUSR1)
newmask:用于阻塞SIGINT的信号集
3.6 进入临界区域
c
/* 阻塞 SIGINT,并保存旧的信号掩码 */
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) == -1) {
perror("sigprocmask(SIG_BLOCK)");
exit(EXIT_FAILURE);
}
pr_mask("in critical region: ");
使用sigprocmask(SIG_BLOCK, &newmask, &oldmask)阻塞SIGINT信号
SIG_BLOCK:将newmask中的信号添加到当前信号掩码中(即阻塞这些信号)
oldmask:保存原来的信号掩码,以便后续恢复
打印当前信号掩码,此时SIGINT应该被阻塞
3.7 使用sigsuspend等待信号
c
/* 进入等待:临时使用 waitmask 作为屏蔽字 */
/* 注意:sigsuspend 总是返回 -1,正常情况 errno 为 EINTR */
if (sigsuspend(&waitmask) == -1 && errno != EINTR) {
perror("sigsuspend");
exit(EXIT_FAILURE);
}
pr_mask("after return from sigsuspend: ");
sigsuspend(&waitmask):原子操作,执行以下步骤:
将进程的信号掩码临时替换为waitmask
暂停进程,直到收到一个未被阻塞的信号
当收到信号并处理完成后,恢复原来的信号掩码
返回(总是返回-1,正常情况下errno设置为EINTR)
在此例中,waitmask仅包含SIGUSR1,因此:
进程暂停,等待任何未被SIGUSR1阻塞的信号
如果收到SIGINT,会调用sig_int处理函数,然后sigsuspend返回
打印返回后的信号掩码,此时应该恢复为原来阻塞SIGINT的状态
3.8 恢复原始信号掩码并退出
c
/* 恢复原始信号掩码(解除对 SIGINT 的阻塞) */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
perror("sigprocmask(SIG_SETMASK)");
exit(EXIT_FAILURE);
}
pr_mask("program exit: ");
exit(EXIT_SUCCESS);
使用sigprocmask(SIG_SETMASK, &oldmask, NULL)恢复原始信号掩码
SIG_SETMASK:将信号掩码设置为指定的值(而不是添加)
打印退出前的信号掩码,此时应该解除对SIGINT的阻塞
正常退出程序
四、信号处理的关键技术点
4.1 信号掩码管理
信号掩码(Signal Mask):进程级别的一个信号集,包含了当前被阻塞的信号
当一个信号被阻塞时,它不会被进程接收,而是会被挂起(pending)
使用sigprocmask函数可以修改和获取信号掩码
常用操作:
SIG_BLOCK:添加信号到掩码(阻塞)
SIG_UNBLOCK:从掩码中移除信号(解除阻塞)
SIG_SETMASK:直接设置掩码为指定值
4.2 sigsuspend函数的作用
sigsuspend是一个原子操作,用于安全地等待信号
它解决了"测试-修改-等待"模式下的竞态条件问题:
如果不使用sigsuspend,而是先修改信号掩码再调用pause,可能会在这两个操作之间错过信号
sigsuspend原子地完成了"修改信号掩码-等待信号-恢复信号掩码"的过程
4.3 信号处理函数的注意事项
信号处理函数应该尽可能简洁,避免使用非异步安全的函数
在信号处理函数执行期间,当前信号会被自动阻塞
可以通过sa.sa_mask指定在信号处理函数执行期间需要额外阻塞的信号
五、程序执行流程和预期行为
5.1 执行结果
c
in critical region: Interrupt
^C
in sig_int: Interrupt User defined signal 1
after return from sigsuspend: Interrupt
program exit: (none)
5.2 执行流程
1.in critical region: Interrupt
程序进入临界区,当前信号掩码阻塞了SIGINT信号(Interrupt)
2.^C
用户按下Ctrl+C发送SIGINT信号
3.in sig_int: Interrupt User defined signal 1
信号处理函数执行时,系统自动阻塞当前信号(SIGINT)
同时由于sigsuspend使用的waitmask阻塞了SIGUSR1(User defined signal 1)
所以这两个信号在处理函数执行期间都被阻塞
4.after return from sigsuspend: Interrupt
sigsuspend返回后,恢复了调用前的信号掩码(newmask)
此时仍然阻塞SIGINT信号
5.program exit: (none)
程序最后恢复了原始信号掩码(oldmask)
原始掩码默认不阻塞任何信号,所以显示"(none)"
5.3 信号掩码变化表

5.4 信号处理函数执行时系统自动阻塞当前信号的机制详解
基本机制
当一个信号的处理函数被调用时,Linux系统会自动将该信号加入当前进程的信号掩码。这意味着在处理函数执行期间,如果再次收到相同的信号,它会被阻塞(暂存),直到处理函数执行完毕。
设计初衷
这个机制是Linux内核的保护设计,主要目的是:
防止递归调用:避免同一信号的处理函数被自身中断,导致无限递归
保证原子性:确保信号处理函数能够完整执行,不受同类信号干扰
避免竞态条件:防止信号处理过程中数据被并发修改
与其他信号操作的关系
与sigprocmask的区别:sigprocmask是手动修改信号掩码,而系统自动阻塞是内核的默认行为
与sigsuspend的配合:sigsuspend临时替换信号掩码,但当信号处理函数执行时,系统仍会自动阻塞当前信号
自动恢复机制:当信号处理函数执行完毕后,系统会自动恢复调用前的信号掩码(移除临时阻塞的当前信号)
手动控制
虽然系统默认自动阻塞当前信号,但可以通过sigaction的sa_flags参数修改此行为:
c
struct sigaction sa;
sa.sa_handler = sig_int;
sigemptyset(&sa.sa_mask);
// 添加其他要阻塞的信号
sigaddset(&sa.sa_mask, SIGUSR1);
sa.sa_flags = SA_NODEFER; // 禁用自动阻塞当前信号的机制
使用SA_NODEFER标志会禁用自动阻塞,使信号处理函数可以被同一信号再次中断(慎用,可能导致问题)。
六、总结
这个程序是一个信号处理演示,展示了如何:
使用sigaction安装信号处理函数
使用sigprocmask管理信号掩码
使用sigsuspend安全地等待信号
在信号处理函数中获取和打印信号掩码
程序的核心是展示了如何在临界区域内阻塞特定信号,然后使用sigsuspend安全地等待信号,同时保持原子性操作,避免竞态条件。这是Unix/Linux系统编程中信号处理的经典模式,常用于进程间同步和通信。
通过这个程序,我们可以深入理解信号掩码、信号处理函数和sigsuspend的工作原理,以及它们在实际编程中的应用。