解码IPC-管道与信号

进程间通信(IPC)

进程间通信(Inter Process Communication,简称 IPC)是进程间的信息交换,核心目的包括数据传输、共享资源、控制进程,方便对进程的管理与调度。常见 IPC 方式有管道通信、信号通信、共享内存、消息队列、信号量组、POSIX 信号量等,本文重点详解管道和信号两种基础且常用的通信方式。

管道通信

管道通信是 Linux 系统提供的单向(半双工)通信方式,同一时刻只能实现发送或接收数据,类似现实中的管道,数据从一端写入、另一端读出。管道在 Linux 中属于特殊文件,分为匿名管道和命名管道两类。

image

管道核心特性

  • 通信方向:半双工,需明确读写端分工
  • 数据存储:写入的数据暂存于内核缓冲区(默认 4M)
  • 读写规则:
    • 写速快于读速:缓冲区满时,写操作阻塞
    • 读速快于写速:管道无数据时,读操作阻塞
    • 不支持 lseek 操作:无法指定位置读写数据

匿名管道(pipe)

匿名管道无文件名,仅适用于有亲缘关系的进程(如父子进程),通过系统调用pipe()创建,依赖文件描述符进行读写。

创建函数:pipe ()

#include <unistd.h>
/**
 * @brief 创建匿名管道,返回读写端文件描述符
 * @param pipefd[2]:输出型参数,存储管道的读写端文件描述符
 *        pipefd[0]:管道读端的文件描述符,仅用于read操作
 *        pipefd[1]:管道写端的文件描述符,仅用于write操作
 * @return 成功返回0,失败返回-1(此时errno会被设置为对应错误码,pipefd保持不变)
 * @note 必须在fork创建子进程前调用,子进程会复制父进程的文件描述符,从而共享管道
 *       内核缓冲区默认4M,数据写入后会暂存,直到被读出
 *       仅支持亲缘进程通信,无文件名,无法通过open函数打开
 */
int pipe(int pipefd[2]);

关键特性

  • 无文件名,无法通过open()创建或打开,仅能通过pipe()创建
  • 仅适用于亲缘进程(父子、兄弟进程),依赖进程复制的文件描述符访问
  • 数据读写遵循 “先进先出”,不保证数据原子性(多进程同时写可能出现数据交织)
  • 子进程会完全复制父进程的代码段、数据段、堆栈段及文件描述符,因此需在fork()前创建管道

image

示例代码:父子进程匿名管道通信

需求:子进程发送字符串给父进程,父进程读取并输出到终端

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
    // 创建匿名管道:pipefd[0]读端,pipefd[1]写端
    int pipefd[2] = {0};  // 初始化文件描述符数组
    int ret = pipe(pipefd);  // 调用pipe创建管道
    if (ret == -1)
    {
        // 打印错误信息:errno为错误码,strerror(errno)获取错误描述
        fprintf(stderr, "pipe error, errno:%d, %s \n", errno, strerror(errno));
        exit(1);  // 终止进程,将终止状态返回给父进程
    }

    // 创建子进程:子进程会复制父进程的文件描述符、代码段等资源
    int child_pid = fork();

    // 区分父进程、子进程及创建失败场景
    if (child_pid > 0)
    {
        // 父进程:从管道读端读取数据
        char recvbuf[128] = {0};  // 接收缓冲区初始化
        // 读操作:从pipefd[0]读,存入recvbuf,最多读128字节(缓冲区大小)
        // 管道无数据时,read会阻塞,直到有数据写入
        read(pipefd[0], recvbuf, sizeof(recvbuf));
        printf("my is parent, read from pipe data is [%s]\n", recvbuf);

        wait(NULL);  // 父进程阻塞,回收子进程资源,避免子进程成为僵尸进程
    }
    else if (child_pid == 0)
    {
        // 子进程:向管道写端写入数据
        char sendbuf[128] = "my is child, hello parent";  // 待发送数据
        // 写操作:向pipefd[1]写,数据来自sendbuf,长度为字符串实际长度
        write(pipefd[1], sendbuf, strlen(sendbuf));
    }
    else
    {
        // fork创建子进程失败
        fprintf(stderr, "fork error, errno:%d, %s\n", errno, strerror(errno));
        exit(2);  // 终止进程,返回错误状态码
    }

    return 0;
}

