从“快递签收规则”看 sigaction:信号处理的“总开关”

一、从"快递签收规则"看 sigaction:信号处理的"总开关"

生活中,我们总会收到各种通知------快递到了、电话来了、闹钟响了。为了应对这些通知,我们会提前制定规则:快递放驿站、陌生电话拒接、闹钟响后起床。在 Linux 系统中,"信号"就像这些通知(比如程序出错时的 SIGSEGV、用户按 Ctrl+C 发送的 SIGINT),而 sigaction 结构体就是用来定义"信号处理规则"的核心工具。

简单说,sigaction 结构体是 Linux 系统中描述信号处理方式的"数据模板",它包含了四个关键信息:用什么函数处理信号(处理者)、处理期间要屏蔽哪些其他信号(免打扰设置)、处理时的特殊行为(附加规则)、以及一个遗留的恢复函数(历史产物)。通过这个结构体,我们能精确控制程序对每个信号的响应方式,比早期的 signal() 函数更灵活、更强大。

比如,当程序收到 SIGINT 信号(Ctrl+C)时,你可以通过 sigaction 定义:调用自定义函数打印"收到中断信号",处理期间不响应 SIGQUIT 信号(避免被打断),并且处理完后自动恢复默认行为------这些细致的控制都离不开 sigaction 结构体的设计。

二、sigaction 结构体的"家庭成员":各成员详解

sigaction 结构体定义在 <signal.h> 头文件中,其原型如下(不同系统可能略有差异,但核心成员一致):

c 复制代码
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),该函数能接收更详细的信号信息(比如信号来源、附加数据等)。此时需要将结构体视为:
c 复制代码
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() 系统调用来"激活"这个规则,让系统知道如何处理指定信号。该函数原型如下:

c 复制代码
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 信号设置处理规则,步骤是:

  1. 初始化 sigaction 结构体,设置 sa_handler、sa_mask、sa_flags 等。
  2. 调用 sigaction(SIGINT, &new_act, &old_act) 激活新规则,同时保存旧规则。
  3. (可选)需要时,调用 sigaction(SIGINT, &old_act, NULL) 恢复旧规则。

四、使用示例:sigaction 结构体的实战场景

示例 1:基础用法------自定义处理 SIGINT 信号

这个示例展示如何用 sigaction 结构体自定义处理 SIGINT 信号(Ctrl+C),替代默认的终止程序行为。

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;
}

代码说明

  1. 定义 sigint_handler 函数,收到 SIGINT 时打印提示信息,不终止程序。
  2. 初始化 sigaction 结构体:sa_handler 设为自定义函数,sa_mask 加入 SIGQUIT(处理 SIGINT 时屏蔽 Ctrl+\),sa_flags 为 0(默认行为)。
  3. 用 sigaction() 激活规则,然后进入循环等待信号(pause() 会暂停进程,直到收到信号)。
示例 2:SA_RESTART 标志------让系统调用自动重启

这个示例展示 SA_RESTART 标志的作用:当系统调用(如 read())被信号中断时,会自动重启,而不是返回错误。

c 复制代码
#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;
}

代码说明

  1. 定义 sigusr1_handler 函数,处理 SIGUSR1 信号(用户自定义信号),模拟耗时 2 秒。
  2. 初始化 sigaction 结构体时设置 sa_flags = SA_RESTART,表示被信号中断的系统调用会自动重启。
  3. 程序启动后,调用 read() 等待用户输入。此时如果另一个终端发送 SIGUSR1 信号(kill -USR1 程序PID),程序会先处理信号,处理完成后 read() 会继续等待输入,而不是返回错误。

如果去掉 SA_RESTART 标志,信号处理完成后 read() 会返回 -1 并设置 errno=EINTR,程序会打印"read 失败"。

示例 3:SA_SIGINFO 标志------获取详细信号信息

这个示例展示 SA_SIGINFO 标志的用法:使用 sa_sigaction 作为处理函数,获取信号的详细信息(如发送者 PID、附加数据等)。

c 复制代码
#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);  // 发送信号的进程 PID
    printf("发送者 UID:%d\n", info->si_uid);    // 发送者的用户 ID
    if (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;
}

