在 C 语言的 Linux 环境中,注册异常处理函数通常是通过 signal () 或 sigaction () 系统调用来完成的。这些函数可以捕获特定的信号,并指定当信号发生时应该执行的处理函数。

使用 signal () 注册信号处理函数

#include <stdio.h>
#include <signal.h>
 
void signal_handler (int signum) {
    printf ("Caught signal %d\n", signum);
    // 在这里可以添加自定义的信号处理逻辑
}
 
int main () {
    // 注册 SIGSYS 信号处理函数
    signal (SIGSYS, signal_handler);
 
    // 模拟等待信号
    while (1) {
        sleep(1);
    }
    
    return 0;
}

使用 sigaction () 注册信号处理函数 相比 signal ()sigaction () 是更推荐的方式,因为它提供了更多的控制和功能(例如可以重新定义信号处理行为,并保证信号的可靠传递)。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
 
void signal_handler (int signum, siginfo_t *info, void *context) {
    printf ("Caught signal %d\n", signum);
    // 处理 SIGSYS 信号或其他信号
}
int main () {
    struct sigaction sa;
 
    // 清除 sigaction 结构体
    memset(&sa, 0, sizeof(sa));
 
    // 设置处理函数
    sa.sa_sigaction = signal_handler;
    
    // 使用 SA_SIGINFO 标志,以便获取更多信息
    sa.sa_flags = SA_SIGINFO;
 
    // 注册 SIGSYS 信号
    sigaction(SIGSYS, &sa, NULL);
 
    // 模拟等待信号
    while (1) {
        sleep(1);
    }
 
    return 0;
}

两种方法的区别

signal () 是传统的方法,使用起来简单,但行为在不同的平台和情况下可能不一致。 sigaction () 提供了更强的控制能力和一致性,特别是在处理复杂信号时。比如,sigaction () 可以指定额外的信息(如 siginfo_t)并提供更多的处理选项。 在上述例子中,当进程收到 SIGSYS 信号时,信号处理函数将被调用。


sigaction() 是一个功能强大的函数,用于在 Linux/Unix 系统中处理信号。与传统的 signal() 相比,sigaction() 提供了更多的功能和更精确的控制。

sigaction() 函数原型

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum:要捕获的信号编号,如 SIGINTSIGTERMSIGSYS 等。
  • act:指向包含新信号处理动作的 sigaction 结构体的指针。
  • oldact:如果不为 NULL,则存储旧的信号处理动作,允许恢复原有的信号处理函数。

sigaction 结构体

struct sigaction {
    void (*sa_handler)(int);       // 传统的信号处理函数指针
    void (*sa_sigaction)(int, siginfo_t *, void *); // 带扩展信息的信号处理函数
    sigset_t sa_mask;              // 在信号处理程序执行期间阻塞的信号集
    int sa_flags;                  // 影响信号处理行为的标志
};
  • sa_handler:指向信号处理函数的指针。当信号触发时,会调用这个函数。它只接受信号编号作为参数。
  • sa_sigaction:指向带扩展信息的信号处理函数,它允许接收更多参数,例如信号的来源和上下文信息。要使用这个字段,需要设置 sa_flags 标志为 SA_SIGINFO
  • sa_mask:定义在处理当前信号时,哪些其他信号将被阻塞。
  • sa_flags:用于设置信号处理的一些行为,常用的标志有:
    • SA_SIGINFO:表示使用 sa_sigaction 代替 sa_handler
    • SA_RESTART:表示被信号中断的系统调用应自动重启。
    • SA_NODEFER:在信号处理期间,不自动阻塞正在处理的信号。
    • SA_NOCLDWAIT:当处理 SIGCHLD 时,子进程终止后不产生僵尸进程。

示例:使用 sigaction() 捕获信号

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
 
// 信号处理函数,带有详细信息
void signal_handler(int signum, siginfo_t *info, void *context) {
    printf("Received signal: %d\n", signum);
    printf("Signal sent by process: %d\n", info->si_pid); // 打印发送信号的进程 ID
}
 
int main() {
    struct sigaction sa;
 
    // 清空 sa 结构体
    memset(&sa, 0, sizeof(sa));
 
    // 设置信号处理函数
    sa.sa_sigaction = signal_handler;
 
    // 使用 SA_SIGINFO 来使用 sa_sigaction 而不是 sa_handler
    sa.sa_flags = SA_SIGINFO;
 
    // 捕获 SIGINT (通常是 Ctrl+C)
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }
 
    // 无限循环,等待信号
    printf("Waiting for signal... Press Ctrl+C to send SIGINT\n");
    while (1) {
        sleep(1); // 持续等待信号
    }
 
    return 0;
}

代码详解

  1. memset(&sa, 0, sizeof(sa));:首先清空 sigaction 结构体,确保所有字段初始化为零。

  2. sa.sa_sigaction = signal_handler;:将 sa_sigaction 设置为自定义的信号处理函数 signal_handler,以便在接收到信号时调用。

  3. sa.sa_flags = SA_SIGINFO;:使用 SA_SIGINFO 标志,以便 sigaction() 调用 sa_sigaction 而不是 sa_handler。通过这种方式,信号处理函数可以接收更多的上下文信息(例如,哪个进程发送了信号)。

  4. sigaction(SIGINT, &sa, NULL);:调用 sigaction() 注册信号处理程序,捕获 SIGINT 信号(当用户按下 Ctrl+C 时会触发)。

  5. while(1):程序进入一个无限循环,等待信号。每次收到信号时,会调用 signal_handler 并打印相关信息。

siginfo_t 结构体

当使用 SA_SIGINFO 标志时,信号处理函数的第二个参数是指向 siginfo_t 结构体的指针,包含了关于信号的详细信息:

typedef struct {
    int      si_signo;   // 信号编号
    int      si_errno;   // 错误编号
    int      si_code;    // 信号产生的代码
    pid_t    si_pid;     // 发送信号的进程 ID
    uid_t    si_uid;     // 发送信号的用户 ID
    void    *si_addr;    // 导致故障的内存地址(例如,段错误时)
    int      si_status;  // 子进程的退出状态(适用于 SIGCHLD)
    long     si_band;    // 描述信号带数据(适用于 SIGPOLL)
} siginfo_t;

常用标志和行为

  • SA_RESTART:让被信号中断的系统调用自动重启,而不是返回错误。这对于保持程序流畅运行非常有用。

  • SA_NODEFER:默认情况下,当信号处理函数运行时,处理该信号的信号会被阻塞。设置此标志可以让信号处理函数在运行期间接受相同的信号。

  • SA_NOCLDWAIT:如果进程捕获 SIGCHLD 信号,此标志会禁止生成僵尸进程,而不需要调用 wait()

使用 sigaction() 的优点

  1. 更强的控制:通过 sa_mask 可以定义在处理特定信号时应该阻塞哪些其他信号。

  2. 可靠性:与 signal() 相比,sigaction() 在某些复杂情况下更加可靠,不会因为内核版本或平台不同而行为不一致。

  3. 支持信号信息:通过 siginfo_t 提供更多关于信号的详细信息。

总结

sigaction() 是一个功能强大且灵活的信号处理机制,允许你在捕获信号时做更多的事情,如访问发送信号的进程信息、阻塞其他信号、自动重启被中断的系统调用等。