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

Linux C语言信号屏蔽的解除方法

2023-03-277.2k 阅读

信号屏蔽概述

在 Linux 系统下,信号是进程间通信(IPC,Inter - Process Communication)的一种方式,用于通知进程系统中发生了某些特定事件。例如,用户按下 Ctrl + C 组合键会向当前前台进程发送 SIGINT 信号,通常会导致进程终止。

进程可以选择屏蔽某些信号,即暂时忽略这些信号的到来。信号屏蔽提供了一种机制,让进程可以在执行关键代码段时,避免被某些信号中断,确保代码的原子性和完整性。

在 C 语言中,使用 sigprocmask 函数来设置信号屏蔽字,从而实现信号屏蔽功能。信号屏蔽字是一个位图,其中的每一位对应一个信号。如果某一位被设置,相应的信号就会被屏蔽。

信号屏蔽的解除时机与需求

  1. 关键代码执行完毕 当进程执行一段不希望被特定信号中断的关键代码时,会屏蔽相关信号。例如,在进行文件操作时,可能会屏蔽 SIGINT 信号,以防止在文件写入过程中被用户意外终止,导致文件损坏。当文件操作完成后,就需要解除对 SIGINT 信号的屏蔽,使得进程能够响应正常的用户终止请求。
  2. 状态改变 进程状态发生改变时,可能需要调整信号屏蔽状态。比如,进程从一个需要高度数据一致性的操作阶段进入到一个相对宽松的运行阶段,此时之前屏蔽的信号可能不再需要屏蔽,就可以解除信号屏蔽。
  3. 恢复默认行为 有时候进程为了完成特定任务临时改变了信号处理方式并屏蔽了一些信号,任务完成后,希望恢复到信号的默认处理行为,这就需要解除信号屏蔽。

解除信号屏蔽的方法

  1. 使用 sigprocmask 函数 sigprocmask 函数不仅可以设置信号屏蔽字,也可以用于解除信号屏蔽。其函数原型如下:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how 参数指定了如何修改信号屏蔽字,有以下几种取值:
    • SIG_BLOCK:将 set 中的信号添加到当前信号屏蔽字中。
    • SIG_UNBLOCK:将 set 中的信号从当前信号屏蔽字中移除,这就是用于解除信号屏蔽的操作。
    • SIG_SETMASK:将当前信号屏蔽字设置为 set
  • set 参数是一个指向 sigset_t 类型的指针,sigset_t 是一个用于表示信号集的数据类型。这个信号集指定了要进行添加、移除或设置的信号。
  • oldset 参数也是一个指向 sigset_t 类型的指针,如果不为 NULL,函数会将原来的信号屏蔽字保存到 oldset 指向的位置。

下面是一个简单的代码示例,展示如何使用 sigprocmask 函数解除信号屏蔽:

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

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

int main() {
    sigset_t set, oldset;
    // 初始化信号处理函数
    signal(SIGINT, sig_handler);

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

    // 屏蔽 SIGINT 信号
    sigprocmask(SIG_BLOCK, &set, &oldset);
    printf("SIGINT signal blocked. Press Ctrl+C to test.\n");
    sleep(5);

    // 解除 SIGINT 信号屏蔽
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    printf("SIGINT signal unblocked. Press Ctrl+C to test.\n");
    sleep(5);

    return 0;
}

在上述代码中,首先使用 sigemptyset 函数清空信号集 set,然后使用 sigaddset 函数将 SIGINT 信号添加到 set 中。接着调用 sigprocmask 函数,以 SIG_BLOCK 方式屏蔽 SIGINT 信号,并将原来的信号屏蔽字保存到 oldset 中。在睡眠 5 秒期间,按下 Ctrl + C 不会终止程序,因为 SIGINT 信号被屏蔽了。之后,再次调用 sigprocmask 函数,以 SIG_UNBLOCK 方式解除对 SIGINT 信号的屏蔽,此时再按下 Ctrl + C,程序会调用 sig_handler 函数并打印接收到的信号编号。

  1. 利用信号处理函数中自动解除屏蔽特性 当进程捕获到一个信号并进入信号处理函数时,系统会自动将该信号添加到进程的信号屏蔽字中,以防止在处理该信号时再次接收到相同的信号而导致递归处理。当信号处理函数返回时,系统会自动解除对该信号的屏蔽,恢复到信号处理函数被调用前的信号屏蔽状态。