扩展:测试管道缓冲区大小

利用pipe2()的非阻塞特性,循环写入数据直到写失败,统计写入字节数即为缓冲区大小

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
    int pipefd[2] = {0};
    // pipe2():扩展pipe功能,第二个参数为标志位,O_NONBLOCK设置非阻塞模式
    int ret = pipe2(pipefd, O_NONBLOCK);
    if (ret == -1)
    {
        fprintf(stderr, "pipe2 error: %s\n", strerror(errno));
        exit(1);
    }

    char buf[1] = "a";  // 每次写入1字节
    int count = 0;
    // 循环写入,直到写失败(非阻塞模式下缓冲区满会返回-1,errno=EAGAIN)
    while (write(pipefd[1], buf, sizeof(buf)) == 1)
    {
        count++;
    }

    printf("管道缓冲区大小:%d 字节\n", count);
    close(pipefd[0]);
    close(pipefd[1]);
    return 0;
}

命名管道(FIFO)

匿名管道的局限性(仅亲缘进程、一对一通信)导致其适用场景受限,命名管道(FIFO)通过文件名标识,支持无亲缘关系的进程通信,且可多路同时写入。

创建函数:mkfifo ()

#include <sys/types.h>
#include <sys/stat.h>
/**
 * @brief 创建命名管道(FIFO特殊文件),用于无亲缘关系进程通信
 * @param pathname:命名管道的文件路径(必须是Linux系统内路径,不可在Windows共享文件夹创建)
 * @param mode:命名管道的权限,采用八进制表示(如0664),最终权限 = mode & ~umask
 *             权限规则:所有者、同组用户、其他用户的读(4)、写(2)、执行(1)权限组合
 * @return 成功返回0,失败返回-1(errno设置对应错误码,如文件已存在则errno=EEXIST)
 * @note 命名管道必须在两端同时打开(一端读、一端写)后,才能进行读写操作
 *       打开读端会阻塞,直到有进程打开写端;反之打开写端也会阻塞,直到有进程打开读端
 *       支持无亲缘关系进程通信,通过文件名定位管道,可通过open()打开
 */
int mkfifo(const char *pathname, mode_t mode);

关键特性

  • 有文件名,存在于文件系统中,可通过open()打开、unlink()删除
  • 支持无亲缘关系进程通信,多个进程可同时向管道写入(需保证数据原子性)
  • 读写规则与匿名管道一致,但支持多路写入,需注意同步问题
  • 不支持 lseek 操作,无法指定位置读写,数据先进先出

示例代码:两个无亲缘进程的命名管道通信

需求:进程 A 写入系统时间到命名管道,进程 B 读取并存储到 log.txt

进程 A(写端):写入系统时间

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#define FIFO_PATH "/tmp/myfifo"  
// 命名管道路径
int main()
{
    // 创建命名管道,权限0664(所有者、同组可读写,其他可读)
    int ret = mkfifo(FIFO_PATH, 0664);
    if (ret == -1 && errno != EEXIST)  // 忽略文件已存在的错误
    {
        fprintf(stderr, "mkfifo error: %s\n", strerror(errno));
        exit(1);
    }

    // 打开管道(O_WRONLY:只写模式),打开会阻塞直到读端打开
    int fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1)
    {
        fprintf(stderr, "open fifo error: %s\n", strerror(errno));
        exit(1);
    }

    // 获取系统时间并写入管道
    time_t now;
    char time_buf[64] = {0};
    time(&now);  // 获取当前时间戳
    strcpy(time_buf, ctime(&now));  // 转换为字符串格式(含换行符)
    write(fd, time_buf, strlen(time_buf));  // 写入管道

    // 关闭文件描述符
    close(fd);
    return 0;
}

