UNIX下C语言编程与实践38-UNIX 信号操作:signal 函数与信号捕获函数的编写

从函数使用到捕获函数规范,掌握 UNIX 信号处理的基础核心

一、核心认知:signal 函数的功能与定位

在 UNIX 信号处理中,signal 函数 是最基础、最常用的接口,定义在 <signal.h> 头文件中。其核心功能是"为指定信号设置处理动作"------通过该函数,进程可决定收到信号后的行为:执行系统默认动作、忽略信号,或调用自定义的信号捕获函数处理。

signal 函数是早期 UNIX 系统为简化信号处理设计的轻量级接口,虽存在兼容性和功能局限性,但因使用简单,至今仍是快速实现信号处理的首选工具(复杂场景需结合 sigaction 函数)。

二、signal 函数的完整解析:原型、参数与返回值

掌握 signal 函数的参数含义和返回值逻辑,是正确使用该函数的前提。其设计简洁但需注意细节(如参数取值范围、返回值判断)。

1. 函数原型

c 复制代码
#include <signal.h>

// 功能:为信号 sig 设置处理动作,返回之前的处理动作 
// 本质:修改内核中该信号的处理函数指针,实现信号行为的自定义
void (*signal(int sig, void (*f)(int)))(int);

// 简化理解:函数返回值为"参数是 int、返回值是 void"的函数指针 
// 可通过 typedef 简化声明(推荐)
typedef void (*sig_handler_t)(int);  // 定义信号处理函数类型
sig_handler_t signal(int sig, sig_handler_t f);

2. 核心参数解析

signal 函数有两个参数,分别控制"目标信号"和"处理动作",参数取值有严格规范:

参数 取值范围 功能说明 典型示例
sig (信号编号) 1~31(非可靠信号) 系统预定义信号,如 SIGINT(2)、SIGTERM(15)、SIGUSR1(10),每个信号对应固定事件 signal(SIGINT, ...)(处理 Ctrl+C 中断信号)
sig (信号编号) 34~64(可靠信号) POSIX 标准扩展信号(SIGRTMIN~SIGRTMAX),支持信号排队,避免丢失 signal(SIGRTMIN + 1, ...)(处理自定义可靠信号)
sig (信号编号) 无效值(如 0、65+) 函数调用失败,返回 SIG_ERR,设置 errno 为 EINVAL signal(0, ...)(无效信号,调用失败)
f (处理动作) SIG_DFL Default:设置信号为系统默认处理动作(如 SIGINT 默认终止进程,SIGSEGV 默认终止并生成 core 文件) signal(SIGINT, SIG_DFL)(恢复 Ctrl+C 默认终止行为)
f (处理动作) SIG_IGN Ignore:设置信号为忽略动作,收到信号后内核直接丢弃,不影响进程执行 signal(SIGTSTP, SIG_IGN)(忽略 Ctrl+Z 暂停信号)
f (处理动作) 自定义函数地址 Catch:设置信号为捕获动作,收到信号时调用自定义函数(需符合 void (*)(int) 类型) signal(SIGUSR1, my_handler)(调用 my_handler 处理 SIGUSR1 信号)

3. 返回值与错误处理

signal 函数的返回值是"之前的信号处理动作",需通过返回值判断调用是否成功,以及恢复原处理动作:

  • 成功返回:返回调用前信号的处理动作(可能是 SIG_DFL、SIG_IGN 或之前注册的函数指针),可用于后续恢复原处理方式;
  • 失败返回 :返回 SIG_ERR(本质是 (void (*)(int))-1),同时设置 errno 标识错误原因,常见错误码:
    • EINVAL:信号编号无效(如 0、65+)或信号不可捕获(如 SIGKILL、SIGSTOP);
    • EFAULT:处理函数地址 f 指向无效内存(如 NULL 或非法地址)。
实例:signal 函数返回值的使用(保存并恢复原处理动作)
c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>

// 自定义 SIGINT 处理函数
void temp_sigint_handler(int sig) {
    printf("\n临时处理 SIGINT 信号,3 秒后恢复默认行为\n");
    sleep(3);
    // 恢复 SIGINT 默认处理动作
    signal(SIGINT, SIG_DFL);
}

