引言
在 Linux 系统编程中,进程间通信(IPC,Inter-Process Communication) 是一个核心课题。进程是独立运行的单位,默认情况下彼此隔离。但很多时候,我们需要让进程之间交换数据或同步执行顺序------这就是进程间通信要解决的问题。
Linux 提供了多种 IPC 机制:
-
管道(Pipe):最简单的 IPC 机制,分为有名管道和无名管道
-
信号量(Semaphore):用于进程同步,协调资源访问顺序
-
共享内存(Shared Memory):最高效的数据交换方式
-
消息队列(Message Queue):结构化数据传递
-
套接字(Socket):网络通信
今天,我将重点讲解管道 和信号量这两种 IPC 机制,涵盖它们的工作原理、使用方法和典型应用场景。
第一部分:管道(Pipe)
一、管道的基本概念
管道是 Linux 中最早的进程间通信方式,它的本质是内核内存中的一个环形缓冲区 ,不占用磁盘空间。
管道的特点:
-
数据存储在内存中(读写速度快)
-
半双工通信(同一时刻只能单向传输)
-
自带同步机制(读写自动阻塞/唤醒)
-
无持久性(进程结束,管道消失)
二、管道的分类
| 类型 | 通信范围 | 创建方式 | 文件标识 |
|---|---|---|---|
| 无名管道 | 仅限父子进程间通信 | pipe() 系统调用 |
无文件路径 |
| 有名管道 | 任意两个进程间通信 | mkfifo 命令或 mkfifo() 函数 |
管道文件(类型为 p) |
三、管道的工作原理
管道在内核中通过环形缓冲区实现,包含两个关键指针:
| 指针 | 功能 | 移动规则 |
|---|---|---|
| 头指针 | 指向下一个待写入的位置 | 写入数据后向后移动 |
| 尾指针 | 指向下一个待读取的位置 | 读取数据后向后移动 |