进程 B(读端):读取并存储到 log.txt

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define FIFO_PATH "/tmp/myfifo"  // 与写端路径一致
int main()
{
    // 打开管道(O_RDONLY:只读模式),打开会阻塞直到写端打开
    int fifo_fd = open(FIFO_PATH, O_RDONLY);
    if (fifo_fd == -1)
    {
        fprintf(stderr, "open fifo error: %s\n", strerror(errno));
        exit(1);
    }

    // 打开log.txt文件(O_WRONLY|O_CREAT|O_APPEND:写+创建+追加模式)
    int log_fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0664);
    if (log_fd == -1)
    {
        fprintf(stderr, "open log.txt error: %s\n", strerror(errno));
        close(fifo_fd);
        exit(1);
    }

    // 从管道读取数据并写入log.txt
    char recv_buf[64] = {0};
    int read_len = read(fifo_fd, recv_buf, sizeof(recv_buf));
    if (read_len > 0)
    {
        write(log_fd, recv_buf, read_len);  // 写入文件
        printf("已写入日志:%s", recv_buf);
    }

    // 关闭文件描述符
    close(fifo_fd);
    close(log_fd);
    return 0;
}

:进程 A 写入数据后关闭管道,进程 B 读取部分数据后关闭,下次打开管道能否读取遗留数据?
:不能。命名管道的内核缓冲区数据一旦被读取就会被清除,且管道关闭后缓冲区会被释放,下次打开是全新的通信通道,无遗留数据。

命名管道(FIFO)日志收集器

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

// 配置参数(可自定义)
#define FIFO_PATH "./log_fifo"   // 命名管道路径
#define LOG_FILE "./app.log"     // 日志文件路径
#define BUF_SIZE 1024            // 读取缓冲区大小

// 全局变量(信号处理函数中需访问)
int fifo_fd = -1;  // FIFO文件描述符
int log_fd = -1;   // 日志文件描述符

// 信号处理函数:捕获Ctrl+C,清理资源后退出
void sigint_handler(int sig) {
    printf("\n[Log Reader] Received Ctrl+C, cleaning up...\n");
    // 关闭文件描述符
    if (fifo_fd != -1) close(fifo_fd);
    if (log_fd != -1) close(log_fd);
    // 删除命名管道(可选,也可保留供下次使用)
    unlink(FIFO_PATH);
    printf("[Log Reader] Cleanup done, exit.\n");
    exit(0);
}

// 获取当前时间戳(格式:YYYY-MM-DD HH:MM:SS)
void get_timestamp(char *buf, size_t buf_len) {
    time_t now = time(NULL);
    struct tm *tm = localtime(&now);
    strftime(buf, buf_len, "%Y-%m-%d %H:%M:%S", tm);
}

int main() {
    char buf[BUF_SIZE];
    char timestamp[32];
    ssize_t read_len;

    // 注册信号处理函数(捕获Ctrl+C)
    signal(SIGINT, sigint_handler);

    // 创建命名管道(FIFO):不存在则创建,存在则忽略(避免EEXIST错误)
    if (mkfifo(FIFO_PATH, 0666) == -1) {
        if (errno != EEXIST) {  // 非“已存在”错误才退出
            perror("mkfifo failed");
            exit(1);
        }
        printf("[Log Reader] FIFO already exists, reuse it.\n");
    } else {
        printf("[Log Reader] FIFO created: %s\n", FIFO_PATH);
    }

    // 以可读可写模式打开FIFO(O_RDWR):避免单独O_RDONLY时阻塞
    fifo_fd = open(FIFO_PATH, O_RDWR);
    if (fifo_fd == -1) {
        perror("open FIFO failed");
        unlink(FIFO_PATH);  // 创建失败,清理FIFO
        exit(1);
    }
    printf("[Log Reader] FIFO opened (RDWR mode), waiting for data...\n");

    // 打开日志文件(追加模式:O_APPEND,不存在则创建:O_CREAT)
    log_fd = open(LOG_FILE, O_WRONLY | O_APPEND | O_CREAT, 0644);
    if (log_fd == -1) {
        perror("open log file failed");
        sigint_handler(SIGINT);  // 调用清理函数退出
    }

    // 循环读取FIFO内容,写入日志文件
    while (1) {
        // 读取FIFO:阻塞直到有数据写入,或写端全部关闭
        read_len = read(fifo_fd, buf, BUF_SIZE - 1);  // 留1字节存终止符
        if (read_len == -1) {
            if (errno == EINTR) continue;  // 被信号中断,继续读取
            perror("read FIFO failed");
            break;
        } else if (read_len == 0) {
            // 写端全部关闭,FIFO读端返回0,这里继续等待(支持重新连接写端)
            printf("[Log Reader] All writers closed, waiting for new writer...\n");
            continue;
        }

        // 处理读取到的数据(添加终止符,避免乱码)
        buf[read_len] = '\0';
        // 去除换行符(如果写端输入时带换行)
        if (buf[read_len - 1] == '\n') {
            buf[read_len - 1] = '\0';
        }

        // 获取时间戳
        get_timestamp(timestamp, sizeof(timestamp));

        // 格式化日志内容(时间戳 + 数据)
        char log_buf[BUF_SIZE + 64];
        snprintf(log_buf, sizeof(log_buf), "[%s] %s\n", timestamp, buf);

        // 写入日志文件
        write(log_fd, log_buf, strlen(log_buf));
        // 同时打印到终端(可选,方便实时查看)
        printf("[Log Reader] Logged: %s", log_buf);
        fsync(log_fd);  // 强制刷新缓冲区,确保日志立即写入文件
    }

    // 异常退出时清理资源
    sigint_handler(SIGINT);
    return 0;
}