int main() {
    sig_handler_t old_handler;
    // 1. 保存 SIGINT 原处理动作,设置临时处理函数
    old_handler = signal(SIGINT, temp_sigint_handler);
    if (old_handler == SIG_ERR) {
        perror("signal 设置临时处理函数失败");
        return 1;
    }
    printf("已设置临时 SIGINT 处理函数,按下 Ctrl+C 测试(5 秒后自动恢复)\n");
    sleep(5);
    // 2. 手动恢复原处理动作(若 5 秒内未触发信号)
    if (signal(SIGINT, old_handler) == SIG_ERR) {
        perror("signal 恢复原处理函数失败");
        return 1;
    }
    printf("已恢复 SIGINT 原处理动作,按下 Ctrl+C 会默认终止程序\n");
    sleep(5);
    return 0;
}

编译与运行

bash 复制代码
gcc signal_return.c -o signal_return
./signal_return

输出示例

复制代码
已设置临时 SIGINT 处理函数,按下 Ctrl+C 测试(5 秒后自动恢复)
^C
临时处理 SIGINT 信号,3 秒后恢复默认行为
已恢复 SIGINT 原处理动作,按下 Ctrl+C 会默认终止程序
^C
Terminated

关键价值:通过保存返回值,可在临时处理信号后恢复原动作(如临时忽略信号,处理完关键逻辑后恢复默认行为),确保程序行为的灵活性和正确性。

三、实战:signal 函数的三种核心使用方式

signal 函数的使用围绕"处理动作"展开,分为"默认处理""忽略信号""捕获信号"三种场景,每种场景对应不同的业务需求,以下通过实例详细演示。

方式 1:设置信号为默认处理(SIG_DFL)

适用场景:需恢复信号的系统默认行为(如之前修改过信号处理动作,后续需还原),或明确指定信号按默认逻辑处理。

实例:恢复 SIGINT 默认终止行为

代码格式化

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// 临时忽略 SIGINT 的函数
void ignore_sigint() {
    printf("临时忽略 SIGINT 信号(10 秒内 Ctrl+C 无效)\n");
    signal(SIGINT, SIG_IGN);
    sleep(10);

    // 10 秒后恢复默认处理
    signal(SIGINT, SIG_DFL);
    printf("已恢复 SIGINT 默认行为,按下 Ctrl+C 终止程序\n");
}

int main() {
    ignore_sigint();

    // 恢复后等待信号
    while (1) {
        sleep(1);
    }
    return 0;
}

运行输出示例

复制代码
临时忽略 SIGINT 信号(10 秒内 Ctrl+C 无效)
^C^C// 10 秒内按下 Ctrl+C 无反应
已恢复 SIGINT 默认行为,按下 Ctrl+C 终止程序
^C Terminated
方式 2:忽略信号(SIG_IGN)

适用场景:需屏蔽特定信号对进程的影响(如后台服务忽略 Ctrl+Z 暂停信号、忽略 SIGCHLD 避免僵死进程),确保进程不受干扰地执行。

实例:忽略 SIGTSTP(Ctrl+Z)和 SIGCHLD 信号
c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main() {
    // 1. 忽略 SIGTSTP 信号(Ctrl+Z 无效)
    if (signal(SIGTSTP, SIG_IGN) == SIG_ERR) {
        perror("signal 忽略 SIGTSTP 失败");
        return 1;
    }
    printf("已忽略 SIGTSTP 信号(Ctrl+Z 无效)\n");

    // 2. 忽略 SIGCHLD 信号(部分系统自动回收子进程,避免僵死)
    if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) {
        perror("signal 忽略 SIGCHLD 失败");
        return 1;
    }
    printf("已忽略 SIGCHLD 信号(子进程终止后自动回收)\n");

    // 创建子进程,验证僵死预防
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork 失败");
        return 1;
    }
    if (pid == 0) {
        printf("子进程 PID = %d,3 秒后终止\n", getpid());
        sleep(3);
        exit(EXIT_SUCCESS);
    }

    // 主进程不调用 wait,观察子进程是否僵死
    printf("父进程 PID = %d,等待 10 秒(可通过 ps 查看子进程状态)\n", getpid());
    sleep(10);
    printf("父进程退出\n");
    return 0;
}

