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

实时信号(可靠信号)

  • 编号 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-12-11 20:33  gccbuaa  阅读(11)  评论(0)    收藏  举报