代码说明

  1. 定义 siginfo_handler 函数,参数包括信号编号(signo)、信号信息结构体(siginfo_t *info)、上下文(void *context),能获取比 sa_handler 更详细的信息。
  2. 初始化 sigaction 结构体时,sa_sigaction 设为自定义函数,sa_flags 设为 SA_SIGINFO,启用详细信息模式。
  3. 为 SIGUSR1 和 SIGUSR2 信号设置处理规则,启动后在另一个终端发送这两个信号,程序会打印信号来源的 PID、UID 等信息。

siginfo_t 结构体包含多种信号相关信息,比如对于 SIGSEGV(段错误),可以通过 info->si_addr 获取导致错误的内存地址。

五、编译与运行:让示例代码跑起来

三个示例均为 C 代码,用 gcc 编译即可,无需链接额外库(信号处理函数属于标准 C 库)。

编译命令
  • 示例 1:

    bash 复制代码
    gcc -o sigaction_basic sigaction_basic.c -Wall
  • 示例 2:

    bash 复制代码
    gcc -o sigaction_restart sigaction_restart.c -Wall
  • 示例 3:

    bash 复制代码
    gcc -o sigaction_siginfo sigaction_siginfo.c -Wall
运行方法
  • 示例 1:

    bash 复制代码
    ./sigaction_basic

    运行后按 Ctrl+C(发送 SIGINT),会触发自定义处理函数;按 Ctrl+\(发送 SIGQUIT),如果此时正在处理 SIGINT,SIGQUIT 会被屏蔽,直到 SIGINT 处理完成才会执行默认行为(终止程序并生成核心转储)。

  • 示例 2:

    bash 复制代码
    ./sigaction_restart

    程序启动后,在另一个终端执行 kill -USR1 程序PID(PID 会在程序启动时打印),程序会处理 SIGUSR1 信号,2 秒后 read() 继续等待输入,输入内容后程序会正常打印。如果注释掉 act.sa_flags = SA_RESTART,信号处理后 read() 会返回错误。

  • 示例 3:

    bash 复制代码
    ./sigaction_siginfo

    在另一个终端分别执行 kill -USR1 程序PIDkill -USR2 程序PID,程序会打印信号编号、发送者 PID 和 UID 等信息。

注意事项
  1. 信号屏蔽的范围:sa_mask 中设置的屏蔽信号仅在当前信号处理期间有效,处理完成后会自动恢复原来的屏蔽集,不会影响其他时候的信号处理。
  2. SA_RESTART 的局限性:并非所有系统调用都支持自动重启(如 select()、poll() 等 I/O 多路复用函数就不支持),具体可参考 man 手册。
  3. 信号处理函数的安全性:信号处理函数应尽量简单,避免调用不可重入函数(如 printf() 虽然常用,但严格来说在信号处理中不保证安全,实际开发中需谨慎)。
  4. 核心信号不可修改: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 信号处理的灵活性和安全性,其核心设计理念包括:

  1. 分离处理逻辑与控制选项:将处理函数(sa_handler)、屏蔽集(sa_mask)、行为标志(sa_flags)分离,让用户能独立配置每个部分,满足不同场景需求。比如既可以自定义处理函数,又能控制处理期间的信号屏蔽,还能设置系统调用重启等附加行为。

  2. 最小权限原则:默认情况下,处理信号时会自动屏蔽当前信号(避免嵌套处理),sa_mask 允许用户添加更多屏蔽信号,确保处理过程不受干扰,这是一种"安全默认"设计。

  3. 向后兼容与扩展:保留 sa_restorer 成员以兼容旧系统,同时通过 sa_flags 提供扩展功能(如 SA_SIGINFO、SA_RESTART),既保证了历史代码的可用性,又能支持新需求。

  4. 细粒度控制:相比早期的 signal() 函数(只能设置处理函数),sigaction 提供了更细的控制能力。比如 signal() 无法指定信号屏蔽集,也不能设置系统调用重启,而 sigaction 可以做到。

用 Mermaid 图展示 sigaction 结构体的设计逻辑:

graph TD A["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:#e1f5fe style B fill:#e8f5e8 style C fill:#fff3e0 style D fill:#ffebee style E fill:#f5f5f5 style F fill:#c8e6c9 style G fill:#fff3e0 style H fill:#e1f5fe style I fill:#e1f5fe style J fill:#e1f5fe

八、常见问题与避坑指南

  1. 忘记初始化 sa_mask:如果不初始化 sa_mask(比如未调用 sigemptyset()),其值是不确定的,可能会意外屏蔽某些信号。正确做法是先用 sigemptyset() 清空,再用 sigaddset() 添加需要屏蔽的信号。

  2. 混淆 sa_handler 和 sa_sigaction:设置 SA_SIGINFO 标志后,必须使用 sa_sigaction 成员,而不是 sa_handler,否则会导致未定义行为(可能程序崩溃)。

  3. 在信号处理函数中做太多事情:信号处理函数应尽量简短,避免调用复杂函数或进行长时间操作。因为信号处理可能打断程序的正常执行流程,长时间处理会影响程序稳定性。

  4. 认为 SA_RESTART 能解决所有中断问题:SA_RESTART 只对部分系统调用有效(如 read()、write() 等),对 select()、poll() 等则无效,这些函数被信号中断后仍会返回 -1,需要手动处理 EINTR 错误。

  5. 忽略信号的默认行为:如果自定义处理函数中没有显式处理信号(比如只是打印信息),程序会继续执行原来的流程。但有些信号(如 SIGSEGV)的默认行为是终止程序,即使自定义处理函数,处理完成后程序仍可能崩溃(因为内存错误已经发生)。

九、总结:sigaction 结构体的核心价值

sigaction 结构体是 Linux 信号处理的"瑞士军刀",它通过四个成员(sa_handler/sa_sigaction、sa_mask、sa_flags、sa_restorer)提供了对信号处理的全方位控制,既可以简单地忽略或默认处理信号,也能自定义复杂的处理逻辑,还能控制处理期间的信号屏蔽和系统调用行为。

相比早期的 signal() 函数,sigaction 更灵活、更可靠,是编写健壮程序的首选。其设计理念体现了 Linux 系统"按需配置"的思想,让开发者能根据实际需求定制信号处理规则,在应对各种异常和用户交互时游刃有余。

最后,用一张 Mermaid 图总结 sigaction 结构体的核心要点:
struct sigaction sa_handler / sa_sigaction:信号处理函数(或特殊值) sa_mask:处理期间屏蔽的信号集 sa_flags:控制行为的标志(如 SA_RESTART、SA_SIGINFO) sa_restorer:废弃的恢复函数 sigaction() 系统调用 设置或获取信号的处理规则 比 signal() 更灵活,支持屏蔽集、标志位等细粒度控制 自定义信号响应、处理子进程状态变化、捕获程序错误等

通过本文的讲解,相信你对 sigaction 结构体的设计和使用已经有了深入的理解。在实际开发中,合理运用 sigaction 能让你的程序更好地应对各种信号事件,提升稳定性和可靠性。记住,信号处理的核心是"规则明确、处理简洁",sigaction 正是为此提供了强大的工具支持。

相关推荐
云泽8082 小时前
Linux 入门指南:从零掌握基础文件与目录操作命令
linux·运维·服务器
Hello_wshuo2 小时前
记一次手机付费充电设备研究
linux·单片机
心灵宝贝2 小时前
unzip-6.0-21.el7.x86_64.rpm怎么安装?CentOS 7手动安装rpm包详细步骤
linux·运维·centos
报错小能手3 小时前
linux学习笔记(11)fork详解
linux·笔记·学习
努力学习的小廉4 小时前
深入了解linux网络—— 基于UDP实现翻译和聊天功能
linux·网络·udp
大聪明-PLUS4 小时前
从技术史看:Unix 从何而来
linux·嵌入式·arm·smarc
励志不掉头发的内向程序员4 小时前
【Linux系列】并发世界的基石:透彻理解 Linux 进程 — 进程概念
linux·运维·服务器·开发语言·学习
---学无止境---5 小时前
Linux中内核堆栈跟踪函数dump_stack的实现
linux
早起的年轻人5 小时前
CentOS 8系统盘大文件查找方法
linux·运维·centos