:使用echo命令往日志管道文件log_fifo里面写内容测试或使用其他进程往日志管道文件log_fifo里面写日志内容

信号通信

信号(signal)是 Unix/Linux 及 POSIX 兼容系统中进程间异步通信的方式,用于通知进程某个事件发生,可中断进程的非原子操作并触发相应处理。

核心概念

同步与异步通信的区别

  • 同步通信:进程发起请求后,需阻塞等待响应,直到请求完成才能继续执行(如 “一手交钱,一手交货”)

    image

  • 异步通信:进程发起请求后,无需等待,继续执行其他任务,响应完成后通过信号通知进程处理(如 “记账消费,后续付款”)

  • 异步优势:提高程序执行效率,避免进程阻塞浪费 CPU 资源

    image

信号的通信机制

  • 信号是异步通知:发送方无需等待接收方响应

  • 信号触发时,操作系统会中断接收进程的当前操作,优先处理信号

  • 接收进程可自定义信号处理逻辑,无自定义则执行系统默认动作

    image

信号的类型

Linux 系统中信号编号为 164,分为普通信号(131)和实时信号(34~64)两类,可通过kill -l命令查看所有信号。

普通信号(不可靠信号)

  • 编号 1~31,继承自 Unix 系统
  • 特性:信号未及时处理时,不会排队,仅保留一个,其余丢弃(可能丢失信号)
  • 部分常用普通信号及含义:
编号 信号名 触发场景 默认动作
1 SIGHUP 用户退出 shell,其启动的进程接收 终止进程
2 SIGINT 用户按下 Ctrl+C 终止进程
3 SIGQUIT 用户按下 Ctrl+\ 终止进程并生成 core 文件
9 SIGKILL 强制终止进程 终止进程(不可捕捉 / 阻塞)
15 SIGTERM 正常终止进程(kill 命令默认信号) 终止进程
17 SIGCHLD 子进程终止或停止 忽略信号
19 SIGSTOP 暂停进程 暂停进程(不可捕捉 / 阻塞)

实时信号(可靠信号)

  • 编号 34~64,Linux 新增
  • 特性:信号未及时处理时,会排队存储,按顺序依次处理,不丢失信号
  • 无固定含义,可由用户自定义使用

关键注意

  • SIGKILL(9)和 SIGSTOP(19)是特殊信号,不可被捕捉、阻塞或忽略,只能执行默认动作
  • 信号名均为宏定义,存储在/usr/include/x86_64-linux-gnu/asm/signal.h头文件中

信号的产生方式

信号的产生源于特定条件触发,主要分为 5 类:

按键触发

用户按下终端快捷键,内核向当前前台进程发送信号:

  • Ctrl+C:触发 SIGINT 信号(终止进程)
  • Ctrl+\:触发 SIGQUIT 信号(终止并生成 core 文件)
  • Ctrl+Z:触发 SIGTSTP 信号(暂停进程)
  • 注意:快捷键仅对前台进程有效,后台进程(加&运行)不受影响