读写规则:
-
写入数据:头指针后移,若缓冲区满则阻塞
-
读取数据:尾指针后移,若缓冲区空则阻塞
-
循环覆盖:指针到达缓冲区末尾后重置到起始位置
四、有名管道(FIFO)
1. 创建有名管道
命令行创建
mkfifo myfifo
查看文件类型(p 表示管道文件)
ls -l myfifo
prw-r--r-- 1 user user 0 Apr 27 10:00 myfifo
cpp
// 程序中创建
#include <sys/types.h>
#include <sys/stat.h>
int main() {
mkfifo("myfifo", 0666);
return 0;
}
2. 有名管道通信示例
写端程序(write.c):
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
int fd = open("myfifo", O_WRONLY);
if (fd == -1) {
perror("open error");
exit(1);
}
char buffer[128];
while (1) {
printf("请输入消息: ");
fgets(buffer, sizeof(buffer), stdin);
buffer[strlen(buffer) - 1] = '\0';
write(fd, buffer, strlen(buffer) + 1);
if (strcmp(buffer, "exit") == 0) break;
}
close(fd);
return 0;
}
读端程序(read.c):
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("myfifo", O_RDONLY);
if (fd == -1) {
perror("open error");
exit(1);
}
char buffer[128];
while (1) {
int n = read(fd, buffer, sizeof(buffer));
if (n == 0) break; // 写端关闭
printf("收到消息: %s\n", buffer);
if (strcmp(buffer, "exit") == 0) break;
}
close(fd);
return 0;
}
五、管道的阻塞特性
| 场景 | 阻塞情况 | 说明 |
|---|---|---|
| open 调用 | 阻塞直到另一端以对应模式打开 | 只读打开时等待写端,只写打开时等待读端 |
| read 调用 | 管道为空且写端未关闭时阻塞 | 写端关闭后 read 立即返回 0 |
| write 调用 | 管道满时阻塞 | 读端读取后解除阻塞 |
cpp
// 演示管道的阻塞特性
// 单独运行写端会阻塞在 open
int main() {
int fd = open("myfifo", O_WRONLY); // 阻塞!等待读端
// 只有读端也打开后,才继续执行
write(fd, "hello", 5);
close(fd);
return 0;
}
六、管道关闭的特殊情况
| 情况 | 结果 | 说明 |
|---|---|---|
| 读端关闭,写端写入 | 进程收到 SIGPIPE 信号(13),默认终止进程 | 内核检测到无效操作 |
| 写端关闭,读端读取 | read 立即返回 0(EOF) | 类似文件末尾 |
| 两端都关闭 | 管道被内核销毁 | 所有文件描述符关闭后释放 |
cpp
// 忽略 SIGPIPE 信号
#include <signal.h>
int main() {
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE
int fd = open("myfifo", O_WRONLY);
// 即使读端关闭,write 也不会导致进程终止
write(fd, "hello", 5);
return 0;
}
七、无名管道(pipe)
1. pipe 函数
cpp
#include <unistd.h>
int pipe(int fd[2]);
// 成功返回 0,失败返回 -1
// fd[0]:读端文件描述符
// fd[1]:写端文件描述符
2. 单进程使用无名管道
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd[2];
pipe(fd);
write(fd[1], "hello", 5);
sleep(3);
char buffer[128];
read(fd[0], buffer, 127);
printf("buffer=%s\n", buffer); // 输出:buffer=hello
close(fd[0]);
close(fd[1]);
return 0;
}
3. 父子进程通过无名管道通信
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
// 子进程:读取数据
close(fd[1]); // 关闭写端
char buffer[128];
read(fd[0], buffer, 127);
printf("子进程收到: %s\n", buffer);
close(fd[0]);
exit(0);
} else {
// 父进程:写入数据
close(fd[0]); // 关闭读端
char buffer[128];
printf("请输入消息: ");
fgets(buffer, sizeof(buffer), stdin);
buffer[strlen(buffer) - 1] = '\0';
write(fd[1], buffer, strlen(buffer) + 1);
close(fd[1]);
wait(NULL);
}
return 0;
}
无名管道的核心特点:
-
仅限父子进程间通信(通过 fork 继承文件描述符)
-
半双工通信,需关闭不需要的描述符
-
自带同步机制,读写自动阻塞/唤醒
八、管道总结
| 特性 | 无名管道 | 有名管道 |
|---|---|---|
| 通信范围 | 父子进程间 | 任意进程间 |
| 创建方式 | pipe() |
mkfifo |
| 文件路径 | 无 | 有 |
| 持久性 | 进程结束消失 | 文件存在,数据随进程 |
| 数据存储 | 内存 | 内存 |
第二部分:信号量(Semaphore)
一、什么是信号量?
信号量是操作系统提供的进程同步机制,用于协调多个进程对共享资源的访问顺序,避免竞态条件。

典型应用场景:
-
打印机等共享设备的互斥访问
-
生产者-消费者问题
-
读者-写者问题
-
多个进程协调执行顺序
二、核心概念
| 概念 | 定义 | 示例 |
|---|---|---|
| 临界资源 | 同一时刻仅允许单个进程访问的资源 | 打印机、试衣间 |
| 临界区 | 访问临界资源的代码段 | 打印数据的代码 |
| P操作 | 申请资源(原子减一) | 进入试衣间前 |
| V操作 | 释放资源(原子加一) | 离开试衣间后 |
| 互斥 | 防止多个进程同时访问临界资源 | 信号量初值设为 1 |
| 同步 | 协调进程执行顺序 | 生产后才能消费 |
三、信号量的工作原理

