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

Linux C语言信号屏蔽的跨进程同步

2023-11-027.0k 阅读

Linux C 语言信号屏蔽的跨进程同步

信号基础概念

在 Linux 系统中,信号(Signal)是一种异步通知机制,用于向进程传达事件的发生。信号可以由内核、其他进程或者用户通过键盘输入(如 Ctrl+C 产生 SIGINT 信号)发送给目标进程。每个信号都有一个唯一的编号和名称,例如 SIGTERM(终止信号)编号为 15,SIGKILL(强制终止信号)编号为 9 等。

信号处理机制允许进程对特定信号做出响应,例如进程可以选择忽略某个信号、执行默认操作(如 SIGTERM 的默认操作是终止进程)或者注册一个信号处理函数来自定义处理逻辑。在 C 语言中,我们使用 signal 函数或者更强大的 sigaction 函数来处理信号。

信号屏蔽与阻塞

信号屏蔽(Signal Masking)是指进程可以选择暂时阻止某些信号的传递,将这些信号加入到信号掩码中。当信号被屏蔽时,它不会立即被递送到进程,而是被挂起(Pending),直到该信号从信号掩码中移除,进程才会收到并处理该信号。

在 C 语言中,我们可以使用 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 是一个用来表示信号集的数据类型,通过一系列函数可以对其进行操作,例如 sigemptyset(清空信号集)、sigaddset(向信号集中添加信号)、sigdelset(从信号集中删除信号)等。
  • oldset 参数是一个可选参数,如果不为 NULL,函数会将原来的信号掩码保存到 oldset 指向的 sigset_t 中,以便后续恢复。

跨进程同步的需求

在多进程编程中,进程之间需要一种方式来协调它们的执行顺序,以避免竞争条件和数据不一致等问题。信号屏蔽可以作为一种简单而有效的跨进程同步手段。例如,一个进程可能需要等待另一个进程完成某个操作后才能继续执行,这时可以通过信号来通知,并利用信号屏蔽来控制信号的接收时机。

基于信号屏蔽的跨进程同步实现方式

  1. 父子进程间同步
    • 假设父进程创建子进程后,子进程需要先执行一些初始化操作,完成后通知父进程。父进程在接收到通知前可以屏蔽相应信号,避免提前处理通知。
    • 以下是代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

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

int main() {
    sigset_t set, oldset;
    // 初始化信号集
    sigemptyset(&set);
    // 将 SIGUSR1 信号添加到信号集
    sigaddset(&set, SIGUSR1);

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

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("Child is doing some initialization...\n");
        sleep(2);
        printf("Child finished initialization, sending signal to parent\n");
        kill(getppid(), SIGUSR1);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        // 注册信号处理函数
        signal(SIGUSR1, signal_handler);
        // 等待信号
        sigsuspend(&oldset);
        printf("Parent is resuming after receiving signal\n");
        wait(NULL);
    }
    return 0;
}

在这段代码中,父进程首先屏蔽了 SIGUSR1 信号,然后创建子进程。子进程在完成初始化操作(这里用 sleep 模拟)后,向父进程发送 SIGUSR1 信号。父进程通过 sigsuspend 函数等待信号,sigsuspend 会暂时将信号掩码设置为 oldset(即解除对 SIGUSR1 的屏蔽),并等待信号的到来。当接收到 SIGUSR1 信号后,sigsuspend 返回,父进程恢复执行。

  1. 无亲缘关系进程间同步
    • 对于无亲缘关系的进程,也可以通过信号屏蔽实现同步。假设进程 A 和进程 B,进程 A 先启动并等待进程 B 的通知。
    • 以下是代码示例:
// 进程 A 的代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

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

int main() {
    sigset_t set, oldset;
    sigemptyset(&set);
    sigaddset(&set, SIGUSR1);
    sigprocmask(SIG_BLOCK, &set, &oldset);

    // 注册信号处理函数
    signal(SIGUSR1, signal_handler);

    printf("Process A is waiting for signal from Process B\n");
    sigsuspend(&oldset);
    printf("Process A is resuming after receiving signal\n");

    return 0;
}
// 进程 B 的代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

