信号处理入门与实战指南

在这里插入图片描述

在Unix/Linux系统中,信号是一种进程间通信的方式,它允许软件对特定事件作出响应。信号可以由硬件异常(如除以零)、软件异常(如杀死进程)或由其他进程发送触发。了解信号及其处理方式对于开发健壮的应用程序至关重要。

一、信号基础

信号是操作系统提供的一种异步通知机制,用来告知进程发生了某些事件。信号可以由系统自动发送(例如,当进程尝试访问非法内存地址时发送SIGSEGV),也可以由其他进程或用户手动发送(例如,使用kill命令)。信号的设计目的是为了允许进程在不中断当前操作的情况下响应外部事件。

信号的特点

  • 异步性:信号可以在任何时候发生,不一定是在进程执行的过程中。
  • 不可靠性:某些信号可能会丢失(例如,如果信号到达时进程正在内核态执行)。
  • 默认行为:每个信号都有一个默认的行为,例如SIGTERM默认终止进程。
  • 可捕获性:大多数信号可以通过编写信号处理器来改变其默认行为。

信号的分类

  • 终端信号:例如SIGKILL和SIGTERM,它们的主要作用是终止进程。
  • 可忽略信号:例如SIGCHLD,用于通知父进程子进程已经退出。
  • 可捕获信号:例如SIGINT,当用户按下Ctrl+C时,进程可以捕获此信号并执行相应的动作。
二、信号处理函数

信号处理函数(也称为信号处理器)是指定的函数,当信号到达时由系统调用。信号处理器可以用来处理各种类型的信号,实现自定义的行为。在C语言中,有两种主要的方式来注册信号处理器:signal()sigaction()

signal()函数
signal()是一个较早的接口,用于设置信号处理器。它的优点是简单易用,但缺点是不够灵活,不能传递额外的信息给信号处理器。

sigaction()函数
sigaction()是一个更现代、更强大的接口,它提供了更多的选项来定制信号处理器的行为。例如,它可以支持传递额外的信息(如SIGINFO)给信号处理器,还可以指定当信号到达时是否恢复被阻塞的信号等。

示例代码:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void signalHandler(int signum) {
    
    
    printf("Caught signal %d\n", signum);
    exit(signum);
}

int main() {
    
    
    // 使用signal()
    signal(SIGINT, signalHandler);

    // 或者使用sigaction()
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = signalHandler;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGINT, &sa, NULL);

    while (1) {
    
    
        printf("Sleeping...\n");
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

三、阻塞信号

在某些情况下,我们可能希望暂时阻止某些信号到达进程,这可以通过设置信号掩码来实现。信号掩码是一个比特集合,用来表示哪些信号应该被阻塞。当信号被阻塞时,信号不会被立即处理,而是被挂起,直到信号掩码被修改或进程被唤醒。

sigprocmask()函数
sigprocmask()允许我们修改进程的信号掩码。通过这个函数,我们可以设置、获取或恢复当前进程的信号掩码。这对于在临界区避免中断很有用,因为某些信号可能会导致进程进入不确定的状态。

示例代码:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void signalHandler(int signum) {
    
    
    printf("Caught signal %d\n", signum);
    exit(signum);
}

int main() {
    
    
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = signalHandler;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGINT, &sa, NULL);

    // 阻塞SIGINT信号
    sigset_t blockSet;
    sigemptyset(&blockSet);
    sigaddset(&blockSet, SIGINT);
    sigprocmask(SIG_BLOCK, &blockSet, NULL);

    // 等待一段时间
    sleep(5);

    // 恢复信号处理
    sigprocmask(SIG_UNBLOCK, &blockSet, NULL);

    while (1) {
    
    
        printf("Sleeping...\n");
        sleep(1);
    }
    return 0;
}
四、捕获多个信号

在多任务环境中,可能需要同时处理多种信号。为此,可以使用sigwait()函数来等待信号集中的任何一个信号,而不会阻塞当前进程。此外,sigpending()函数可以用来检查当前进程是否有未处理的信号。

sigwait()函数
sigwait()允许我们等待信号集中的任何一个信号。当信号到达时,该函数会返回,并通过参数返回捕获到的信号号。这在多线程环境中特别有用,因为它可以避免阻塞其他线程。

示例代码:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void signalHandler(int signum) {
    
    
    printf("Caught signal %d\n", signum);
    exit(signum);
}