硬件异常

硬件检测到错误时,内核向出错进程发送信号:

  • 除以 0:触发 SIGFPE 信号(浮点运算异常)
  • 非法内存访问:触发 SIGSEGV 信号(段错误)
  • 非法指令执行:触发 SIGILL 信号(非法指令)

系统调用触发

通过系统函数主动发送信号,常用函数有kill()raise()

函数 1:kill ()

#include <sys/types.h>
#include <signal.h>
/**
 * @brief 向指定进程或进程组发送信号
 * @param pid:目标进程/进程组ID
 *            pid > 0:发送给PID为pid的进程
 *            pid = 0:发送给当前进程所在进程组的所有进程
 *            pid = -1:发送给当前进程有权限发送的所有进程(排除init进程和自身)
 *            pid < -1:发送给进程组ID为-pid的所有进程
 * @param sig:要发送的信号编号(如SIGKILL、SIGUSR1)或0(仅检查权限,不发送信号)
 * @return 成功返回0(至少发送给一个进程),失败返回-1(errno设置错误码)
 * @note 发送信号需权限:要么进程有CAP_KILL特权,要么发送者与接收者的用户ID匹配
 *       信号sig为0时,仅做存在性和权限检查,不触发信号处理动作
 */
int kill(pid_t pid, int sig);

函数 2:raise ()

#include <signal.h>
/**
 * @brief 向当前进程(或线程)发送信号
 * @param sig:要发送的信号编号
 * @return 成功返回0,失败返回非0值
 * @note 单线程程序中等价于kill(getpid(), sig)
 *       多线程程序中等价于pthread_kill(pthread_self(), sig)
 *       信号处理函数执行完成后,raise()才会返回
 */
int raise(int sig);

终端命令触发

通过kill命令发送信号,本质是调用kill()函数:

  • 格式:kill [选项] <pid>
  • 默认发送 SIGTERM 信号(15)
  • 指定信号:kill -9 <pid>(发送 SIGKILL 信号)、kill -SIGUSR1 <pid>(发送 SIGUSR1 信号)

内核检测触发

内核检测到特定软件条件时发送信号:

  • 闹钟超时:alarm()函数设置的定时器到期,触发 SIGALRM 信号
  • 管道写端无读者:向管道写数据但读端已关闭,触发 SIGPIPE 信号

辅助函数:alarm ()

#include <unistd.h>
/**
 * @brief 设置内核定时器,到期后向当前进程发送SIGALRM信号
 * @param seconds:定时器时长(秒),seconds=0时取消已设置的闹钟
 * @return 成功返回之前未到期的闹钟剩余秒数,无之前闹钟则返回0
 * @note 闹钟不堆叠,仅能设置一个未到期的闹钟,重复调用会覆盖之前的设置
 *       定时器计时是实时时间,不受进程阻塞状态影响
 *       SIGALRM信号默认动作是终止进程,需自定义处理函数避免进程退出
 */
unsigned alarm(unsigned seconds);

信号的处理方式

进程接收信号后,有 3 种处理方式:默认处理、捕捉处理、忽略处理。

默认处理

系统为每个信号预设的动作,无自定义处理时执行:

  • Term:终止进程(如 SIGINT、SIGTERM)
  • Core:终止进程并生成 core 文件(用于调试,如 SIGQUIT、SIGSEGV)
  • Ign:忽略信号(如 SIGCHLD、SIGURG)
  • Stop:暂停进程(如 SIGSTOP、SIGTSTP)
  • Cont:恢复暂停的进程(如 SIGCONT)

捕捉处理

自定义信号处理函数,将信号与函数关联,信号触发时执行自定义逻辑,而非默认动作。

核心函数:signal ()

