进程间通信(IPC)
进程间通信(Inter Process Communication,简称 IPC)是进程间的信息交换,核心目的包括数据传输、共享资源、控制进程,方便对进程的管理与调度。常见 IPC 方式有管道通信、信号通信、共享内存、消息队列、信号量组、POSIX 信号量等,本文重点详解管道和信号两种基础且常用的通信方式。
管道通信
管道通信是 Linux 系统提供的单向(半双工)通信方式,同一时刻只能实现发送或接收数据,类似现实中的管道,数据从一端写入、另一端读出。管道在 Linux 中属于特殊文件,分为匿名管道和命名管道两类。

管道核心特性
- 通信方向:半双工,需明确读写端分工
- 数据存储:写入的数据暂存于内核缓冲区(默认 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()前创建管道

示例代码:父子进程匿名管道通信
需求:子进程发送字符串给父进程,父进程读取并输出到终端
#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.txtchar 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); // 创建失败,清理FIFOexit(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 兼容系统中进程间异步通信的方式,用于通知进程某个事件发生,可中断进程的非原子操作并触发相应处理。
核心概念
同步与异步通信的区别
-
同步通信:进程发起请求后,需阻塞等待响应,直到请求完成才能继续执行(如 “一手交钱,一手交货”)

-
异步通信:进程发起请求后,无需等待,继续执行其他任务,响应完成后通过信号通知进程处理(如 “记账消费,后续付款”)
-
异步优势:提高程序执行效率,避免进程阻塞浪费 CPU 资源

信号的通信机制
-
信号是异步通知:发送方无需等待接收方响应
-
信号触发时,操作系统会中断接收进程的当前操作,优先处理信号
-
接收进程可自定义信号处理逻辑,无自定义则执行系统默认动作

信号的类型
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的PIDint 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 128int 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);// 发送自己PIDwrite(tx_fd, &my_pid, sizeof(my_pid));// 接收B的PIDread(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的写端// 交换PIDexchange_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 128int 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);// 发送自己PIDwrite(tx_fd, &my_pid, sizeof(my_pid));// 接收A的PIDread(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的读端// 交换PIDexchange_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 ())
- 注意事项:管道读写阻塞特性、信号处理函数的线程安全、特殊信号的不可修改性