Linux系统编程之基本信号处理

概述

在前一篇文章中我们已经提到过,信号本质上是一个整数编号,每个编号对应一种特定类型的事件。当某个条件满足时,操作系统会生成相应的信号,并将其传递给目标进程。接收到信号后,进程可以选择忽略它、执行默认动作、或调用自定义的处理函数来响应。

signal函数

捕获信号最简单的方式是使用signal函数,它允许我们指定当特定信号到达时要执行的动作。其函数原型如下。

cpp 复制代码
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

各个参数和返回值的含义如下。

signum:待捕获信号的编号,如SIGINT、SIGTERM等。

handler:指向信号处理函数sighandler_t的指针。我们可以提供一个函数名作为参数,该函数将在接收到指定信号时被调用。这个函数应该接受一个整型参数(即信号编号),并且没有返回值。handler参数还可以是以下两个特殊值之一。

(1)SIG_DFL:恢复默认行为。对于大多数信号,默认行为是终止进程。对于某些信号(比如:SIGCHLD),则是忽略它们。

(2)SIG_IGN:忽略此信号。如果信号被忽略,那么即使它再次发生也不会有任何效果,除非后续又改变了信号处理方式。

返回值:成功时返回一个指向先前信号处理程序的指针,可用来恢复之前的处理方式。失败时返回SIG_ERR,可通过errno获取具体的错误代码。

在下面的示例代码中,我们定义了一个名为HandleSigint的函数来处理SIGINT信号。当用户按下Ctrl+C时,操作系统会向进程发送SIGINT信号。然后,我们的处理函数会被调用,打印一条消息并退出程序。

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

static void HandleSigint(int signum)
{
    printf("Caught SIGINT(%d)\n", signum);
    exit(0);
}

int main()
{
    // 设置SIGINT信号的处理器
    if (signal(SIGINT, HandleSigint) == SIG_ERR)
    {
        printf("Set handler failed\n");
        return 1;
    }

    printf("Press Ctrl+C to send SIGINT\n");

    while (1)
    {
        // 等待信号
        pause();
    }

    return 0;
}

可以看到,signal函数的使用相对比较简单,但它也有一些局限性和潜在的问题。

1、不可靠性。在某些系统上,使用signal函数设置的信号处理器可能会被重置为默认行为。这意味着,如果同一个信号再次到来,在处理完第一次之后,后续的信号将按照默认行为处理。

2、线程安全性。由于signal函数可能会覆盖已有的信号处理程序,并且它的实现可能不是线程安全的,在多线程环境中应谨慎使用。

因此,如果需要对信号进行更精细的控制,或确保更可靠的行为时,建议使用下面的sigaction函数。

sigaction函数

sigaction函数提供了比signal函数更加灵活和可靠的接口,允许我们为特定的信号指定更复杂的处理行为。通过sigaction,我们可以控制信号处理期间的行为,比如:是否重启被中断的系统调用、哪些信号应该在处理期间被阻塞等。

cpp 复制代码
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

各个参数和返回值的含义如下。

signum:待捕获信号的编号,如SIGINT、SIGTERM等。

act:指向sigaction结构体的指针,描述了新的信号处理动作。如果这个参数为NULL,则只查询当前的信号处理信息而不修改它。

oldact:也是指向sigaction结构体的指针,用来存储当前信号处理的信息。如果不需要保存旧的处理信息,可以将此参数设为NULL。

sigaction结构体的原型如下。

cpp 复制代码
struct sigaction
{
    union
    {
        // 基本信号处理函数
        void (*sa_handler)(int);
        // 扩展信号处理函数,需要SA_SIGINFO标志
        void (*sa_sigaction)(int, siginfo_t *, void *);
    } __sigaction_handler;
    // 在信号处理期间要阻塞的其他信号集合
    sigset_t sa_mask;
    // 控制信号处理行为的标志位
    int sa_flags;
    // 已废弃,不应使用
    void (*sa_restorer)(void);
};

返回值:成功时返回0,失败时返回-1,并设置errno以指示具体的错误原因。

在下面的示例代码中,我们不仅设置了SIGINT信号处理器,还利用sa_mask字段暂时屏蔽了SIGQUIT信号,防止两个信号同时到来造成混乱。另外,通过设置SA_RESTART标志,确保被信号中断的系统调用(比如:read或write函数)能够自动重启,而不是返回EINTR错误码。

对于SIGUSR1信号,我们选择了更详细的处理方式:启用了SA_SIGINFO标志,并使用sa_sigaction成员来获取更多关于信号的信息(比如:发送信号的进程ID)。

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

// 基本信号处理函数
static void HandleSigint(int signum)
{
    printf("Caught SIGINT(%d)\n", signum);
    exit(0);
}

// 扩展信号处理函数,需要SA_SIGINFO标志
static void HandleSigusr1(int signum, siginfo_t *info, void *context)
{
    printf("Caught SIGUSR1(%d) from PID %d\n", signum, info->si_pid);
}

int main(void)
{
    // 设置SIGINT信号的处理器
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = &HandleSigint;
    sigemptyset(&sa.sa_mask);
    // 在处理SIGINT时,屏蔽SIGQUIT
    sigaddset(&sa.sa_mask, SIGQUIT);
    // 自动重启被中断的系统调用
    sa.sa_flags = SA_RESTART;
    sigaction(SIGINT, &sa, NULL);

    // 设置SIGUSR1信号的处理器
    sa.sa_sigaction = &HandleSigusr1;
    // 启用SA_SIGINFO标志
    sa.sa_flags |= SA_SIGINFO;
    sigaction(SIGUSR1, &sa, NULL);
   
    printf("Press Ctrl+C to send SIGINT or use kill -s SIGUSR1 <pid>\n");
    while (1)
    {
        pause();
    }

    return 0;
}
相关推荐
历程里程碑1 天前
53 多路转接select
linux·开发语言·数据结构·数据库·c++·sql·排序算法
WYH2871 天前
一、驱动基础
linux·嵌入式硬件
痕忆丶1 天前
openharmony开发之磁盘相关
linux
z202305081 天前
RDMA 之RoCEv2 的报文格式(5)
linux·服务器·网络·人工智能
uesowys1 天前
CentOS Linux安装部署Hermes Agent智能体
linux·centos·hermes agent
毋语天1 天前
Linux 命令——文件、进程、网络与 Vim 编辑器
linux·网络·编辑器
William.csj1 天前
Linux——查看cuda版本的全面方法
linux·运维·服务器
薛定猫AI1 天前
Codex 与 Claude Code 全平台安装配置指南(Windows / macOS / Linux)
linux·windows·macos
kidwjb1 天前
信号量在进程中的使用
linux·进程间通信
sulikey1 天前
个人Linux操作系统学习笔记2 - gcc与库的理解
linux·笔记·学习·操作系统·gcc·