在 Linux 系统中,进程是资源分配的基本单位,而进程间通信(IPC)则是实现进程协作、数据共享和事件通知的核心机制。无论是简单的命令行管道还是复杂的共享内存,掌握 IPC 技术都是 Linux 开发工程师的必备技能。本文将以经典 IPC 技术为脉络,结合代码实例,带你从原理到实战,全面掌握进程间通信的核心知识点。
一、进程间通信概述:为什么需要 IPC?
进程是操作系统中独立的执行单元,拥有独立的地址空间。但在实际开发中,进程往往需要协作完成任务 —— 比如一个进程采集数据,另一个进程分析数据,这就需要通过 IPC 打破进程的 “隔离墙”。
1.1 进程间通信的 4 个核心目的
数据传输:进程间需要传递数据(如文件内容、网络数据),例如 “who | wc -l” 命令中,who进程的输出需传递给wc -l进程。
资源共享:多个进程共享同一份资源(如配置文件、硬件设备),避免资源冗余存储。
事件通知:一个进程需向其他进程发送事件信号(如子进程退出时通知父进程回收资源)。
进程控制:控制进程(如 Debug 工具)需监控并干预目标进程的执行(如拦截异常、查看状态)。
1.2 IPC 技术的发展脉络
Linux 的 IPC 技术经历了三个主要阶段,不同阶段的技术适用于不同场景:
| 发展阶段 | 核心技术 | 特点 |
|---|---|---|
| 早期 Unix | 管道(匿名 / 命名) | 简单易用,适用于单向数据流 |
| System V | 消息队列、共享内存、信号量 | 内核级支持,生命周期随内核,适用于复杂场景 |
| POSIX | 消息队列、共享内存、互斥量、条件变量 | 跨平台兼容,接口更统一,支持线程间通信 |
二、管道:最经典的 IPC 方式
管道是 Unix 系统中最古老的 IPC 机制,其本质是内核维护的一个字节流缓冲区,进程通过文件描述符读写该缓冲区,实现数据传递。管道分为 “匿名管道” 和 “命名管道”,前者适用于亲缘进程,后者支持非亲缘进程通信。
2.1 匿名管道(pipe):亲缘进程的 “秘密通道”
匿名管道通过pipe()函数创建,返回两个文件描述符:fd[0](读端)和fd[1](写端),数据从写端流入、读端流出,是半双工(单向)通信。
2.1.1 匿名管道的核心原理
- 创建管道:父进程调用
pipe()创建管道,获得fd[0]和fd[1]。 - 共享管道:父进程
fork()子进程,子进程继承父进程的文件描述符,因此共享同一管道。 - 单向通信:父进程关闭读端(
close(fd[0])),子进程关闭写端(close(fd[1])),形成 “父写子读” 的单向通道(反之同理)。
2.1.2 匿名管道代码实例
下面的代码实现 “父进程从键盘读数据,写入管道;子进程从管道读数据,打印到屏幕”:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while(0)
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) ERR_EXIT("pipe error");
pid_t pid = fork();
if (pid == -1) ERR_EXIT("fork error");
if (pid == 0) { // 子进程:读管道
close(pipefd[1]); // 关闭写端
char buf[1024] = {0};
ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);
if (n > 0) printf("子进程收到:%s\n", buf);
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else { // 父进程:写管道
close(pipefd[0]); // 关闭读端
char buf[1024] = {0};
printf("父进程输入:");
fgets(buf, sizeof(buf), stdin);
write(pipefd[1], buf, strlen(buf));
close(pipefd[1]);
waitpid(pid, NULL, 0); // 等待子进程退出
}
return 0;
}
2.1.3 匿名管道的读写规则
管道的读写行为受 “阻塞 / 非阻塞” 模式影响,核心规则如下:
| 场景 | 阻塞模式(默认) | 非阻塞模式(O_NONBLOCK) |
|---|---|---|
| 管道无数据 | read 阻塞,直到有数据 | read 返回 - 1,errno=EAGAIN |
| 管道满(约 64KB) | write 阻塞,直到有进程读 | write 返回 - 1,errno=EAGAIN |
| 所有写端关闭 | read 返回 0(类似文件 EOF) | 同阻塞模式 |
| 所有读端关闭 | write 触发 SIGPIPE 信号,进程退出 | 同阻塞模式 |
2.2 命名管道(FIFO):非亲缘进程的 “公开管道”
匿名管道的局限是 “仅支持亲缘进程”,而命名管道(FIFO)通过文件系统中的特殊文件(FIFO 文件)标识管道,实现非亲缘进程的通信。
2.2.1 命名管道的创建与打开
命令行创建:mkfifo 管道文件名(如mkfifo myfifo)。
代码创建:调用mkfifo(const char *filename, mode_t mode)函数,mode为文件权限(如 0644)。
打开规则:
读打开(O_RDONLY):若管道未被写打开,阻塞直到有写进程打开。
写打开(O_WRONLY):若管道未被读打开,阻塞直到有读进程打开。
非阻塞打开(O_NONBLOCK):读打开立即成功,写打开若无人读则返回 - 1(errno=ENXIO)。
2.2.2 命名管道实战:实现文件拷贝
通过两个非亲缘进程,用命名管道实现 “文件读取→管道传输→文件写入” 的拷贝功能:
写进程(read_file.c):读取源文件,写入 FIFO:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while(0)
#define FIFO_NAME "myfifo"
int main(int argc, char *argv[]) {
if (argc != 2) { fprintf(stderr, "用法:%s 源文件\n", argv[0]); exit(1); }
// 创建FIFO(若已存在则忽略)
mkfifo(FIFO_NAME, 0644);
// 打开源文件和FIFO
int infd = open(argv[1], O_RDONLY);
int outfd = open(FIFO_NAME, O_WRONLY);
if (infd == -1) ERR_EXIT("open 源文件失败");
if (outfd == -1) ERR_EXIT("open FIFO失败");
// 读取源文件,写入FIFO
char buf[1024];
ssize_t n;
while ((n = read(infd, buf, sizeof(buf))) > 0) {
write(outfd, buf, n);
}
close(infd);
close(outfd);
return 0;
}
读进程(write_file.c):从 FIFO 读数据,写入目标文件:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while(0)
#define FIFO_NAME "myfifo"
int main(int argc, char *argv[]) {
if (argc != 2) { fprintf(stderr, "用法:%s 目标文件\n", argv[0]); exit(1); }
// 打开FIFO和目标文件(创建+截断)
int infd = open(FIFO_NAME, O_RDONLY);
int outfd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (infd == -1) ERR_EXIT("open FIFO失败");
if (outfd == -1) ERR_EXIT("open 目标文件失败");
// 从FIFO读数据,写入目标文件
char buf[1024];
ssize_t n;
while ((n = read(infd, buf, sizeof(buf))) > 0) {
write(outfd, buf, n);
}
close(infd);
close(outfd);
unlink(FIFO_NAME); // 删除FIFO文件
return 0;
}
编译运行:
gcc read_file.c -o read_file
gcc write_file.c -o write_file
# 终端1:启动写进程(读源文件→写FIFO)
./read_file test.txt
# 终端2:启动读进程(读FIFO→写目标文件)
./write_file test_copy.txt
三、共享内存:最快的 IPC 机制
共享内存是最快的 IPC 方式—— 它直接将内核中的一块内存映射到多个进程的地址空间,进程读写该内存无需经过内核转发(其他 IPC 如管道需内核拷贝),仅在 “映射 / 解除映射” 时涉及内核操作。
3.1 共享内存的核心原理
- 创建共享内存:通过
shmget()函数创建内核中的共享内存段,获得唯一标识shmid。 - 映射到地址空间:进程通过
shmat()函数将共享内存映射到自己的虚拟地址空间,获得内存指针。 - 进程间通信:多个进程通过映射后的指针直接读写共享内存,实现数据共享。
- 解除映射与删除:进程通过
shmdt()解除映射;最后一个进程通过shmctl()删除共享内存(否则内核会一直保留)。
3.2 共享内存关键函数
| 函数 | 功能 | 核心参数 | 返回值 |
|---|---|---|---|
shmget() | 创建 / 获取共享内存 | key(标识)、size(大小)、shmflg(权限) | 成功返回shmid,失败 - 1 |
shmat() | 映射共享内存到地址空间 | shmid、shmaddr(映射地址,NULL 为自动分配) | 成功返回内存指针,失败 - 1 |
shmdt() | 解除映射 | shmaddr(shmat()返回的指针) | 成功 0,失败 - 1 |
shmctl() | 控制共享内存(删除 / 查询) | shmid、cmd(如 IPC_RMID 删除) | 成功 0,失败 - 1 |
3.3 共享内存实战:进程间数据同步
下面通过 “服务器 - 客户端” 模型,实现共享内存通信(注意:共享内存本身无同步机制,需手动保证数据一致性):
公共头文件(comm.h):定义共享内存参数和函数:
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "." // ftok的路径(需存在)
#define PROJ_ID 0x6666 // 项目ID(自定义)
#define SHM_SIZE 4096 // 共享内存大小(建议为4096整数倍)
// 创建共享内存(服务器用)
int createShm() {
key_t key = ftok(PATHNAME, PROJ_ID);
if (key == -1) { perror("ftok"); return -1; }
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1) { perror("shmget"); return -2; }
return shmid;
}
// 获取共享内存(客户端用)
int getShm() {
key_t key = ftok(PATHNAME, PROJ_ID);
if (key == -1) { perror("ftok"); return -1; }
int shmid = shmget(key, SHM_SIZE, IPC_CREAT);
if (shmid == -1) { perror("shmget"); return -2; }
return shmid;
}
// 删除共享内存
int destroyShm(int shmid) {
if (shmctl(shmid, IPC_RMID, NULL) == -1) { perror("shmctl"); return -1; }
return 0;
}
#endif
服务器端(server.c):创建共享内存,读取客户端写入的数据:
#include "comm.h"
#include <unistd.h>
#include <string.h>
int main() {
// 1. 创建共享内存
int shmid = createShm();
if (shmid < 0) return 1;
// 2. 映射共享内存
char *shmaddr = (char*)shmat(shmid, NULL, 0);
if (shmaddr == (void*)-1) { perror("shmat"); return 1; }
// 3. 读取共享内存数据(循环等待客户端输入)
while (1) {
printf("客户端数据:%s\n", shmaddr);
if (strcmp(shmaddr, "quit") == 0) break; // 退出条件
sleep(1);
}
// 4. 解除映射+删除共享内存
shmdt(shmaddr);
destroyShm(shmid);
return 0;
}
客户端(client.c):获取共享内存,向其中写入数据:
#include "comm.h"
#include <unistd.h>
#include <string.h>
int main() {
// 1. 获取共享内存
int shmid = getShm();
if (shmid < 0) return 1;
// 2. 映射共享内存
char *shmaddr = (char*)shmat(shmid, NULL, 0);
if (shmaddr == (void*)-1) { perror("shmat"); return 1; }
// 3. 向共享内存写入数据
char buf[1024];
while (1) {
printf("客户端输入:");
fgets(buf, sizeof(buf), stdin);
strncpy(shmaddr, buf, SHM_SIZE-1); // 避免越界
if (strcmp(shmaddr, "quit\n") == 0) break; // 退出条件
}
// 4. 解除映射
shmdt(shmaddr);
sleep(1); // 等待服务器删除共享内存
return 0;
}
编译运行:
gcc server.c comm.c -o server
gcc client.c comm.c -o client
# 终端1:启动服务器
./server
# 终端2:启动客户端,输入数据(输入quit退出)
./client
3.4 共享内存的同步问题
共享内存的优势是 “快”,但缺点是 “缺乏同步机制”—— 若多个进程同时读写共享内存,会导致数据错乱(如进程 A 写一半时,进程 B 开始读)。
解决方式:结合管道 / 信号量实现同步。例如:
- 客户端写共享内存前,通过管道通知服务器 “等待”。
- 客户端写完后,通过管道通知服务器 “可以读”。
- 服务器读完后,通过管道通知客户端 “可以继续写”。
四、System V 消息队列与信号量:了解与应用
除了管道和共享内存,System V IPC 还包括消息队列和信号量,它们适用于特定场景,但在现代开发中逐渐被 POSIX 接口替代,此处做简要介绍。
4.1 消息队列:带 “类型” 的消息传递
消息队列是内核中的 “消息链表”,进程可按 “消息类型” 发送 / 接收消息,避免了管道的 “字节流无结构” 问题。
核心特点:
消息有类型,接收者可按类型筛选消息(如优先处理类型为 1 的紧急消息)。
生命周期随内核,需手动删除(否则重启前一直存在)。
效率低于共享内存(需内核拷贝),高于管道(结构化消息)。
关键函数:
msgget():创建 / 获取消息队列。
msgsnd():发送消息(指定类型和数据)。
msgrcv():接收消息(按类型筛选)。
msgctl():控制消息队列(删除 / 查询)。
4.2 信号量:进程同步与互斥的 “计数器”
信号量本质是一个 “内核维护的计数器”,用于实现进程间的同步(顺序控制)和互斥(独占访问)。
4.2.1 核心概念
临界资源:一次仅允许一个进程访问的资源(如共享内存、打印机)。
临界区:访问临界资源的代码段(需保护)。
P 操作:申请资源(计数器 - 1,若计数器 < 0 则阻塞)。
V 操作:释放资源(计数器 + 1,若有进程阻塞则唤醒)。
4.2.2 信号量的应用
信号量常用于保护共享资源,例如:
- 初始化信号量计数器为 1(互斥锁)。
- 进程进入临界区前执行 P 操作(申请资源)。
- 进程离开临界区后执行 V 操作(释放资源)。
关键函数:
semget():创建 / 获取信号量集。
semop():执行 P/V 操作(修改信号量计数器)。
semctl():控制信号量(初始化 / 删除)。
五、内核如何管理 IPC 资源?
Linux 内核通过struct ipc_ids和struct kern_ipc_perm等数据结构统一管理所有 System V IPC 资源(消息队列、共享内存、信号量)。
5.1 核心数据结构
struct ipc_ids:全局 IPC 资源管理器,记录资源的数量、最大 ID、序列号等,通过 “哈希表 + 链表” 组织资源。
struct kern_ipc_perm:每个 IPC 资源的公共属性,包含:
key:资源的唯一标识(用户层通过ftok()生成)。
uid/gid:资源的所有者 / 组 ID。
mode:资源的访问权限(如 0666)。
seq:序列号(避免 ID 重复)。
5.2 IPC 资源的生命周期
创建:进程通过shmget()/msgget()/semget()创建资源,内核分配kern_ipc_perm结构并加入ipc_ids管理。
使用:进程通过shmat()/msgsnd()/semop()等函数访问资源。
删除:进程通过shmctl()/msgctl()/semctl()(cmd=IPC_RMID)删除资源,内核释放相关结构。
注意:若进程未删除 IPC 资源,即使进程退出,资源也会保留在 kernel 中,需通过ipcs命令查看、ipcrm命令手动删除。
六、总结与实践建议
进程间通信是 Linux 开发的核心技术,不同 IPC 机制各有优劣,选择需结合场景:
| IPC 机制 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 匿名管道 | 简单、无需手动删除 | 仅亲缘进程、单向 | 父子进程数据传递(如命令行管道) |
| 命名管道 | 支持非亲缘进程 | 需文件系统、单向 | 无亲缘关系的进程通信(如跨程序数据传输) |
| 共享内存 | 最快、无内核拷贝 | 需同步机制、手动删除 | 高频数据共享(如实时数据采集) |
| 消息队列 | 结构化消息、按类型接收 | 效率低、需手动删除 | 低频、需分类的消息传递 |
| 信号量 | 实现同步互斥 | 仅用于控制,不传递数据 | 保护共享资源(如共享内存的访 |
919

被折叠的 条评论
为什么被折叠?