下面的代码示例展示了这一特性:

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

void sig_handler(int signum) {
    printf("Received signal %d. Inside handler, sleeping for 3 seconds.\n", signum);
    sleep(3);
    printf("Exiting handler.\n");
}

int main() {
    sigset_t set, oldset;
    // 初始化信号处理函数
    signal(SIGINT, sig_handler);

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

    // 屏蔽 SIGINT 信号
    sigprocmask(SIG_BLOCK, &set, &oldset);
    printf("SIGINT signal blocked. Press Ctrl+C to test.\n");
    sleep(5);

    // 解除 SIGINT 信号屏蔽
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    printf("SIGINT signal unblocked. Press Ctrl+C to test.\n");

    while(1) {
        printf("Main loop running...\n");
        sleep(1);
    }

    return 0;
}

在这个例子中,当 SIGINT 信号被捕获并进入 sig_handler 函数时,SIGINT 信号会被自动屏蔽。在 sig_handler 函数中睡眠 3 秒期间,再次按下 Ctrl + C 不会导致该信号再次被处理。当 sig_handler 函数返回时,SIGINT 信号的屏蔽状态会恢复到进入函数前的状态,在这个例子中就是非屏蔽状态,因为之前已经调用 sigprocmaskSIG_UNBLOCK 方式解除了屏蔽。

解除信号屏蔽时的注意事项

  1. 竞态条件 在解除信号屏蔽的过程中,可能会出现竞态条件。例如,当进程准备解除信号屏蔽时,信号可能在解除屏蔽操作执行前的极短时间内到达。为了避免这种情况,可以使用 sigwait 函数。sigwait 函数可以等待特定信号的到来,并在信号到达时进行处理,同时保证在等待期间信号处于屏蔽状态,从而避免竞态条件。

下面是一个使用 sigwait 函数避免竞态条件的代码示例:

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

void* sig_thread(void* arg) {
    sigset_t *set = (sigset_t *)arg;
    int signum;
    while(1) {
        sigwait(set, &signum);
        switch(signum) {
            case SIGINT:
                printf("Received SIGINT in thread.\n");
                break;
            case SIGTERM:
                printf("Received SIGTERM in thread.\n");
                break;
        }
    }
    return NULL;
}

int main() {
    sigset_t set;
    pthread_t tid;

    // 初始化信号集并添加 SIGINT 和 SIGTERM 信号
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGTERM);

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

    // 创建一个线程来等待信号
    pthread_create(&tid, NULL, sig_thread, &set);

    printf("Main thread is running. Press Ctrl+C or send SIGTERM to test.\n");
    while(1) {
        printf("Main loop running...\n");
        sleep(1);
    }

    pthread_join(tid, NULL);
    return 0;
}

在上述代码中,主线程屏蔽了 SIGINTSIGTERM 信号,然后创建了一个新线程。新线程使用 sigwait 函数等待这些信号。当信号到达时,新线程会处理信号,避免了主线程在解除信号屏蔽时可能出现的竞态条件。

  1. 信号处理函数的可重入性 在解除信号屏蔽后,如果信号被处理,信号处理函数必须是可重入的。可重入函数是指可以被中断,然后在中断返回后继续执行而不会出现错误的函数。例如,标准库中的许多函数,如 printf,在信号处理函数中使用是不安全的,因为它们不是可重入的。而像 write 这样的系统调用通常是可重入的。

下面是一个错误使用不可重入函数的示例:

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

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

int main() {
    // 初始化信号处理函数
    signal(SIGINT, sig_handler);

    printf("Press Ctrl+C to test.\n");
    while(1) {
        sleep(1);
    }

    return 0;
}

在这个例子中,sig_handler 函数使用了 printf,如果在 printf 执行过程中 SIGINT 信号到达,可能会导致程序出现未定义行为。应该使用可重入函数,如 write 来改写 sig_handler 函数:

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

void sig_handler(int signum) {
    const char *msg = "Received signal ";
    char num_str[10];
    snprintf(num_str, sizeof(num_str), "%d\n", signum);
    write(STDOUT_FILENO, msg, strlen(msg));
    write(STDOUT_FILENO, num_str, strlen(num_str));
}

int main() {
    // 初始化信号处理函数
    signal(SIGINT, sig_handler);

    printf("Press Ctrl+C to test.\n");
    while(1) {
        sleep(1);
    }

    return 0;
}