#include <signal.h>
/**
 * @brief 设置信号的处理方式(捕捉、默认、忽略)
 * @param signum:目标信号编号(如SIGUSR1、SIGINT),SIGKILL和SIGSTOP除外
 * @param handler:信号处理方式
 *                SIG_IGN:忽略该信号
 *                SIG_DFL:执行默认处理动作
 *                函数指针:自定义处理函数(格式:void (*sighandler_t)(int))
 * @return 成功返回之前的信号处理方式,失败返回SIG_ERR(errno设置错误码)
 * @note 不同系统中signal()行为可能不一致,推荐使用sigaction()替代
 *       自定义处理函数格式:void func(int signum),无返回值,参数为信号编号
 *       信号处理函数是独立控制流程,与主流程异步,需注意全局资源访问冲突
 *       处理函数执行期间,当前信号可能被自动阻塞,返回后解除阻塞
 */
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

示例代码 1:自定义信号处理(进程 A 接收 SIGUSR1 信号)

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 自定义信号处理函数
void signal_handler(int signum)
{
    // 根据信号编号区分处理逻辑
    switch(signum)
    {
        case SIGUSR1:
            printf("进程A收到SIGUSR1信号,执行自定义处理\n");
            break;
        case SIGUSR2:
            printf("进程A收到SIGUSR2信号,执行自定义处理\n");
            break;
        default:
            printf("收到未知信号:%d\n", signum);
    }
}

int main()
{
    printf("进程A PID:%d\n", getpid());
    // 设置SIGUSR1和SIGUSR2的处理函数
    signal(SIGUSR1, signal_handler);
    signal(SIGUSR2, signal_handler);
    // 忽略SIGINT信号(Ctrl+C无法终止进程)
    signal(SIGINT, SIG_IGN);

    // 死循环等待信号
    while(1)
    {
        sleep(1);  // 降低CPU占用
    }

    return 0;
}

示例代码 2:进程 B 向进程 A 发送信号

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("用法:%s <进程A PID>\n", argv[0]);
        exit(1);
    }

    pid_t a_pid = atoi(argv[1]);  // 进程A的PID
    int count = 0;

    // 每隔2秒发送一次SIGUSR1信号
    while(1)
    {
        kill(a_pid, SIGUSR1);
        printf("已向进程A发送SIGUSR1信号(第%d次)\n", ++count);
        sleep(2);
    }

    return 0;
}

忽略处理

进程接收到信号后,直接丢弃,不执行任何动作。通过signal(signum, SIG_IGN)设置:

// 忽略Ctrl+C触发的SIGINT信号,进程不会被终止
signal(SIGINT, SIG_IGN);

信号的阻塞

进程暂时不希望响应某些信号时,可将其阻塞(屏蔽),被阻塞的信号不会递达,直到解除阻塞。

核心函数与操作

信号阻塞通过 “信号集” 管理,常用函数包括sigemptyset()sigaddset()sigprocmask()等:

#include <signal.h>
/**
 * @brief 初始化信号集为空(所有信号均不包含)
 * @param set:指向信号集的指针
 * @return 成功返回0,失败返回-1
 * @note 信号集使用前必须初始化,否则结果未定义
 */
int sigemptyset(sigset_t *set);

/**
 * @brief 向信号集添加指定信号
 * @param set:指向信号集的指针(需已初始化)
 * @param signum:要添加的信号编号
 * @return 成功返回0,失败返回-1
 */
int sigaddset(sigset_t *set, int signum);

/**
 * @brief 设置进程的信号掩码(阻塞/解除阻塞信号)
 * @param how:操作方式
 *            SIG_BLOCK:添加信号集到掩码(阻塞信号集中的信号)
 *            SIG_UNBLOCK:从掩码中移除信号集(解除阻塞)
 *            SIG_SETMASK:用信号集替换当前掩码(设置新的阻塞信号集)
 * @param set:要操作的信号集(NULL表示不改变信号集)
 * @param oldset:保存原来的信号掩码(NULL表示不保存)
 * @return 成功返回0,失败返回-1(errno设置错误码)
 * @note 阻塞与忽略的区别:阻塞是信号不递达,忽略是信号递达后不处理
 *       SIGKILL和SIGSTOP无法被阻塞
 */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

