MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言信号处理的信号屏蔽

2023-02-167.3k 阅读

信号屏蔽概述

在Linux环境下使用C语言进行开发时,信号处理是一个重要的部分。信号是一种异步通知机制,它允许系统或其他进程向目标进程发送事件信息。然而,在某些情况下,我们可能希望暂时阻止某些信号的传递,直到特定的代码段执行完毕,这就是信号屏蔽的作用。

信号屏蔽通过设置信号掩码来实现。信号掩码是一个位图,其中的每一位对应一个信号。当某个信号对应的位被设置时,该信号就会被屏蔽,即进程不会立刻处理这个信号,而是将其挂起,直到该信号从信号掩码中移除。

信号屏蔽的函数

在Linux的C语言编程中,主要有以下几个函数用于信号屏蔽操作:

  1. sigprocmask函数
    • 函数原型
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- **参数说明**:
    - `how`:指定对信号掩码的操作方式,有以下几种取值:
        - `SIG_BLOCK`:将`set`指向的信号集添加到当前信号掩码中,即屏蔽`set`中的信号。
        - `SIG_UNBLOCK`:将`set`指向的信号集从当前信号掩码中移除,即解除对`set`中信号的屏蔽。
        - `SIG_SETMASK`:将当前信号掩码设置为`set`指向的信号集。
    - `set`:指向要操作的信号集。如果为`NULL`,则`how`的设置将被忽略,`sigprocmask`函数仅返回当前信号掩码到`oldset`。
    - `oldset`:指向一个`sigset_t`类型的变量,用于保存当前信号掩码。如果不需要保存当前信号掩码,可以将其设置为`NULL`。
- **返回值**:成功时返回0,出错时返回 -1,并设置`errno`。

2. sigemptyset函数 - 函数原型

#include <signal.h>
int sigemptyset(sigset_t *set);
- **功能**:初始化一个信号集`set`,使其不包含任何信号。
- **返回值**:成功时返回0,出错时返回 -1。

3. sigfillset函数 - 函数原型

#include <signal.h>
int sigfillset(sigset_t *set);
- **功能**:初始化一个信号集`set`,使其包含所有信号。
- **返回值**:成功时返回0,出错时返回 -1。

4. sigaddset函数 - 函数原型

#include <signal.h>
int sigaddset(sigset_t *set, int signum);
- **功能**:将信号`signum`添加到信号集`set`中。
- **返回值**:成功时返回0,出错时返回 -1。

5. sigdelset函数 - 函数原型

#include <signal.h>
int sigdelset(sigset_t *set, int signum);
- **功能**:将信号`signum`从信号集`set`中移除。
- **返回值**:成功时返回0,出错时返回 -1。

6. sigismember函数 - 函数原型

#include <signal.h>
int sigismember(const sigset_t *set, int signum);
- **功能**:检查信号`signum`是否在信号集`set`中。
- **返回值**:如果`signum`在`set`中,返回1;不在则返回0;出错时返回 -1。

信号屏蔽的应用场景

  1. 临界区保护 在多线程或多进程编程中,可能存在一些临界区,在这些区域内的代码执行时不希望被信号打断。例如,在对共享资源进行读写操作时,为了避免数据不一致,需要屏蔽可能会导致进程中断的信号,直到操作完成。
  2. 原子操作 有些操作需要以原子方式执行,即要么完全执行,要么完全不执行。信号屏蔽可以确保在执行这些原子操作时不会被信号干扰。
  3. 安全的信号处理 在安装信号处理函数时,为了防止在信号处理函数执行期间再次收到相同的信号,可以先屏蔽该信号,处理完成后再解除屏蔽。

代码示例

以下是一个简单的示例,展示了如何使用信号屏蔽:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void signal_handler(int signum) {
    printf("Received signal %d\n", signum);
}

int main() {
    sigset_t set, oldset;

    // 初始化信号集
    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    // 保存当前信号掩码
    sigprocmask(SIG_BLOCK, &set, &oldset);

    // 安装信号处理函数
    signal(SIGINT, signal_handler);

    printf("Sleeping for 10 seconds...\n");
    sleep(10);

    printf("Unblocking SIGINT...\n");
    // 恢复原来的信号掩码
    sigprocmask(SIG_SETMASK, &oldset, NULL);

    printf("Sleeping for another 5 seconds...\n");
    sleep(5);

    return 0;
}

在这个示例中:

  1. 首先,使用sigemptyset函数初始化一个信号集set,然后使用sigaddset函数将SIGINT(通常由用户按下Ctrl+C产生)添加到信号集中。
  2. 接着,使用sigprocmask函数并指定SIG_BLOCK操作,将SIGINT信号屏蔽,并保存当前信号掩码到oldset
  3. 安装SIGINT信号的处理函数signal_handler
  4. 进程睡眠10秒,在这期间,即使按下Ctrl+CSIGINT信号也不会被处理,因为它被屏蔽了。
  5. 10秒后,使用sigprocmask函数并指定SIG_SETMASK操作,恢复原来的信号掩码,即解除对SIGINT信号的屏蔽。
  6. 进程再睡眠5秒,此时如果按下Ctrl+CSIGINT信号将被处理,signal_handler函数将被调用。