在这个改进的代码中,sig_handler 函数使用 write 函数来输出信息,确保了信号处理函数的可重入性。

  1. 进程组与信号屏蔽 当进程处于进程组中时,信号屏蔽的解除可能会影响整个进程组。例如,如果父进程解除了对某个信号的屏蔽,子进程可能也会受到影响,因为子进程会继承父进程的信号屏蔽字。在设计进程组相关的程序时,需要仔细考虑信号屏蔽的解除操作对整个进程组的影响。

下面是一个展示进程组中信号屏蔽继承关系的代码示例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

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

int main() {
    sigset_t set;
    pid_t pid;

    // 初始化信号处理函数
    signal(SIGINT, sig_handler);

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

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

    pid = fork();
    if(pid == 0) {
        // 子进程
        printf("Child process. SIGINT is blocked.\n");
        sleep(3);
        printf("Child process exiting.\n");
    } else if(pid > 0) {
        // 父进程
        printf("Parent process. Unblocking SIGINT.\n");
        sigprocmask(SIG_UNBLOCK, &set, NULL);
        wait(NULL);
        printf("Parent process exiting.\n");
    } else {
        perror("fork");
        return 1;
    }

    return 0;
}

在这个例子中,父进程首先屏蔽了 SIGINT 信号,然后创建子进程。子进程会继承父进程的信号屏蔽字,所以子进程中 SIGINT 信号也是屏蔽的。父进程随后解除了 SIGINT 信号的屏蔽,此时子进程中的 SIGINT 信号屏蔽也被解除(因为继承关系)。如果在子进程睡眠 3 秒期间按下 Ctrl + C,子进程会捕获到 SIGINT 信号并调用 sig_handler 函数。

  1. 信号屏蔽与多线程 在多线程程序中,信号屏蔽的解除需要特别注意。在 Linux 下,线程共享信号处理函数和信号屏蔽字。当一个线程解除信号屏蔽时,会影响到整个进程中的所有线程。为了在多线程环境中更好地控制信号,可以使用线程特定的信号屏蔽字。通过 pthread_sigmask 函数可以设置线程特定的信号屏蔽字,该函数与 sigprocmask 函数类似,但作用于单个线程。

下面是一个多线程环境下使用 pthread_sigmask 函数的代码示例:

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

void* thread_func(void* arg) {
    sigset_t set;
    int signum;
    // 获取线程特定信号屏蔽字
    pthread_sigmask(0, NULL, &set);
    // 解除 SIGINT 信号屏蔽
    sigdelset(&set, SIGINT);
    pthread_sigmask(SIG_SETMASK, &set, NULL);

    printf("Thread waiting for SIGINT.\n");
    sigwait(&set, &signum);
    printf("Thread received SIGINT.\n");

    return NULL;
}

int main() {
    sigset_t set;
    pthread_t tid;

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

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

    // 创建一个线程
    pthread_create(&tid, NULL, thread_func, NULL);

    printf("Main thread is running. Press Ctrl+C to test.\n");
    sleep(5);

    // 主线程解除 SIGINT 信号屏蔽
    sigdelset(&set, SIGINT);
    pthread_sigmask(SIG_SETMASK, &set, NULL);

    sleep(5);

    pthread_join(tid, NULL);
    return 0;
}

在这个代码中,主线程首先屏蔽了 SIGINT 信号,然后创建了一个新线程。新线程获取当前线程特定的信号屏蔽字,解除 SIGINT 信号的屏蔽,并使用 sigwait 等待 SIGINT 信号。主线程在睡眠 5 秒后也解除了 SIGINT 信号的屏蔽。当按下 Ctrl + C 时,新线程会捕获到 SIGINT 信号并进行处理。通过这种方式,可以在多线程环境中更精细地控制信号屏蔽和解除操作。

总结

解除 Linux C 语言中的信号屏蔽是一个需要谨慎处理的操作。通过 sigprocmask 函数以及利用信号处理函数中的自动解除屏蔽特性,可以实现信号屏蔽的解除。在解除信号屏蔽时,要注意避免竞态条件,确保信号处理函数的可重入性,考虑进程组和多线程环境下信号屏蔽的继承与影响。正确地解除信号屏蔽,能够使进程在复杂的系统环境中稳定、可靠地运行,确保进程能够正确响应各种信号事件,同时保证关键代码段的完整性和原子性。