int main() {
    sleep(3);
    printf("Process B is sending signal to Process A\n");
    // 假设进程 A 的 PID 已知为 1234(实际应用中需要动态获取)
    kill(1234, SIGUSR1);
    return 0;
}

在实际应用中,进程 B 需要知道进程 A 的 PID 才能发送信号。可以通过一些进程间通信机制(如文件、共享内存等)来传递 PID。这里为了简化示例,假设进程 A 的 PID 为 1234 硬编码在进程 B 中。进程 A 屏蔽 SIGUSR1 信号后等待,进程 B 延迟 3 秒后向进程 A 发送 SIGUSR1 信号,进程 A 接收到信号后恢复执行。

信号屏蔽跨进程同步的优缺点

  1. 优点
    • 简单易用:基于信号的跨进程同步机制相对简单,不需要复杂的共享内存或消息队列等机制的配置。只需要熟悉信号相关的函数(如 sigprocmaskkillsigsuspend 等)即可实现基本的同步功能。
    • 异步特性:信号是异步通知机制,这使得进程在等待同步信号时可以继续执行其他任务,不会像一些同步机制(如互斥锁)那样阻塞进程的执行,从而提高了进程的并发处理能力。
  2. 缺点
    • 信号数量有限:Linux 系统中可用的信号数量是有限的,不同系统可能有所不同,但通常数量不是很多。这限制了可以用于同步的信号种类,在复杂的多进程同步场景中可能不够用。
    • 信号处理的复杂性:信号处理函数的执行上下文比较特殊,在信号处理函数中需要注意避免使用一些不安全的函数(如标准 I/O 函数,因为它们可能不是异步信号安全的),这增加了编程的复杂性。而且信号可能会在进程执行的任何时刻到达,可能会打断进程的正常执行流程,需要仔细处理以避免数据不一致等问题。
    • PID 依赖:在无亲缘关系进程间同步时,发送信号需要知道目标进程的 PID。获取和管理进程 PID 可能会变得复杂,特别是在动态创建和销毁进程的场景中。

实际应用场景

  1. 守护进程与控制进程同步:守护进程在后台运行,执行一些长期任务。控制进程可以通过信号与守护进程同步,例如控制进程向守护进程发送特定信号,通知守护进程重新加载配置文件。守护进程在处理配置文件加载等关键操作时,可以暂时屏蔽信号,避免在操作过程中被打断,完成后再解除屏蔽,等待下一次信号。
  2. 多进程数据处理流水线:在数据处理流水线中,不同的进程负责不同的处理阶段。例如,一个进程负责数据采集,另一个进程负责数据处理。数据采集进程完成一批数据采集后,通过信号通知数据处理进程。数据处理进程在处理数据前可以屏蔽信号,防止在处理过程中被新的信号打断,处理完成后再解除屏蔽接收新的通知。

注意事项

  1. 异步信号安全:在信号处理函数中,只能调用异步信号安全的函数。异步信号安全的函数列表可以在相关文档中查找,一般像 write_exit 等函数是异步信号安全的,而 printf 等标准 I/O 函数不是。如果在信号处理函数中调用了非异步信号安全的函数,可能会导致程序崩溃或数据不一致。
  2. 信号丢失问题:如果在信号被屏蔽期间,信号多次发送,一般情况下信号不会累积,只会记录一次(实时信号除外)。这可能会导致信号丢失的问题,在设计同步机制时需要考虑这种情况,例如可以采用其他机制(如计数器、共享内存标志等)来辅助确认信号的接收情况。
  3. 信号优先级:不同信号在系统中有不同的优先级,虽然信号屏蔽主要是控制信号的接收时机,但信号优先级可能会影响信号的处理顺序。在多信号同步场景中,需要了解信号优先级对同步逻辑的潜在影响。

通过合理利用信号屏蔽,在 Linux C 语言编程中可以实现有效的跨进程同步,尽管存在一些局限性,但在许多场景下它提供了一种简洁的同步解决方案。在实际应用中,需要综合考虑系统需求、信号特性以及其他进程间通信机制的优缺点,选择最合适的同步方式。