信号屏蔽的本质

从操作系统的角度来看,信号屏蔽涉及到进程控制块(PCB)中的信号掩码字段。当一个信号产生时,操作系统首先检查目标进程的信号掩码。如果信号对应的位在掩码中被设置,说明该信号被屏蔽,操作系统会将该信号挂起,放入进程的未决信号队列中。

只有当信号从信号掩码中移除,并且进程处于可处理信号的状态(例如,不在系统调用的不可中断睡眠状态)时,操作系统才会将未决信号队列中的信号传递给进程,进程会根据信号的类型执行相应的信号处理函数。

信号屏蔽的本质就是通过对进程信号掩码的操作,来控制信号的传递时机,从而实现对进程执行流程的精细控制。这对于编写可靠、健壮的程序非常重要,尤其是在处理异步事件时。

信号屏蔽与信号处理函数的交互

当信号被屏蔽时,信号处理函数不会立即执行。一旦信号解除屏蔽,如果该信号在此期间产生过,信号处理函数会被立即调用。

需要注意的是,在信号处理函数执行期间,系统通常会自动再次屏蔽该信号,以防止在处理信号的过程中再次收到相同信号导致递归调用。处理完成后,信号屏蔽会恢复到之前的状态。

例如,假设我们有一个处理SIGTERM信号的函数:

void term_handler(int signum) {
    // 处理逻辑
}

int main() {
    signal(SIGTERM, term_handler);
    // 主程序逻辑
}

SIGTERM信号产生时,term_handler函数被调用。在term_handler函数执行期间,SIGTERM信号会被自动屏蔽,直到函数返回。

不可屏蔽信号

并非所有信号都可以被屏蔽。在Linux系统中,有一些信号是不可屏蔽的,例如SIGKILLSIGSTOP

  1. SIGKILL
    • SIGKILL信号用于无条件终止进程。它不能被捕获、忽略或屏蔽,发送该信号后,进程会立即终止。这是一种强制终止进程的机制,通常用于处理失控的进程。
  2. SIGSTOP
    • SIGSTOP信号用于暂停进程的执行。同样,它也不能被捕获、忽略或屏蔽。进程收到SIGSTOP信号后,会进入暂停状态,直到收到SIGCONT信号才会继续执行。

信号屏蔽与多线程编程

在多线程环境下,信号屏蔽变得更加复杂。每个线程都有自己的信号掩码,但整个进程也有一个共享的信号掩码。

当一个信号产生时,它会被发送到整个进程。如果信号被屏蔽,它会被挂起,直到信号被解除屏蔽。在多线程环境中,通常建议在主线程中进行信号屏蔽操作,以确保整个进程的一致性。

例如,在一个多线程程序中,可以在主线程中屏蔽某些信号,然后创建多个线程:

#include <pthread.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void *thread_function(void *arg) {
    // 线程逻辑
    return NULL;
}

int main() {
    sigset_t set;
    pthread_t thread;

    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    // 在主线程中屏蔽SIGINT信号
    sigprocmask(SIG_BLOCK, &set, NULL);

    pthread_create(&thread, NULL, thread_function, NULL);

    // 主线程逻辑
    pthread_join(thread, NULL);

    return 0;
}

在这个示例中,主线程屏蔽了SIGINT信号,创建的线程也会继承这个屏蔽设置。这样可以确保在多线程环境下,整个进程对信号的处理更加统一和可控。

信号屏蔽与系统调用

当进程在执行系统调用时,如果收到一个未屏蔽的信号,系统调用可能会被中断。为了避免这种情况,可以在执行系统调用前屏蔽相关信号,执行完系统调用后再解除屏蔽。

例如,在进行文件读写操作时:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

int main() {
    int fd;
    sigset_t set, oldset;
    char buffer[1024];

    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    // 屏蔽SIGINT信号
    sigprocmask(SIG_BLOCK, &set, &oldset);

    fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    ssize_t read_bytes = read(fd, buffer, sizeof(buffer));
    if (read_bytes == -1) {
        perror("read");
        close(fd);
        exit(1);
    }

    close(fd);

    // 恢复原来的信号掩码
    sigprocmask(SIG_SETMASK, &oldset, NULL);

    return 0;
}

在这个示例中,在执行openread系统调用前屏蔽了SIGINT信号,以防止在系统调用过程中被SIGINT信号中断,确保文件操作的完整性。操作完成后,恢复原来的信号掩码。

信号屏蔽的错误处理

在使用信号屏蔽相关函数时,需要注意错误处理。例如,sigprocmask函数在出错时会返回 -1,并设置errno。常见的错误包括无效的how参数、无效的信号集指针等。

#include <stdio.h>
#include <signal.h>

int main() {
    sigset_t set;
    int ret;

    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    ret = sigprocmask(SIG_BLOCK, &set, NULL);
    if (ret == -1) {
        perror("sigprocmask");
        return 1;
    }

    return 0;
}

在这个示例中,检查sigprocmask函数的返回值,如果出错,使用perror函数打印错误信息,以便调试和定位问题。

信号屏蔽的优化