int main() {
    
    
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = signalHandler;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    sigset_t pendingSet;
    sigemptyset(&pendingSet);

    while (1) {
    
    
        if (sigpending(&pendingSet) == 0 && !sigisemptyset(&pendingSet)) {
    
    
            int sig;
            if (sigwait(&pendingSet, &sig) == 0) {
    
    
                printf("Caught signal %d\n", sig);
                exit(sig);
            }
        }
        printf("Sleeping...\n");
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

五、发送信号

除了接收信号外,进程也可以发送信号给其他进程。发送信号可以用于进程间的同步、控制以及其他形式的通信。常用的发送信号的函数有kill()raise()alarm()

kill()函数
kill()函数允许我们向指定的进程发送信号。这通常用于请求进程执行某些操作,比如终止自身。

raise()函数
raise()函数允许我们向调用进程自身发送信号。这通常用于调试目的,比如模拟用户中断。

alarm()函数
alarm()函数允许我们设置一个定时器,在指定的时间后发送SIGALRM信号。这对于实现超时控制非常有用。

示例代码:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

void signalHandler(int signum) {
    
    
    printf("Caught signal %d\n", signum);
    exit(signum);
}

int main() {
    
    
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = signalHandler;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGINT, &sa, NULL);

    pid_t pid = fork();
    if (pid == 0) {
    
    
        // 子进程
        printf("Child process sleeping...\n");
        sleep(10);
        printf("Child process exiting...\n");
        exit(EXIT_SUCCESS);
    } else if (pid > 0) {
    
    
        // 父进程
        printf("Parent process waiting...\n");
        sleep(5);
        kill(pid, SIGINT); // 向子进程发送SIGINT信号
        wait(NULL); // 等待子进程结束
    } else {
    
    
        perror("fork");
        exit(EXIT_FAILURE);
    }

    return 0;
}
六、信号与线程

在多线程环境下,信号的处理变得更加复杂,因为信号可能会影响到所有线程。为了更好地控制信号在多线程环境下的行为,POSIX标准引入了一些新的API,如sigqueue()sigsuspend()

sigqueue()函数
sigqueue()允许我们向指定进程发送带有整数值的信号。这对于需要传递更多信息给信号处理器的情况非常有用。

sigsuspend()函数
sigsuspend()允许我们暂停当前进程直到接收到一个信号。这在多线程环境下特别有用,因为它可以确保信号只被一个线程处理,从而避免竞争条件。

示例代码:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

void signalHandler(int signum, siginfo_t *info, void *context) {
    
    
    printf("Caught signal %d with value %ld\n", signum, info->si_value.sival_int);
    exit(signum);
}

int main() {
    
    
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_sigaction = signalHandler;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGUSR1, &sa, NULL);

    pid_t pid = fork();
    if (pid == 0) {
    
    
        // 子进程
        printf("Child process sleeping...\n");
        sigsuspend(NULL); // 等待信号
        printf("Child process exiting...\n");
        exit(EXIT_SUCCESS);
    } else if (pid > 0) {
    
    
        // 父进程
        printf("Parent process waiting...\n");
        sleep(5);
        sigqueue(pid, SIGUSR1, (union sigval){
    
    .sival_int = 1234}); // 向子进程发送带有整数值的SIGUSR1信号
        wait(NULL); // 等待子进程结束
    } else {
    
    
        perror("fork");
        exit(EXIT_FAILURE);
    }

    return 0;
}

在这里插入图片描述

七、信号集

信号集是一个比特集合,用来表示一组信号。信号集可以用来存储、比较或操作信号。POSIX标准提供了一系列的函数来操作信号集,如sigemptyset()sigfillset()sigaddset()sigdelset()sigismember()

sigemptyset()函数
sigemptyset()函数用来初始化一个信号集为空集。

sigfillset()函数
sigfillset()函数用来初始化一个信号集为包含所有信号的集合。

sigaddset()函数
sigaddset()函数用来将指定信号添加到信号集中。

sigdelset()函数
sigdelset()函数用来从信号集中删除指定信号。

sigismember()函数
sigismember()函数用来检查指定信号是否存在于信号集中。

示例代码:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    
    
    sigset_t set;
    sigemptyset(&set); // 初始化为空集
    sigaddset(&set, SIGINT); // 添加SIGINT信号
    sigaddset(&set, SIGTERM); // 添加SIGTERM信号

    if (sigismember(&set, SIGINT)) {
    
    
        printf("SIGINT is in the set\n");
    }
    if (!sigismember(&set, SIGQUIT)) {
    
    
        printf("SIGQUIT is not in the set\n");
    }

    sigdelset(&set, SIGINT); // 删除SIGINT信号

    return 0;
}
八、信号屏蔽

在某些情况下,我们需要暂时屏蔽某些信号,防止它们干扰进程的正常执行。这可以通过修改信号掩码来实现。sigpending()函数可以用来查看当前进程是否有未处理的信号,而sigprocmask()函数可以用来设置、获取或恢复当前进程的信号掩码。

示例代码:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void signalHandler(int signum) {
    
    
    printf("Caught signal %d\n", signum);
    exit(signum);
}

int main() {
    
    
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = signalHandler;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGINT, &sa, NULL);

    sigset_t blockSet;
    sigemptyset(&blockSet);
    sigaddset(&blockSet, SIGINT);
    sigprocmask(SIG_BLOCK, &blockSet, NULL); // 阻塞SIGINT信号

    sigset_t pendingSet;
    sigemptyset(&pendingSet);

    // 等待一段时间
    sleep(5);

    sigprocmask(SIG_UNBLOCK, &blockSet, NULL); // 恢复SIGINT信号

    while (1) {
    
    
        printf("Sleeping...\n");
        sleep(1);
    }
    return 0;
}
总结

本文详细介绍了信号的基本概念、常用的信号及其处理方法,并提供了丰富的实战示例代码以及对常用信号API接口的说明。通过学习这些知识,你将能够在实际项目中更有效地管理和利用信号机制。