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

Linux C语言信号集与阻塞机制

2024-07-117.4k 阅读

信号集概述

在Linux系统中,信号是一种异步通知机制,用于向进程发送事件消息。信号集(signal set)是一个用于表示多个信号的数据结构,它允许我们对一组信号进行统一的管理和操作。

信号集的数据结构

在C语言中,信号集通过sigset_t类型来表示,定义在<signal.h>头文件中。sigset_t本质上是一个位图(bitmap),每一位对应一个信号。例如,第1位表示信号SIGINT,第2位表示信号SIGQUIT等。

信号集操作函数

  1. 初始化信号集
    • int sigemptyset(sigset_t *set);
      • 功能:将信号集set初始化为空,即清除所有信号。
      • 返回值:成功返回0,失败返回-1。
    • int sigfillset(sigset_t *set);
      • 功能:将信号集set初始化为包含所有信号。
      • 返回值:成功返回0,失败返回-1。
  2. 添加和删除信号
    • int sigaddset(sigset_t *set, int signum);
      • 功能:将信号signum添加到信号集set中。
      • 返回值:成功返回0,失败返回-1。
    • int sigdelset(sigset_t *set, int signum);
      • 功能:将信号signum从信号集set中删除。
      • 返回值:成功返回0,失败返回-1。
  3. 检查信号是否在信号集中
    • int sigismember(const sigset_t *set, int signum);
      • 功能:检查信号signum是否在信号集set中。
      • 返回值:如果signumset中,返回1;否则返回0。如果set无效,返回-1。

以下是一个简单的示例代码,演示信号集的基本操作:

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

int main() {
    sigset_t set;
    // 初始化信号集为空
    if (sigemptyset(&set) == -1) {
        perror("sigemptyset");
        return 1;
    }
    // 添加SIGINT信号到信号集
    if (sigaddset(&set, SIGINT) == -1) {
        perror("sigaddset");
        return 1;
    }
    // 检查SIGINT是否在信号集中
    if (sigismember(&set, SIGINT) == 1) {
        printf("SIGINT is in the set\n");
    } else {
        printf("SIGINT is not in the set\n");
    }
    // 删除SIGINT信号
    if (sigdelset(&set, SIGINT) == -1) {
        perror("sigdelset");
        return 1;
    }
    // 再次检查SIGINT是否在信号集中
    if (sigismember(&set, SIGINT) == 1) {
        printf("SIGINT is in the set\n");
    } else {
        printf("SIGINT is not in the set\n");
    }
    return 0;
}

信号阻塞机制

信号阻塞(signal blocking)是一种控制信号处理时机的机制。通过阻塞信号,进程可以暂时阻止某些信号的传递,直到解除阻塞后才处理这些信号。

信号掩码

每个进程都有一个信号掩码(signal mask),它是一个信号集,用于表示当前被阻塞的信号。当一个信号被阻塞时,它不会被立即传递给进程,而是处于“挂起”状态。

设置信号掩码

  1. sigprocmask函数
    • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
      • how参数:指定如何修改信号掩码,有以下几种取值:
        • SIG_BLOCK:将set中的信号添加到当前信号掩码中。
        • SIG_UNBLOCK:将set中的信号从当前信号掩码中移除。
        • SIG_SETMASK:将当前信号掩码设置为set
      • set参数:指向要操作的信号集。
      • oldset参数:指向一个信号集,用于保存旧的信号掩码。如果不需要保存旧的信号掩码,可以设置为NULL
      • 返回值:成功返回0,失败返回-1。

下面是一个示例代码,展示如何使用sigprocmask函数阻塞和解除阻塞信号:

#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;
    // 初始化信号集为空
    if (sigemptyset(&set) == -1) {
        perror("sigemptyset");
        return 1;
    }
    // 添加SIGINT信号到信号集
    if (sigaddset(&set, SIGINT) == -1) {
        perror("sigaddset");
        return 1;
    }
    // 注册信号处理函数
    if (signal(SIGINT, signal_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }
    // 阻塞SIGINT信号
    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }
    printf("SIGINT is blocked. Press Ctrl+C to test.\n");
    sleep(10);
    // 解除阻塞SIGINT信号
    if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }
    printf("SIGINT is unblocked. Press Ctrl+C to test.\n");
    while (1) {
        sleep(1);
    }
    return 0;
}

挂起信号

当一个信号被阻塞时,如果该信号产生,它会被挂起(pending)。我们可以使用sigpending函数来检查当前进程有哪些信号处于挂起状态。

  1. sigpending函数
    • int sigpending(sigset_t *set);
      • 功能:获取当前进程中处于挂起状态的信号集,并将其存储在set中。
      • 返回值:成功返回0,失败返回-1。

以下代码示例展示了如何使用sigpending函数:

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

void print_pending_signals() {
    sigset_t pending_set;
    if (sigpending(&pending_set) == -1) {
        perror("sigpending");
        return;
    }
    printf("Pending signals: ");
    for (int i = 1; i < NSIG; i++) {
        if (sigismember(&pending_set, i) == 1) {
            printf("%d ", i);
        }
    }
    printf("\n");
}

int main() {
    sigset_t set;
    // 初始化信号集为空
    if (sigemptyset(&set) == -1) {
        perror("sigemptyset");
        return 1;
    }
    // 添加SIGINT信号到信号集
    if (sigaddset(&set, SIGINT) == -1) {
        perror("sigaddset");
        return 1;
    }
    // 阻塞SIGINT信号
    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }
    printf("SIGINT is blocked. Send SIGINT (Ctrl+C) to make it pending.\n");
    sleep(10);
    print_pending_signals();
    // 解除阻塞SIGINT信号
    if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }
    printf("SIGINT is unblocked. The pending SIGINT should be delivered now.\n");
    sleep(5);
    return 0;
}