运行输出:

复制代码
已忽略 SIGTSTP 信号(Ctrl+Z 无效)
已忽略 SIGCHLD 信号(子进程终止后自动回收)
子进程 PID = 1235,3 秒后终止
父进程 PID = 1234,等待 10 秒(可通过 ps 查看子进程状态)
^Z                  // Ctrl+Z 无反应
父进程退出

验证 :子进程终止后,执行 ps aux | grep 1235 | grep -v grep,无输出(子进程已被自动回收,未产生僵死进程)。

方式 3:捕获信号(自定义函数)

适用场景:需对信号进行自定义处理(如收到终止信号后清理资源、收到用户信号后执行特定任务),是 signal 函数最灵活的使用方式。

实例:捕获 SIGUSR1/SIGUSR2 信号并计数

代码示例

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

// 全局变量:统计信号接收次数(需用 volatile 确保不被编译器优化)
volatile int sigusr1_count = 0;
volatile int sigusr2_count = 0;

// 自定义信号捕获函数
void sig_handler(int sig) {
    switch (sig) {
        case SIGUSR1:
            sigusr1_count++;
            printf("捕获到 SIGUSR1 信号,累计次数:%d\n", sigusr1_count);
            break;
        case SIGUSR2:
            sigusr2_count++;
            printf("捕获到 SIGUSR2 信号,累计次数:%d\n", sigusr2_count);
            break;
        default:
            printf("捕获到未知信号:%d\n", sig);
            break;
    }
}

int main() {
    // 注册 SIGUSR1 和 SIGUSR2 捕获函数(共享同一个处理函数)
    if (signal(SIGUSR1, sig_handler) == SIG_ERR || signal(SIGUSR2, sig_handler) == SIG_ERR) {
        perror("signal 注册捕获函数失败");
        return 1;
    }

    printf("程序启动,PID = %d\n", getpid());
    printf("发送 SIGUSR1:kill -10 %d\n", getpid());
    printf("发送 SIGUSR2:kill -12 %d\n", getpid());
    printf("按下 Ctrl+C 终止程序\n");

    // 主循环,持续运行等待信号
    while (1) {
        sleep(1);
    }

    return 0;
}

编译与运行步骤

  1. 编译程序

    bash 复制代码
    gcc signal_catch.c -o signal_catch
  2. 启动程序

    bash 复制代码
    ./signal_catch
  3. 发送信号

    在新终端执行以下命令(假设程序 PID 为 1234):

    bash 复制代码
    kill -10 $(pgrep signal_catch)  # 发送 SIGUSR1
    kill -12 $(pgrep signal_catch)  # 发送 SIGUSR2
    kill -10 $(pgrep signal_catch)  # 再次发送 SIGUSR1

预期输出示例

复制代码
程序启动,PID = 1234
发送 SIGUSR1:kill -10 1234
发送 SIGUSR2:kill -12 1234
按下 Ctrl+C 终止程序
捕获到 SIGUSR1 信号,累计次数:1
捕获到 SIGUSR2 信号,累计次数:1
捕获到 SIGUSR1 信号,累计次数:2
^C Terminated

三、信号捕获函数的编写规范与注意事项

自定义信号捕获函数是信号处理的核心,但因信号的异步特性,函数编写需遵循严格规范,否则易导致程序崩溃、数据错乱等问题。

1. 捕获函数的基础规范

捕获函数必须满足的语法规则

  • 函数签名固定 :必须符合 void (*handler)(int) 类型,即"参数为 int(信号编号)、返回值为 void",示例:
    void my_handler(int sig) { ... }
  • 参数用途明确 :参数 sig 为当前收到的信号编号,可用于区分多个信号共享同一个处理函数(如前文示例中通过 switch 处理 SIGUSR1 和 SIGUSR2);
  • 避免返回值 :返回值为 void,不可返回任何数据,若需传递状态,需通过全局变量或静态变量(需加 volatile 修饰,避免编译器优化)。