四、信号量接口
使用信号量需要包含头文件 <sys/sem.h>。
1. semget------创建/获取信号量
cpp
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
// 参数:
// key: 键值,多个进程通过相同的 key 获取同一个信号量
// nsems: 信号量个数
// semflg: 标志位(IPC_CREAT 创建,IPC_EXCL 排他)
// 返回值:成功返回信号量 ID,失败返回 -1
2. semctl------控制信号量(初始化/删除)
cpp
// 自定义联合体(某些系统需要定义)
union semun {
int val; // 用于 SETVAL
struct semid_ds *buf;
unsigned short *array;
};
int semctl(int semid, int semnum, int cmd, ...);
// 参数:
// semid: 信号量 ID
// semnum: 信号量编号(多个信号量时使用)
// cmd: 命令(SETVAL 初始化,IPC_RMID 删除)
3. semop------执行 P/V 操作
cpp
struct sembuf {
unsigned short sem_num; // 信号量编号
short sem_op; // -1(P操作)或 +1(V操作)
short sem_flg; // 标志位(通常为 0)
};
int semop(int semid, struct sembuf *sops, size_t nsops);
五、信号量封装示例
sem.h(头文件):
cpp
#ifndef SEM_H
#define SEM_H
int sem_init(int key);
void sem_p(int semid);
void sem_v(int semid);
void sem_destroy(int semid);
#endif
sem.c(实现文件):
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int sem_init(int key) {
int semid;
// 尝试创建信号量
semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0600);
if (semid == -1) {
// 已存在,直接获取
semid = semget(key, 1, 0600);
} else {
// 创建成功,初始化值为 1
union semun a;
a.val = 1;
semctl(semid, 0, SETVAL, a);
}
return semid;
}
void sem_p(int semid) {
struct sembuf buf = {0, -1, 0};
semop(semid, &buf, 1);
}
void sem_v(int semid) {
struct sembuf buf = {0, 1, 0};
semop(semid, &buf, 1);
}
void sem_destroy(int semid) {
semctl(semid, 0, IPC_RMID);
}
六、使用信号量实现进程互斥
进程A(a.c):
cpp
#include <stdio.h>
#include <unistd.h>
#include "sem.h"
#define KEY 0x1234
int main() {
int semid = sem_init(KEY);
for (int i = 0; i < 5; i++) {
sem_p(semid); // 进入临界区
printf("A"); // 临界区
fflush(stdout);
sleep(1);
printf("A\n"); // 临界区
fflush(stdout);
sem_v(semid); // 退出临界区
sleep(1);
}
// 注意:应由最后一个进程销毁信号量
// 此处简化,实际需要协作
return 0;
}
进程B(b.c):
cpp
#include <stdio.h>
#include <unistd.h>
#include "sem.h"
#define KEY 0x1234
int main() {
int semid = sem_init(KEY);
for (int i = 0; i < 5; i++) {
sem_p(semid); // 进入临界区
printf("B"); // 临界区
fflush(stdout);
sleep(1);
printf("B\n"); // 临界区
fflush(stdout);
sem_v(semid); // 退出临界区
sleep(1);
}
return 0;
}
编译与运行:
# 编译
gcc -c sem.c
gcc a.c sem.o -o a
gcc b.c sem.o -o b# 运行(在两个终端分别执行)
./a
./b**# 输出结果(严格交替):
A
A
B
B
A
A
...**
七、信号量的注意事项
| 要点 | 说明 |
|---|---|
| 初始化时机 | 由第一个进程创建并初始化,其他进程只获取 |
| 销毁时机 | 由最后一个进程销毁(需进程间协调) |
| SEM_UNDO 标志 | 进程异常终止时自动恢复信号量状态,避免死锁 |
| 原子性 | P/V 操作是原子的,不会被中断 |
总结
一、管道核心要点
| 特性 | 说明 |
|---|---|
| 存储位置 | 内存(环形缓冲区) |
| 通信模式 | 半双工 |
| 同步机制 | 读写自动阻塞/唤醒 |
| 无名管道 | 仅父子进程 |
| 有名管道 | 任意进程 |
二、信号量核心要点
| 概念 | 说明 |
|---|---|
| P操作 | 申请资源(值-1,为0时阻塞) |
| V操作 | 释放资源(值+1,唤醒等待进程) |
| 互斥信号量 | 初值为 1,保证互斥访问 |
| 同步信号量 | 初值为 0,协调执行顺序 |
| 临界区 | 被 P/V 包围的代码段 |
三、对比总结
| IPC 机制 | 数据存储 | 通信模式 | 同步机制 | 适用场景 |
|---|---|---|---|---|
| 管道 | 内存 | 单向 | 自动阻塞 | 简单数据流 |
| 信号量 | 计数器 | 信号 | P/V 操作 | 资源同步 |
| 共享内存 | 内存 | 双向 | 需配合信号量 | 大数据量 |
| 消息队列 | 内核 | 双向 | 自动排队 | 结构化消息 |
管道和信号量是 Linux 进程间通信的基础机制。管道解决了数据传输问题,信号量解决了同步问题。理解它们的原理和使用方法,是掌握多进程编程的关键。
学习建议:
-
理解管道的阻塞特性,注意读写端的正确管理
-
区分无名管道和有名管道的使用场景
-
掌握信号量的 P/V 操作逻辑
-
区分互斥(保护临界资源)和同步(协调执行顺序)