示例代码:阻塞 SIGINT 信号(Ctrl+C)

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
    sigset_t set;  // 定义信号集
    sigemptyset(&set);  // 初始化信号集为空
    sigaddset(&set, SIGINT);  // 将SIGINT信号添加到信号集

    // 设置信号掩码:阻塞信号集中的信号(SIGINT)
    int ret = sigprocmask(SIG_BLOCK, &set, NULL);
    if (ret == -1)
    {
        perror("sigprocmask error");
        exit(1);
    }

    printf("已阻塞Ctrl+C信号,进程不会被终止\n");
    while(1)
    {
        sleep(1);
    }

    return 0;
}

信号的挂起

进程的信号挂起是指:所有发送给进程的信号,会先存储在 “挂起信号集” 中,仅当进程处于运行态(获得 CPU 资源)时,才能处理这些信号。

  • 进程状态影响:就绪态 / 运行态可处理信号,阻塞态 / 暂停态无法处理,信号暂存于挂起集
  • 挂起信号集:存储进程的待处理信号,按信号类型排队(实时信号排队,普通信号不排队)
  • 信号处理时机:进程从内核态返回用户态时,会检查挂起信号集,处理未被阻塞的信号

示例:两个进程通过管道(两个)通信

进程A固定顺序:先创建→先开写端→再开读端

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

#define FIFO_A_TX "/tmp/fifo_a_to_b"  // A发B收
#define FIFO_A_RX "/tmp/fifo_b_to_a"  // B发A收
#define BUF_SIZE 128

int rx_fd = -1;
int tx_fd = -1;
pid_t peer_pid = -1;

// 初始化管道(先创建,再阻塞打开)
int fifo_init(const char *path, int mode) {
    // 先创建两个管道(确保双方都能访问)
    if (mkfifo(FIFO_A_TX, 0666) == -1 && errno != EEXIST) {
        perror("mkfifo A_TX failed");
        exit(EXIT_FAILURE);
    }
    if (mkfifo(FIFO_A_RX, 0666) == -1 && errno != EEXIST) {
        perror("mkfifo A_RX failed");
        exit(EXIT_FAILURE);
    }

    // 阻塞模式打开指定管道
    int fd = open(path, mode);
    if (fd == -1) {
        fprintf(stderr, "open %s failed: %s\n", path, strerror(errno));
        exit(EXIT_FAILURE);
    }
    printf("已打开管道:%s(fd=%d)\n", path, fd);
    return fd;
}

// 信号处理函数(异步安全)
void recv_msg(int sig) {
    if (rx_fd == -1) return;
    char buf[BUF_SIZE] = {0};
    ssize_t len = read(rx_fd, buf, BUF_SIZE - 1);
    if (len > 0) {
        write(STDOUT_FILENO, "收到B的消息:", 17);
        write(STDOUT_FILENO, buf, len);
        write(STDOUT_FILENO, "\n", 1);
    }
}

// 交换PID
void exchange_pid(int tx_fd) {
    pid_t my_pid = getpid();
    printf("A的PID:%d\n", my_pid);

    // 发送自己PID
    write(tx_fd, &my_pid, sizeof(my_pid));
    // 接收B的PID
    read(rx_fd, &peer_pid, sizeof(peer_pid));
    printf("B的PID:%d\n", peer_pid);
}

void cleanup(int sig) {
    (void)sig;
    if (rx_fd != -1) {
        close(rx_fd);
        rx_fd = -1;
    }
    if (tx_fd != -1) {
        close(tx_fd);
        tx_fd = -1;
    }
    printf("\n退出成功\n");
    exit(EXIT_SUCCESS);
}

int main(void) {
    // 固定初始化顺序:先开发送端(A_TX),再开接收端(A_RX)
    tx_fd = fifo_init(FIFO_A_TX, O_WRONLY);  // 阻塞等B开A_TX的读端
    rx_fd = fifo_init(FIFO_A_RX, O_RDONLY);  // 阻塞等B开A_RX的写端

    // 交换PID
    exchange_pid(tx_fd);

    // 注册信号
    signal(SIGRTMIN, cleanup);
    signal(SIGRTMIN, recv_msg);

    // 发送消息循环
    char buf[BUF_SIZE];
    while (1) {
		    signal(SIGINT,cleanup);
        printf("A请输入消息(回车发送):\n");
        fflush(stdout);
        bzero(buf,sizeof(buf));
        fgets(buf, BUF_SIZE, stdin);
        write(tx_fd, buf, strlen(buf));
        kill(peer_pid, SIGRTMIN);  // 通知B接收
    }
    return 0;
}