信号集与阻塞机制的应用场景

  1. 确保关键代码段的执行
    • 在一些关键代码段,如共享资源的访问,为了防止信号的干扰导致数据不一致,可以在进入关键代码段前阻塞相关信号,在离开关键代码段后解除阻塞。
    • 示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int shared_variable = 0;

void critical_section() {
    sigset_t set;
    sigset_t oldset;
    // 初始化信号集为空
    if (sigemptyset(&set) == -1) {
        perror("sigemptyset");
        return;
    }
    // 添加SIGINT信号到信号集
    if (sigaddset(&set, SIGINT) == -1) {
        perror("sigaddset");
        return;
    }
    // 阻塞SIGINT信号
    if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
        perror("sigprocmask");
        return;
    }
    // 关键代码段
    printf("Entering critical section. shared_variable = %d\n", shared_variable);
    shared_variable++;
    sleep(2);
    printf("Leaving critical section. shared_variable = %d\n", shared_variable);
    // 恢复原来的信号掩码
    if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
        perror("sigprocmask");
        return;
    }
}

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

int main() {
    // 注册信号处理函数
    if (signal(SIGINT, signal_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }
    critical_section();
    return 0;
}
  1. 进程间同步
    • 信号阻塞机制可以用于进程间同步。例如,父进程可以阻塞某个信号,子进程在完成特定任务后向父进程发送该信号,父进程解除信号阻塞后处理信号,从而实现进程间的同步。
    • 示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

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

int main() {
    sigset_t set;
    // 初始化信号集为空
    if (sigemptyset(&set) == -1) {
        perror("sigemptyset");
        return 1;
    }
    // 添加SIGUSR1信号到信号集
    if (sigaddset(&set, SIGUSR1) == -1) {
        perror("sigaddset");
        return 1;
    }
    // 阻塞SIGUSR1信号
    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }
    // 注册信号处理函数
    if (signal(SIGUSR1, signal_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        sleep(2);
        printf("Child sending SIGUSR1 to parent\n");
        if (kill(getppid(), SIGUSR1) == -1) {
            perror("kill");
            return 1;
        }
        exit(0);
    } else {
        // 父进程
        printf("Parent waiting for SIGUSR1\n");
        sigset_t oldset;
        // 解除阻塞SIGUSR1信号
        if (sigprocmask(SIG_UNBLOCK, &set, &oldset) == -1) {
            perror("sigprocmask");
            return 1;
        }
        wait(NULL);
    }
    return 0;
}
  1. 防止信号处理函数的重入问题
    • 当一个信号处理函数正在执行时,如果又收到相同的信号,可能会导致重入问题(re - entry problem),即信号处理函数再次进入自身,造成数据混乱。通过阻塞信号,可以防止在信号处理函数执行期间再次收到相同的信号。
    • 示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int global_variable = 0;

void signal_handler(int signum) {
    sigset_t set;
    sigset_t oldset;
    // 初始化信号集为空
    if (sigemptyset(&set) == -1) {
        perror("sigemptyset");
        return;
    }
    // 添加当前信号到信号集
    if (sigaddset(&set, signum) == -1) {
        perror("sigaddset");
        return;
    }
    // 阻塞当前信号
    if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
        perror("sigprocmask");
        return;
    }
    // 信号处理函数代码
    printf("Received signal %d. global_variable = %d\n", signum, global_variable);
    global_variable++;
    sleep(2);
    printf("Leaving signal handler. global_variable = %d\n", global_variable);
    // 恢复原来的信号掩码
    if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
        perror("sigprocmask");
        return;
    }
}

int main() {
    // 注册信号处理函数
    if (signal(SIGINT, signal_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }
    while (1) {
        sleep(1);
    }
    return 0;
}

信号集与阻塞机制的注意事项

  1. 不可靠信号 在早期的UNIX系统中,信号是不可靠的,可能会出现信号丢失的情况。即使信号被阻塞,也不能保证信号一定不会丢失。在现代的Linux系统中,大多数信号已经是可靠的,但在编写代码时仍需注意这个历史遗留问题。
  2. 信号处理函数中的操作 在信号处理函数中,应尽量避免复杂的操作,因为信号处理函数可能在任何时候被调用,包括进程执行系统调用等关键时期。例如,避免在信号处理函数中调用标准I/O函数(如printf),除非这些函数是可重入的。
  3. 信号掩码的继承 当进程调用fork创建子进程时,子进程会继承父进程的信号掩码。这意味着如果父进程阻塞了某些信号,子进程默认也会阻塞这些信号。在需要子进程以不同的信号掩码运行时,需要在子进程中重新设置信号掩码。
  4. 信号与系统调用的交互 如果一个进程在执行系统调用时收到信号,默认情况下,系统调用会被中断并返回错误EINTR。在处理信号时,需要考虑这种情况,可能需要重新发起系统调用。例如:
#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() {
    // 注册信号处理函数
    if (signal(SIGINT, signal_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }
    ssize_t bytes_read;
    char buffer[1024];
    while (1) {
        bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
        if (bytes_read == -1 && errno == EINTR) {
            printf("Read was interrupted by a signal. Retrying...\n");
            continue;
        } else if (bytes_read == -1) {
            perror("read");
            return 1;
        }
        buffer[bytes_read] = '\0';
        printf("Read: %s", buffer);
    }
    return 0;
}

通过深入理解Linux C语言中的信号集与阻塞机制,并在实际编程中合理运用,我们可以编写出更健壮、更可靠的程序,能够有效地处理各种异步事件,提高程序的稳定性和性能。同时,注意上述提到的注意事项,可以避免在使用过程中出现难以调试的问题。