2. 捕获函数的关键注意事项

  • 严禁调用不可重入函数

    信号捕获函数执行时可能中断主流程的任意操作,若调用"不可重入函数"(如 mallocfreeprintffopen),会导致全局数据结构损坏(如 malloc 维护的内存链表被中断后错乱)。

    可重入函数列表 (安全调用):writeclose_exitsigactionmemcpymemset 等(完整列表参考 man 7 signal);
    不可重入函数列表 (禁止调用):mallocfreeprintffprintfstrtoktime 等。

    替代方案 :若需日志输出,用 write(STDOUT_FILENO, ...) 替代 printf;若需复杂逻辑,在捕获函数中仅设置"事件标记"(如 volatile int flag = 1),主流程轮询标记并执行逻辑。

  • 全局变量需加 volatile 修饰

    编译器会优化未被修改的全局变量(如缓存到寄存器),若捕获函数修改全局变量(如信号计数),主流程可能无法感知变化。需用 volatile 修饰变量,告知编译器"变量可能被异步修改,每次访问需从内存读取"。

    错误示例:int sig_count = 0;(无 volatile,主流程可能读取缓存值);

    正确示例:volatile int sig_count = 0;(有 volatile,确保数据同步)。

  • 重新注册信号处理函数(兼容旧系统)

    部分旧 UNIX 系统(如 BSD)中,信号捕获函数执行一次后会自动恢复为默认动作(SIG_DFL),导致后续信号按默认方式处理(如首次捕获 SIGINT 后,再次按下 Ctrl+C 会终止进程)。

    兼容方案:在捕获函数开头重新注册自身,确保后续信号仍能被捕获,示例:

    void sig_handler(int sig) { signal(sig, sig_handler); // 重新注册,兼容旧系统 printf("捕获到信号:%d\n", sig); }

  • 避免耗时操作

    捕获函数执行期间,进程会暂停原流程,若函数中存在 sleep、循环等耗时操作,会导致主流程长时间阻塞。建议捕获函数仅执行"快速动作"(如设置标记、记录日志、清理资源),耗时逻辑移到主流程。

规范的捕获函数示例(安全处理信号)

代码示例

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

// 全局事件标记(volatile 修饰)
volatile int exit_flag = 0;
volatile int rotate_flag = 0;

// 安全日志输出函数(用 write 替代 printf)
void safe_log(const char *msg) {
    write(STDOUT_FILENO, msg, strlen(msg));
    write(STDOUT_FILENO, "\n", 1);
}

// 规范的捕获函数
void sig_handler(int sig) {
    // 1. 重新注册函数(兼容旧系统)
    signal(sig, sig_handler);
    // 2. 仅执行快速动作,设置标记
    switch (sig) {
        case SIGINT:
        case SIGTERM:
            safe_log("收到终止信号,设置退出标记");
            exit_flag = 1;
            break;
        case SIGUSR1:
            safe_log("收到日志轮转信号,设置轮转标记");
            rotate_flag = 1;
            break;
        default:
            char buf[64];
            snprintf(buf, sizeof(buf), "收到未知信号:%d", sig);
            safe_log(buf);
            break;
    }
}

// 主流程:轮询标记并执行复杂逻辑
void main_loop() {
    while (1) {
        // 检查退出标记
        if (exit_flag) {
            safe_log("主流程检测到退出标记,清理资源后退出");
            // 执行资源清理(如关闭文件、释放内存)
            sleep(2);
            safe_log("资源清理完成,程序退出");
            exit(EXIT_SUCCESS);
        }
        // 检查日志轮转标记
        if (rotate_flag) {
            safe_log("主流程检测到轮转标记,执行日志轮转");
            // 执行日志轮转(如重命名旧日志)
            sleep(1);
            safe_log("日志轮转完成");
            rotate_flag = 0; // 重置标记
        }
        // 主业务逻辑
        safe_log("主业务逻辑执行中...");
        sleep(3);
    }
}

int main() {
    // 注册信号捕获函数
    if (signal(SIGINT, sig_handler) == SIG_ERR ||
        signal(SIGTERM, sig_handler) == SIG_ERR ||
        signal(SIGUSR1, sig_handler) == SIG_ERR) {
        perror("signal 注册失败");
        return 1;
    }
    safe_log("程序启动,PID = %d", getpid());
    safe_log("发送终止信号:kill -2 %d 或 kill -15 %d", getpid(), getpid());
    safe_log("发送轮转信号:kill -10 %d", getpid());
    // 启动主流程
    main_loop();
    return 0;
}

运行示例

复制代码
程序启动,PID = 1234
发送终止信号:kill -2 1234 或 kill -15 1234
发送轮转信号:kill -10 1234
主业务逻辑执行中...
主业务逻辑执行中...
收到日志轮转信号,设置轮转标记
主流程检测到轮转标记,执行日志轮转
日志轮转完成
主业务逻辑执行中...
收到终止信号,设置退出标记
主流程检测到退出标记,清理资源后退出
资源清理完成,程序退出

规范点 :捕获函数仅设置标记,主流程处理复杂逻辑;用 write 替代 printf;全局变量加 volatile;重新注册函数兼容旧系统,符合所有编写规范。

四、signal 函数的局限性与常见错误

signal 函数虽简单易用,但存在兼容性差、功能有限等局限性,且使用中易因参数错误、逻辑疏漏导致问题。

1. signal 函数的核心局限性

  • 跨系统兼容性差

    不同 UNIX 系统对 signal 函数的行为定义不一致:

    • Linux 系统:捕获函数注册后持续有效,无需重新注册;
    • BSD 系统:捕获函数执行一次后自动恢复为 SIG_DFL,需重新注册;
    • Solaris 系统:部分信号(如 SIGALRM)的处理行为与其他系统差异较大。

    这导致依赖 signal 函数的程序在不同系统中行为不一致,需额外适配。

  • 不支持信号掩码与排队

    signal 函数无法设置"信号掩码"(处理函数执行期间阻塞的其他信号),易导致信号嵌套触发(如处理函数执行时被其他信号中断);同时不支持可靠信号的排队机制,非可靠信号(1~31)短时间内多次发送会丢失。

  • 无法获取信号附加信息

    捕获函数仅能获取信号编号,无法获取信号的附加信息(如发送者 PID、信号携带的数据),无法满足进程间传递额外信息的需求(如分布式系统中的任务 ID 传递)。

  • 不支持自动重启系统调用

    当进程阻塞在可中断系统调用(如 readaccept)时,收到信号后系统调用会返回 -1,错误码为 EINTR,需手动重试;signal 函数无法配置"自动重启",需额外编写重试逻辑。

2. 使用 signal 函数的常见错误与解决方法

常见错误 问题现象 原因分析 解决方法
信号编号错误 signal 调用失败,返回 SIG_ERR,perror 提示"Invalid argument" 1. 信号编号为 0 或超过系统最大信号编号(如 65+); 2. 信号不可捕获(如 SIGKILL=9、SIGSTOP=19),设置捕获函数时失败 1. 用 kill -l 查看系统支持的信号编号,确保参数有效; 2. 避免对 SIGKILL、SIGSTOP 调用 signal 函数(这两个信号不可捕获、不可忽略); 3. 示例:`if (sig < 1
捕获函数中调用不可重入函数 程序随机崩溃、日志错乱、内存泄漏,错误难以复现 捕获函数执行时中断主流程的不可重入函数(如 malloc),导致全局数据结构损坏(如内存链表断裂) 1. 捕获函数中仅使用可重入函数(如 write、close); 2. 复杂逻辑(如日志、内存操作)移到主流程,捕获函数仅设置标记; 3. 用工具(如 valgrind)检测内存错误,定位不可重入函数调用
未检查返回值 signal 调用失败未察觉,后续信号按默认方式处理(如预期捕获却终止进程) 开发者默认 signal 调用成功,未判断返回值是否为 SIG_ERR,忽略了"信号编号无效""权限不足"等错误场景 1. 必须检查 signal 返回值,失败时打印错误信息并处理; 2. 示例: if (signal(SIGUSR1, handler) == SIG_ERR) { perror("signal 失败"); exit(1); }
全局变量未加 volatile 修饰 捕获函数修改全局变量后,主流程读取的值未更新(如计数始终为 0) 编译器优化未被修改的全局变量,将其缓存到寄存器,捕获函数异步修改内存中的变量后,主流程仍读取寄存器缓存值 1. 所有被捕获函数修改的全局/静态变量,均需加 volatile 修饰; 2. 示例:volatile int count = 0;(而非 int count = 0;
系统调用未处理 EINTR 错误 进程阻塞在 read、accept 等系统调用时,收到信号后直接退出,未完成预期 I/O 操作 可中断系统调用被信号中断后返回 -1,错误码为 EINTR,未手动重试,导致 I/O 逻辑中断 1. 对可中断系统调用,检查错误码为 EINTR 时重试; 2. 示例: while ((n = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR); // 重试 3. 复杂场景改用 sigaction 函数,设置 SA_RESTART 标志自动重启系统调用

五、拓展:sigaction 函数------signal 的增强替代方案

为解决 signal 函数的局限性,POSIX 标准定义了 sigaction 函数,支持信号掩码、自动重启、可靠信号、附加信息获取等功能,是复杂场景下的首选接口。

1. sigaction 函数原型与核心参数

c 复制代码
#include <signal.h>

// 功能:设置或获取信号处理动作,支持信号掩码、自动重启等增强功能
// 返回值:成功返回 0,失败返回 -1,设置 errno
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);

// 信号处理动作结构体(核心参数)
struct sigaction {
    void (*sa_handler)(int);                 // 基础捕获函数(同 signal 的 f 参数)
    void (*sa_sigaction)(int, siginfo_t *, void *); // 增强捕获函数(获取附加信息)
    sigset_t sa_mask;                        // 信号掩码:处理函数执行期间阻塞的信号
    int sa_flags;                            // 功能标志(如自动重启、使用增强函数)
    void (*sa_restorer)(void);               // 已废弃,无需使用
};

// 增强捕获函数的 siginfo_t 结构体(存储信号附加信息)
typedef struct siginfo_t {
    int si_signo;                            // 信号编号
    pid_t si_pid;                            // 信号发送者 PID
    uid_t si_uid;                            // 信号发送者 UID
    void *si_addr;                           // 触发信号的内存地址(如 SIGSEGV 时的无效地址)
    int si_value;                            // 信号携带的整数数据(自定义信号时传递)
    // ... 其他字段(按需使用)
} siginfo_t;

2. sigaction 的核心优势与使用示例

优势 1:设置信号掩码,避免嵌套中断

通过 sa_mask 设置处理函数执行期间阻塞的信号,防止其他信号中断处理函数,示例:

代码示例

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int sig) {
    printf("捕获到信号 %d,处理中(2 秒内阻塞其他信号)...\n", sig);
    sleep(2); // 模拟处理耗时
    printf("信号 %d 处理完成\n", sig);
}

int main() {
    struct sigaction sa;
    // 1. 初始化信号掩码:处理函数执行期间阻塞 SIGINT 和 SIGUSR1
    sigemptyset(&sa.sa_mask); // 清空掩码
    sigaddset(&sa.sa_mask, SIGINT); // 添加 SIGINT 到掩码
    sigaddset(&sa.sa_mask, SIGUSR1); // 添加 SIGUSR1 到掩码

    // 2. 设置处理函数和标志
    sa.sa_handler = sig_handler;
    sa.sa_flags = 0; // 无特殊标志

    // 3. 注册信号
    sigaction(SIGUSR1, &sa, NULL);

    printf("程序启动,PID = %d\n", getpid());
    printf("先发送 SIGUSR1,2 秒内发送 SIGINT 测试阻塞\n");
    while (1) {
        sleep(1);
    }
    return 0;
}

运行结果示例

复制代码
程序启动,PID = 1234
先发送 SIGUSR1,2 秒内发送 SIGINT 测试阻塞
捕获到信号 10,处理中(2 秒内阻塞其他信号)...
^C// 2 秒内按下 Ctrl+C,被阻塞
信号 10 处理完成
Terminated// 处理完成后,SIGINT 生效,终止进程
优势 2:获取信号附加信息(如发送者 PID)

通过 sa_flags = SA_SIGINFO 使用增强捕获函数,获取信号发送者 PID、携带数据等信息,示例:

以下为规范格式后的代码内容,仅调整缩进和排版,未修改原内容:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

// 增强捕获函数(带附加信息)
void sig_info_handler(int sig, siginfo_t *info, void *context) {
    printf("捕获到信号 %d\n", sig);
    printf("发送者 PID:%d\n", info->si_pid);  // 获取发送者 PID
    printf("发送者 UID:%d\n", info->si_uid);  // 获取发送者 UID
    printf("信号携带数据:%d\n", info->si_value);  // 获取携带数据
}

int main() {
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;  // 使用增强捕获函数
    sa.sa_sigaction = sig_info_handler;  // 设置增强函数

    // 注册可靠信号(SIGRTMIN + 1)
    sigaction(SIGRTMIN + 1, &sa, NULL);

    printf("程序启动,PID = %d\n", getpid());
    printf("发送信号并携带数据:kill -%d %d -o 123\n", SIGRTMIN + 1, getpid());

    while (1) {
        sleep(1);
    }
    return 0;
}

// 程序输出示例
程序启动,PID = 1234
发送信号并携带数据:kill -35 1234 -o 123
捕获到信号 35
发送者 PID:5678  // 发送者终端的 PID
发送者 UID:1000  // 发送者的 UID
信号携带数据:123  // 自定义携带数据
优势 3:自动重启系统调用

通过 sa_flags |= SA_RESTART 配置自动重启被中断的系统调用,避免手动重试,示例:

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

void sig_handler(int sig) {
    printf("捕获到信号 %d,处理完成\n", sig);
}

int main() {
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = sig_handler;
    sa.sa_flags = SA_RESTART; // 自动重启系统调用
    
    sigaction(SIGUSR1, &sa, NULL);

    // 打开文件,阻塞读取(测试系统调用重启)
    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open 失败");
        return 1;
    }

    char buf[128];
    printf("阻塞读取文件(发送 kill -10 %d 测试重启)\n", getpid());
    ssize_t n = read(fd, buf, sizeof(buf)); // 被信号中断后自动重启
    if (n == -1) {
        perror("read 失败");
        return 1;
    }

    buf[n] = '\0';
    printf("读取文件内容:%s\n", buf);
    close(fd);
    return 0;
}

阻塞读取文件(发送 kill -10 1234 测试重启) 捕获到信号 10,处理完成 读取文件内容:Hello from test.txt // read 被中断后自动重启,成功读取

UNIX 中 signal 函数的使用方法、信号捕获函数的编写规范,分析了 signal 函数的局限性与常见错误,并拓展了增强接口 sigaction 的使用。signal 函数是信号处理的基础,适合简单场景;复杂场景(如可靠信号、信号掩码、自动重启)需使用 sigaction 函数,确保程序的健壮性和跨平台兼容性。

编写信号捕获函数时,需严格遵循"禁用不可重入函数、全局变量加 volatile、避免耗时操作"等规范,避免异步特性导致的程序崩溃。同时,务必检查 signal/sigaction 的返回值,处理无效信号、不可捕获信号等错误场景,确保信号处理逻辑的正确性。

相关推荐
La Pulga3 小时前
【STM32】I2C通信—软件模拟
c语言·stm32·单片机·嵌入式硬件·mcu
做运维的阿瑞3 小时前
Linux系统性能监控与故障定位实战:CPU/内存/I/O/网络
linux·运维·网络
驱动探索者3 小时前
车库到双子星:惠普的百年科技传奇
linux
啊?啊?6 小时前
1 玩转Linux命令行:基础文件操作实战教程
linux·服务器·基础指令
Yupureki6 小时前
从零开始的C++学习生活 6:string的入门使用
c语言·c++·学习·visual studio
Code Warrior6 小时前
【Linux】线程概念与控制(2)
linux
Java 码农6 小时前
CentOS 7 上安装 PostgreSQL
linux·postgresql·centos
筑梦之路6 小时前
CentOS 7 升级perl版本到5.40.3 —— 筑梦之路
linux·运维·centos
一个不秃头的 程序员6 小时前
从 0 到上线、长期运行、后续更新的**全流程**(适配 CentOS 服务器)
linux·服务器·centos