在实际应用中,合理地使用信号屏蔽可以提高程序的性能和稳定性。

  1. 尽量缩短屏蔽时间 过长时间地屏蔽信号可能会导致信号长时间得不到处理,影响系统的响应性。因此,应尽量缩短信号屏蔽的时间,只在必要的临界区进行屏蔽。
  2. 避免不必要的屏蔽 不要过度屏蔽信号,只屏蔽那些可能会对关键代码段产生影响的信号。过度屏蔽可能会导致程序错过一些重要的异步事件。
  3. 优化信号处理函数 信号处理函数应尽量简单和高效,避免在处理函数中执行复杂的操作,以减少信号处理对程序正常执行流程的影响。

信号屏蔽与实时信号

实时信号是Linux系统中一类具有更高优先级的信号,它们可以提供更精确的信号处理。在处理实时信号时,信号屏蔽的原理与普通信号相同,但需要注意实时信号的排队和优先级等特性。

例如,实时信号可以在信号队列中排队,而普通信号通常不会排队(相同信号多次产生时,只保留一个)。在屏蔽实时信号时,同样需要考虑对信号队列的影响。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void realtime_signal_handler(int signum) {
    printf("Received real - time signal %d\n", signum);
}

int main() {
    sigset_t set, oldset;

    // 初始化信号集
    sigemptyset(&set);
    sigaddset(&set, SIGRTMIN);

    // 保存当前信号掩码
    sigprocmask(SIG_BLOCK, &set, &oldset);

    // 安装实时信号处理函数
    signal(SIGRTMIN, realtime_signal_handler);

    printf("Sleeping for 10 seconds...\n");
    sleep(10);

    printf("Unblocking SIGRTMIN...\n");
    // 恢复原来的信号掩码
    sigprocmask(SIG_SETMASK, &oldset, NULL);

    printf("Sleeping for another 5 seconds...\n");
    sleep(5);

    return 0;
}

在这个示例中,对实时信号SIGRTMIN进行了屏蔽和处理,展示了实时信号与信号屏蔽的结合使用。

信号屏蔽与进程间通信

在进程间通信(IPC)场景中,信号屏蔽也起着重要作用。例如,在使用信号进行进程间同步时,可能需要在接收信号的进程中合理地屏蔽信号,以确保同步操作的正确性。 假设进程A向进程B发送一个信号来通知某个事件的发生,进程B在处理这个信号之前,可能需要屏蔽该信号,直到它准备好处理这个事件。

// 进程A
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        // 子进程(进程B)
        sleep(2);
        kill(getppid(), SIGUSR1);
        exit(0);
    } else {
        // 父进程(进程A)
        sigset_t set, oldset;
        sigemptyset(&set);
        sigaddset(&set, SIGUSR1);

        // 屏蔽SIGUSR1信号
        sigprocmask(SIG_BLOCK, &set, &oldset);

        printf("Waiting for signal...\n");
        sleep(5);

        // 解除屏蔽
        sigprocmask(SIG_SETMASK, &oldset, NULL);

        printf("Signal received (should be after unblocking).\n");
        wait(NULL);
    }

    return 0;
}

在这个示例中,父进程(进程A)在等待子进程(进程B)发送的SIGUSR1信号时,先屏蔽了该信号,然后在适当的时候解除屏蔽,以确保信号能够在合适的时机被处理,实现进程间的同步。

信号屏蔽与守护进程

守护进程是在后台运行且不与任何终端关联的进程。在创建守护进程时,信号屏蔽也需要谨慎处理。 守护进程通常需要屏蔽一些可能会导致其异常终止或干扰其正常运行的信号,例如SIGINTSIGQUIT等。同时,对于一些需要进行特定处理的信号,如SIGHUP(通常用于通知守护进程重新加载配置文件),守护进程需要安装相应的信号处理函数,并在处理函数执行期间合理地处理信号屏蔽。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>

void sighup_handler(int signum) {
    // 重新加载配置文件的逻辑
    printf("Received SIGHUP, reloading configuration...\n");
}

int main() {
    pid_t pid;
    sigset_t set;

    // 创建子进程
    pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    } else if (pid > 0) {
        // 父进程退出
        exit(0);
    }

    // 子进程成为新的会话组长
    if (setsid() == -1) {
        perror("setsid");
        exit(1);
    }

    // 屏蔽一些信号
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 安装SIGHUP信号处理函数
    signal(SIGHUP, sighup_handler);

    // 守护进程的主循环
    while (1) {
        // 守护进程的工作逻辑
        sleep(1);
    }

    return 0;
}

在这个守护进程示例中,屏蔽了SIGINTSIGQUIT信号,以防止守护进程被用户通过终端发送的这些信号意外终止。同时,安装了SIGHUP信号处理函数,以便在收到SIGHUP信号时重新加载配置文件。

通过深入理解和合理应用信号屏蔽,我们可以编写出更加健壮、可靠的Linux C语言程序,有效地处理各种异步事件,提高程序的稳定性和性能。无论是在单进程、多线程还是进程间通信等场景下,信号屏蔽都是一项关键的技术,需要开发者熟练掌握和运用。