进程B固定顺序:先创建→先开读端→再开写端

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

#define FIFO_B_TX "/tmp/fifo_b_to_a"  // B发A收(对应A的RX)
#define FIFO_B_RX "/tmp/fifo_a_to_b"  // B收A发(对应A的TX)
#define BUF_SIZE 128

int rx_fd = -1;
int tx_fd = -1;
pid_t peer_pid = -1;

// 初始化管道(先创建,再阻塞打开)
int fifo_init(const char *path, int mode) {
    // 先创建两个管道(与A一致,确保存在)
    if (mkfifo(FIFO_B_TX, 0666) == -1 && errno != EEXIST) {
        perror("mkfifo B_TX failed");
        exit(EXIT_FAILURE);
    }
    if (mkfifo(FIFO_B_RX, 0666) == -1 && errno != EEXIST) {
        perror("mkfifo B_RX failed");
        exit(EXIT_FAILURE);
    }

    // 阻塞模式打开指定管道
    int fd = open(path, mode);
    if (fd == -1) {
        fprintf(stderr, "open %s failed: %s\n", path, strerror(errno));
        exit(EXIT_FAILURE);
    }
    printf("已打开管道:%s(fd=%d)\n", path, fd);
    return fd;
}

// 信号处理函数(异步安全)
void recv_msg(int sig) {
    if (rx_fd == -1) return;
    char buf[BUF_SIZE] = {0};
    ssize_t len = read(rx_fd, buf, BUF_SIZE - 1);
    if (len > 0) {
        write(STDOUT_FILENO, "收到A的消息:", 17);
        write(STDOUT_FILENO, buf, len);
        write(STDOUT_FILENO, "\n", 1);
    }
}

// 交换PID
void exchange_pid(int tx_fd) {
    pid_t my_pid = getpid();
    printf("B的PID:%d\n", my_pid);

    // 发送自己PID
    write(tx_fd, &my_pid, sizeof(my_pid));
    // 接收A的PID
    read(rx_fd, &peer_pid, sizeof(peer_pid));
    printf("A的PID:%d\n", peer_pid);
}

void cleanup(int sig) {
    (void)sig;
    if (rx_fd != -1) {
        close(rx_fd);
        rx_fd = -1;
    }
    if (tx_fd != -1) {
        close(tx_fd);
        tx_fd = -1;
    }
    printf("\n退出成功\n");
    exit(EXIT_SUCCESS);
}

int main(void) {
    // 固定初始化顺序:先开接收端(B_RX),再开发送端(B_TX)
    rx_fd = fifo_init(FIFO_B_RX, O_RDONLY);  // 阻塞等A开B_RX的写端
    tx_fd = fifo_init(FIFO_B_TX, O_WRONLY);  // 阻塞等A开B_TX的读端

    // 交换PID
    exchange_pid(tx_fd);

    // 注册信号
    signal(SIGINT,cleanup);
    signal(SIGRTMIN, recv_msg);

    // 发送消息循环
    char buf[BUF_SIZE];
    
    while (1) {
		    signal(SIGINT,cleanup);
        printf("B请输入消息(回车发送):\n");
        fflush(stdout);
        bzero(buf,sizeof(buf));
        fgets(buf, BUF_SIZE, stdin);
        write(tx_fd, buf, strlen(buf));
        kill(peer_pid, SIGRTMIN);  // 通知A接收
    }

    return 0;
}

核心总结

  • 管道通信:匿名管道适用于亲缘进程,命名管道支持无亲缘进程,均为半双工,依赖内核缓冲区
  • 信号通信:异步通知机制,分普通(不可靠)和实时(可靠)信号,SIGKILL 和 SIGSTOP 不可捕捉 / 阻塞
  • 关键函数:管道(pipe ()、mkfifo ())、信号(kill ()、signal ()、sigprocmask ())
  • 注意事项:管道读写阻塞特性、信号处理函数的线程安全、特殊信号的不可修改性
posted @ 2025-11-14 20:19  YouEmbedded  阅读(6)  评论(0)    收藏  举报