学习操作系统和 Linux 系统编程,进程间通信 IPC 是绕不开的核心知识点。它解决了进程之间 "互不干扰、无法直接访问对方内存" 的隔离问题,让多个进程可以交换数据、同步执行、协同工作。
这篇笔记我把最常用的 IPC 机制整理出来,包含概念、接口、代码封装、实战案例。
进程间通信IPC基础概念
IPC(Inter-Process Communication):指同一台计算机上不同进程之间交换数据、同步行为的机制
为什么需要IPC?
Linux 下,每个进程都有独立的虚拟地址空间。进程 A 不能直接读写进程 B 的内存。想要让多个进程配合完成任务,就必须依靠 IPC 打破这种隔离。
常见 IPC 机制
- 管道(Pipe):最基础,单向通信,分为匿名管道(父子进程)和命名管道(任意进程)。
- 消息队列(Message Queue):内核维护的消息链表,支持结构化数据传输。
- 共享内存(Shared Memory):效率最高,多个进程共享同一块物理内存,需信号量同步。
- 信号量(Semaphore):用于进程同步,解决临界资源竞争问题。
- 套接字(Socket):支持本地/跨网络进程通信,是网络编程基础。
一、管道 Pipe
管道是 Linux 中最基础、最古老的进程间通信方式,本质是内核内存中的一块缓冲区 ,遵循 FIFO(先进先出) 原则。
管道创建后会直接在内存上开辟空间,不会占用磁盘空间 ,因此查看管道文件时大小永远为 0
分类
- 有名管道(命名管道 / FIFO):支持任意无关进程之间通信
- 无名管道(匿名管道):仅支持父子进程 / 亲缘进程之间通信
1、有名管道
支持任意两个无关进程之间通信,以文件形式存在于文件系统中(仅作为标识,不占磁盘空间)
常用接口
- 创建:mkfifo
- 打开:open()
- 读数据:read()
- 写数据:write()
- 关闭:close()
使用示例
示例1:单条数据写入与读取
1.终端创建管道文件
bash
mkfifo mypipe
- mk = make(创建)
- fifo = First In First Out(先进先出)

2.写程序:wmain.c
cpp
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
// 以只写形式打开管道
int fd = open("mypipe", O_WRONLY);
if(fd == -1){
printf("open pipe failed\n");
return -1;
}
printf("%d\n", fd);
// 向管道写入内容
write(fd, "hello", 5);
close(fd);// 关闭文件
return 0;
}
3.读程序: rmain.c
cpp
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
// 以只读形式打开管道文件
int fd = open("mypipe", O_RDONLY);
if(fd == -1){
printf("open pipe failed\n");
return -1;
}
printf("fd: %d\n", fd);
char buff[128] = {0};
read(fd, buff, 127);// 将管道文件内容读取到buff中
printf("pipe text: %s\n", buff);
close(fd);// 关闭文件
return 0;
}
注意:结束后要关闭文件!
运行现象
1.只执行写程序/读程序 → 阻塞


2.同时执行,正常运行输出 hello

示例2:循环键盘写入 + 循环读取
写程序:wmain.c
cpp
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
// 打开已创建的管道文件(只写形式打开)
int fd = open("mypipe", O_WRONLY);
if(fd == -1){
printf("open pipe failed\n");
return -1;
}
printf("%d\n", fd);
char buff[128] = {0};
// 写入内容
while(1){
memset(buff, 0, 128);// 将buff内容置为0
printf("please input:");
fgets(buff, 127, stdin);// 从键盘获取内容到buff中
// 输入end停止输入
if(strncmp(buff, "end", 3) == 0){
break;
}
write(fd, buff, strlen(buff));// 将buff内容写入管道
}
close(fd);
return 0;
}
读程序:rmain.c
cpp
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
// 以只读形式打开管道文件
int fd = open("mypipe", O_RDONLY);
if(fd == -1){
printf("open pipe failed\n");
return -1;
}
printf("fd: %d\n", fd);
while(1){
char buff[128] = {0};
if(read(fd, buff, 127) ==0 )// 将管道文件内容读取到buff中
{
break;
}
printf("read text: %s", buff);
}
close(fd);
return 0;
}
运行情况
- 写进程键盘输入一行,读进程立即打印一行
- 输入 end,写进程退出,读进程自动结束
- 全程阻塞式通信,无数据不打印

2、无名管道
无名管道只用于 父子进程通信,没有文件实体,由系统直接创建
接口
cpp
int pipe(int pipefd[2]);
- pipefd[0]:读端
- pipefd[1]:写端
使用示例(父子进程通信)
cpp
#include<stdio.h>
#include<unistd.h>
int main(){
// 定义文件描述符数组:fd[0]-读端;fd[1]-写端
int fd[2];
// 创建无名管道
int res = pipe(fd);
if(res == -1){ // 判断是否创建失败
printf("pipe failed\n");
return -1;
}
// 创建子进程
pid_t pid = fork();
if(pid == -1){
printf("fork failed\n");
return -1;
}
if(pid == 0){ // 子进程-写数据
close(fd[0]);// 关闭读端
write(fd[1], "hello", 5);
close(fd[1]);
}else{ // 父进程-读数据
close(fd[1]);// 关闭写端
char buff[128] = {0};
read(fd[0], buff, 127);
printf("parent read:%s\n", buff);
close(fd[0]);
}
return 0;
}

管道核心特点
- 必须读写端同时open,否则会阻塞
- 管道没有数据,read会阻塞
- 写端关闭,读端 read 返回值为0
- 管道打开的方式只有只读和只写两种,读写方式打开是未定义的
- 管道的数据存放在内存中,管道的大小永远为0
- 管道是一种半双工通信方式:数据单向流动
- 读端关闭,写端会触发异常(SIGPIPE)
二、信号量
1、基本概念
信号量
一个特殊的变量,一般取正数值,代表当前允许访问的资源数目。
作用:控制多个进程对临界资源的访问,保证同一时刻只有一个进程能使用临界资源
P、V操作(原子操作)
-
P 操作(原子减 1)
-
请求资源
-
信号量 > 0:占用一个资源,值 -1
-
信号量 = 0:没有资源,进程阻塞等待
-
-
V 操作(原子加 1)
-
释放资源
-
信号量 +1,唤醒等待的进程
-
信号量分类:
- **二值信号量:**相当于互斥锁,同一时刻只允许一个进程访问,信号量的值只取0,1
- **计数信号量:**允许指定数量的进程同时访问资源,信号量的值大于1
临界资源 :同一时刻,只允许被一个进程或者线程 访问的资源(比如打印机)
临界区:代码中访问临界资源的代码段
2、信号量系统调用接口
头文件
cpp
#include <sys/sem.h>
semget
作用:用于创建一个新的信号量 或者 获取一个已经存在的信号量
cpp
int semget(key_t key, int nsems, int semflg);
参数说明:
- key:所有进程用 同一个 key 就能找到 同一个信号量
- nsems:创建信号量的个数
- semflg:标志位:
- IPC_CREAT:如果不存在就创建,并返回创建的信号量ID - semid;
- IPC_EXCL:如果已经存在就报错,并返回 -1
- 还可以设置信号量权限,例如:0600 → 只有当前用户能使用
semop
**作用:**对信号量进行改变,做P操作或者V操作
cpp
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数说明:
- semid:信号量的id号,也就是semget的返回值;说明对哪个信号量进行操作;
- sops:操作结构体
- nsops:要同时操作几个信号量
cpp
struct sembuf {
unsigned short sem_num; // 信号量下标 0,1,2...
short sem_op; // 操作:-1=P,+1=V
short sem_flg; // 标志,一般写 SEM_UNDO
};
semctl
**作用:**对信号量进行控制:初始化/删除信号量
cpp
int semctl(int semid, int semnum, int cmd,...);
参数说明:
- semid:信号量id
- semnum:要操作第几个信号量(下标)
- cmd:命令
- SETVAL:设置信号量初始值
- GETVAL:获取当前信号量的值
- IPC_RMID:删除整个信号量集
3、具体使用
1.封装

直接使用 System V 信号量的原生接口(semget/semctl/semop)存在以下问题:
- 每次操作需手动构建 sembuf/semun 结构体,代码冗余;
- 易因结构体初始化错误导致逻辑 bug;
- 创建与初始化非原子,可能出现 "进程 A 创建信号量未初始化,进程 B 直接使用" 的崩溃场景;
- 接口命名不直观,不符合开发者的使用习惯。
因此,我们需要对原生接口进行封装,提供初始化、P 操作、V 操作、销毁的一站式接口。
sem.h
cpp
#ifndef __SEM_H__
#define __SEM_H__
#include <stdio.h>
#include <sys/sem.h>
#include <sys/ipc.h>
int sem_init(int key, int nums, int *arr_value);
int sem_p(int semid, int index);
int sem_v(int semid, int index);
void sem_destroy(int semid);
#endif
sem.c
cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/ipc.h>
#include "sem.h"
// 必须自己定义 semun 联合体
union semun{
int val;
};
// 信号量的初始化
int sem_init(int key, int nsems, int* arr_values){
// 尝试创建信号量
int semid = semget((key_t)key, nsems, IPC_CREAT | IPC_EXCL | 0600);
if(semid == -1){ // 信号量已存在:获取信号量内容
semid = semget((key_t)key, nsems, 0600);
}
else{
// 对新创建的信号量进行逐个初始化
for(int i=0;i<nsems;i++){
union semun init_value;
init_value.val = arr_values[i];
int res = semctl(semid, i,SETVAL, init_value);
if(res == -1){ // 创建失败:销毁资源
sem_destroy(semid);
semid = -1;
break;
}
}
}
return semid;
}
重要注意点
上述代码中的信号量的 创建 + 初始化 不是原子操作。可能出现的问题:
- 进程 A 创建了信号量,但还没来得及初始化
- 进程 B 直接获取并使用→ 导致未初始化的信号量被使用,逻辑错乱
解决方案:
利用 struct semid_ds 中的 sem_otime 时间戳实现原子性判断:
- 刚创建未初始化:sem_otime = 0;
- 初始化完成:执行空semop操作,sem_otime刷新为当前时间(非 0)。
空semop操作是原子的,会阻塞所有后续进程直到初始化完成,从根本上杜绝 "未初始化使用" 的问题
cpp
#include <sys/sem.h>
struct semid_ds {
struct ipc_perm sem_perm; // 权限
unsigned short sem_nsems; // 有几个信号量
time_t sem_otime; // 上次 semop 操作时间(P/V 时间)
time_t sem_ctime; // 上次 修改/初始化 时间(semctl)
};
正确且安全代码
cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/ipc.h>
#include "sem.h"
union semun{
int val;
struct semid_ds *buf;
};
// 信号量的初始化
int sem_init(int key, int nsems, int* arr_values){
// 尝试创建信号量
int semid = semget((key_t)key, nsems, IPC_CREAT | IPC_EXCL | 0666);
// 获取信号量状态
struct semid_ds buf;
semctl(semid, 0, IPC_STAT, &buf);
// 判断是否已经初始化
if(semid != -1 && buf.sem_otime == 0){ // 创建成功且没有初始化
for(int i=0;i<nsems;i++){
union semun init_value;
init_value.val = arr_values[i];
int res = semctl(semid, i, SETVAL, init_value);// 进行初始化
if(res == -1){ // 失败:销毁资源
sem_destroy(semid);
semid = -1;
break;
}
}
// 因为semctl不会更新sem_otime
// 所以需要:执行一次空操作,让sem_otime刷新时间
struct sembuf tmp = {0, 0, SEM_UNDO};
semop(semid, &tmp, 1);// 是原子操作,会让其他进程卡住
}else if(semid == -1 && buf.sem_otime != 0){
// 信号量已被创建且已经被初始化:获取信号量内容
semid = semget((key_t)key, nsems, 0600);
}
return semid;
}
// P操作
int sem_p(int semid, int index){
struct sembuf sops;// 创建sembuf结构体
// 初始化结构体元素
sops.sem_num = index;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
// 调用semop函数实现操作
return semop(semid, &sops, 1);
}
// V操作
int sem_v(int semid, int index){
struct sembuf sops;
sops.sem_num = index;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
return semop(semid, &sops, 1);
}
// 删除信号量
void sem_destroy(int semid){
semctl(semid, 0, IPC_RMID);
}
封装信号量的好处
- 简化代码:不用每次都写复杂的 semget/semctl/semop
- 不用手写结构体:P/V 操作内部封装 sembuf,外部一行调用
- 名字直观易懂:sem_p / sem_v 符合操作系统经典命名
- 不易写错:统一封装,减少重复代码导致的 bug
- 可直接用于项目:接口稳定、通用性强
- 便于维护:修改信号量逻辑只需改封装层
2.单个信号量------竞争/互斥
多个进程抢同一个资源,只能一个进程使用,比如打印机。
创建互斥信号量 mutex 来实现竞争的关系
- 初始值 = 1
- 进入前:P 操作(拿锁)
- 离开后:V 操作(放锁)
例子:假设A、B为两台打印机,现要求只有A打印完整的一次才能打印B(打印第一个A,表示打印活动开始,第二个A表示当前打印活动结束,B同理)
maina.c
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include"sem.h"
int main(){
int value[] = {1};// 只创建一个信号量
int semid = sem_init(1234, 1, value);
if(semid == -1)
{
printf("sem init failed\n");
return -1;
}
for(int i = 0; i < 5; i++)
{
sem_p(semid, 0);// P操作
printf("A\n");
sleep(rand() % 3);// 模拟真实业务的操作耗时
printf("A\n");
sem_v(semid, 0);// V操作
sleep(rand() % 3);// 模拟进程的竞争/等待
}
return 0;
}
mainb.c
和maina.c一样,只是打印的是 B
可以在 mainb.c 的return之前调用sem_destroy,对信号量进行删除,这样一来,就可以保证每次运行不受之前没被删除的信号量的影响
为了使两个进程并发执行,写一个脚本实现
cpp
#/bin/bash
./maina &
./mainb &
wait
echo "over"
运行结果
AA 和 BB 永远不会交叉打印,保证了互斥访问
连续的A之间的打印有时间间隔;AA和BB之间的打印也有时间间隔

3.多个信号量------协作/同步
多个进程一起工作,必须按顺序执行
使用合作信号量:
- 初始值 = 0
- A 做完:V 操作(发通知)
- B 等待:P 操作(等通知)
例子:三个进程a、b、c分别输出"A"、"B"、"C",要求输出的结果必须是"ABCABCABC..."循环输出,不能打乱顺序、不能跳着来。
核心逻辑:
- 初始化信号量:使用sem_init函数一次性创建3个信号量,且初始值分别为1、0、0
- P/V操作:
- 进程a:对semA做P操作,然后输出A,再对semB做V操作
- 进程b:对semB做P操作,然后输出B,再对semC做V操作
- 进程c:对semC做P操作,然后输出C,再对semA做V操作
- 循环执行:每个进程重复操作,就可以实现循环输出
初始化信号量
cpp
int values[] = {1, 0, 0};
- sem [0] = 1 → A 可以直接运行
- sem [1] = 0 → B 必须等 A
- sem [2] = 0 → C 必须等 B
maina.c
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include"sem.h"
int main(){
int values[] = {1, 0, 0};
int semid = sem_init(1234, 3, values);
if(semid == -1)
{
printf("sem init failed\n");
return -1;
}
int count = 3;
while(count--){
sem_p(semid, 0);
printf("A\n");
sleep(rand() % 3);
sem_v(semid, 1);
sleep(rand() % 3);
}
return 0;
}
mainb.c
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include"sem.h"
int main(){
int values[] = {1, 0, 0};
int semid = sem_init(1234, 3, values);
if(semid == -1)
{
printf("sem init failed\n");
return -1;
}
int count = 3;
while(count--){
sem_p(semid, 1);
printf("B\n");
sleep(rand() % 3);
sem_v(semid, 2);
sleep(rand() % 3);
}
return 0;
}
mainc.c
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include"sem.h"
int main(){
int values[] = {1, 0, 0};
int semid = sem_init(1234, 3, values);
if(semid == -1)
{
printf("sem init failed\n");
return -1;
}
int count = 3;
while(count--){
sem_p(semid, 2);
printf("C\n");
sleep(rand() % 3);
sem_v(semid, 0);
sleep(rand() % 3);
}
return 0;
}
cpp
#/bin/bash
./maina &
./mainb &
./mainc &
wait
echo "over"
执行结果

三、共享内存
1、核心概念
共享内存是 效率最高的一种进程间通信(IPC)方式
原理:在物理内存 上申请一块公共的存储区域,允许多个进程将这块内存映射到自己的虚拟地址空间
这样一来:
- 所有进程都能直接读写同一块内存区域
- 任何进程写入的数据,其他进程能立刻看到
- 不需要在内核和用户空间之间复制数据,速度极快
注意点:共享内存本身不提供任何同步机制 。多个进程同时读写会造成数据混乱,因此必须搭配信号量来实现同步与互斥。
2、接口介绍
头文件
cpp
#include <sys/shm.h>
shmget
创建一块新的共享内存,或者获取一个已经存在的共享内存
cpp
int shmget(key_t key, size_t size, int shmflg);
- key:共享内存的唯一标识。不同进程只要使用相同的 key,就能找到并使用同一块共享内存(这里的值和信号量的值一样也没有关系,因为类型不一样)
- size:共享内存的大小。创建时必须指定,单位为字节
- shmflg:标志位,常用组合
- IPC_CREAT:如果共享内存不存在,则创建
- IPC_EXCL:如果要创建的共享内存已存在,则报错返回
- 0664:权限设置(通常与上面标志位进行按位或操作)
- 返回值:成功返回共享内存的 ID, 失败返回-1
shmat
将获取到的共享内存,映射到当前进程的虚拟地址空间中。映射成功后,进程就可以像操作普通内存一样读写这块区域
cpp
void* shmat(int shmid, const void *shmaddr, int shmflg);
- shmid:由 shmget 获取到的共享内存 ID
- shmaddr:指定映射到进程空间的具体地址,通常填 NULL,交由操作系统自动分配
- shmflg: 访问模式
- 0:默认值,表示可读可写
- SHM_RDONLY:只读模式,其他的为读写
- 返回值:成功返回返回共享内存的首地址,失败返回 (void*)-1
shmdt
将共享内存从当前进程的地址空间中断开映射
cpp
int shmdt(const void *shmaddr);
- shmaddr:shmat 返回的映射首地址
- 返回值:成功返回 0, 失败返回-1
注意:调用这个函数只是当前进程 "不再使用" 这块内存了,但物理内存依然存在,其他连接了该内存的进程依然可以正常使用
shmctl
控制共享内存(常用于删除共享内存)
cpp
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid:共享内存 ID
- cmd: 要执行的命令
- IPC_RMID:删除共享内存
- buf:指向存放共享内存属性信息的结构体,删除操作时传 NULL 即可
- 返回值:成功返回 0,失败返回-1
注意 :"标记删除" 机制 ------ 如果还有进程正连接着这块共享内存,内核不会立即删除,而是先标记上 "待删除" 状态,等待最后一个进程断开映射(shmdt)后,才会真正释放物理内存
3、使用示例
示例1:单项读写
功能说明:
- shma.c 向共享内存写入字符串 "hello"
- shmb.c 从共享内存读取并打印
- 共享内存内容会一直保留在内核中,直到被删除
shma.c
cpp
#include<stdio.h>
#include<string.h>
#include<sys/shm.h>
int main()
{
// 创建一块共享内存
int shmid = shmget(1234, 128, IPC_CREAT | 0600);
if(shmid == -1)// 创建失败
{
printf("shm get failed\n");
return -1;
}
// 将获取到的共享内存映射到虚拟地址空间
char* shm_addr = shmat(shmid, NULL, 0);
if(shm_addr == (void *)-1)// 映射失败
{
printf("shmat failed\n");
return -1;
}
strcpy(shm_addr, "hello");
// 断开映射
if(shmdt(shm_addr) == -1)
{
printf("shmdt failed\n");
return -1;
}
return 0;
}
shmb.c
cpp
#include<stdio.h>
#include<string.h>
#include<sys/shm.h>
int main()
{
// 创建一块共享内存
int shmid = shmget(1234, 128, IPC_CREAT | 0600);
if(shmid == -1)// 创建失败
{
printf("shm get failed\n");
return -1;
}
// 将获取到的共享内存映射到虚拟地址空间
char* shm_addr = shmat(shmid, NULL, 0);
if(shm_addr == (void *)-1)// 映射失败
{
printf("shmat failed\n");
return -1;
}
// 对共享内存里的内容进行操作
printf("B:%s\n",shm_addr);
// 断开映射
if(shmdt(shm_addr) == -1)
{
printf("shmdt failed\n");
return -1;
}
return 0;
}

运行现象
- 先运行 ./shmb → 共享内存为空,什么都输出不了
- 再运行 ./shma → 写入 "hello"
- 再运行 ./shmb → 成功输出 hello
共享内存一旦写入,数据会一直保存在内核中,多个进程可以反复访问
示例2:循环交互通信
功能要求:
- 进程 A 循环从键盘输入,写入共享内存
- 进程 B 循环从共享内存读取并打印
- 必须满足:A 写一次 → B 读一次
- A 不输入,B 就不输出
如果我们只加循环、不加同步,会出现严重问题:
- A 还没输入,B 就疯狂读取旧数据
- 数据重复打印、乱序打印
- 无法做到 "写一次、读一次"

根本原因 :**共享内存本身没有任何同步机制。**它只管提供内存,不管谁在写、谁在读、什么时候读
解决方案
使用前面学过的 信号量 来做同步:
-
创建两个信号量:
- sem_write:初始值 1(A 可以先写)
- sem_read:初始值 0(B 必须等待)
-
流程:
- A:P (写) → 写入数据 → V (读)
- B:P (读) → 读取数据 → V (写)
这样就能严格保证:写一次、读一次,不会乱、不会重复
shma.c
cpp
#include<stdio.h>
#include<string.h>
#include<sys/shm.h>
#include "sem.h"
int main()
{
// 创建一块共享内存
int shmid = shmget(1234, 128, IPC_CREAT | 0600);
if(shmid == -1)// 创建失败
{
printf("shm get failed\n");
return -1;
}
// 将获取到的共享内存映射到虚拟地址空间
char* shm_addr = shmat(shmid, NULL, 0);
if(shm_addr == (void *)-1)// 映射失败
{
printf("shmat failed\n");
return -1;
}
// 初始化信号量
int values[] = {1, 0};
int semid = sem_init(123, 2, values);
if(semid == -1)
{
printf("sem init failed\n");
// 必须要释放之前申请的资源
shmctl(shmid, IPC_RMID, NULL);
return -1;
}
// 循环进行输入
while(1)
{
sem_p(semid, 0);
printf("please input:");
scanf("%s", shm_addr);
sem_v(semid, 1);
// 输入 end 表示结束
if(strncmp(shm_addr, "end", 3) == 0)
{
break;
}
}
// 断开映射
if(shmdt(shm_addr) == -1)
{
printf("shmdt failed\n");
return -1;
}
return 0;
}
shamb.c
cpp
#include<stdio.h>
#include<string.h>
#include<sys/shm.h>
#include "sem.h"
int main()
{
// 创建一块共享内存
int shmid = shmget(1234, 128, IPC_CREAT | 0600);
if(shmid == -1)// 创建失败
{
printf("shm get failed\n");
return -1;
}
// 将获取到的共享内存映射到虚拟地址空间
char* shm_addr = shmat(shmid, NULL, 0);
if(shm_addr == (void *)-1)// 映射失败
{
printf("shmat failed\n");
return -1;
}
// 获取信号量
int values[] = {1, 0};
int semid = sem_init(123, 2, values);
if(semid == -1)
{
printf("sem init failed");
shmctl(shmid, IPC_RMID, NULL);
return -1;
}
// 循环读取
while(1)
{ sem_p(semid, 1);
printf("B:%s\n", shm_addr);
sem_v(semid, 0);
// 读取到 end 结束
if(strncmp(shm_addr, "end", 3) == 0)
{
break;
}
}
// 断开映射
if(shmdt(shm_addr) == -1)
{
printf("shmdt failed\n");
return -1;
}
// 删除共享内存
if(shmctl(shmid, IPC_RMID, NULL) == -1)
{
printf("shmctl failed\n");
return -1;
}
// 删除信号量
sem_destroy(semid);
return 0;
}
运行现象
- 先运行 shmb: 进程 B 会直接阻塞,等待 A 写入

- 运行 shma:A 等待输入,输入一次,B 就打印一次。

3.**输入 end:**A 退出,B 打印 end 后也自动退出。
4.自动清理资源:因为在shmb.c中最后进行了信号量和共享内层的删除,所以无需手动删除

四、消息队列
消息队列是消息的链接表,存储在内核中,由消息队列标识符唯一标识
它允许进程以 消息类型+消息内容 的形式进行数据交换,支持指定类型读取、按顺序接收、阻塞/非阻塞收发
1、接口介绍
头文件
cpp
#include <sys/msg.h>
msgget
创建一个新的消息队列,或获取一个已存在的消息队列
cpp
int msgget(key_t key, int msqflg);
- key:息队列的唯一键值,不同进程通过相同 key 访问同一队列
- msqflg:标志位
- IPC_CREAT:不存在则创建
- 可搭配权限,如 0600
- 返回值:成功返回消息队列 ID(msqid),失败返回-1
msgsnd
像消息队列发送一条消息
cpp
int msgsnd(int msqid, const void *msqp, size_t msqsz, int msqflg);
- msqid:消息队列 ID,因为系统中可能有多个消息队列,这id指明往哪个消息队列中添加数据
- msqp:往消息队列中添加的结构体
- msqsz:消息数据部分的长度(不包含消息类型)
- msqflg:标志位
- 0:队列满时阻塞
- IPC_NOWAIT:队列满时不阻塞,直接报错返回
- 返回值:成功返回 0, 失败返回-1
消息结构体(必须自己进行定义)
cpp
struct msgbuf {
long mtype; // 消息类型, 必须大于0(或者说>=1),长整型,比如图中的1,2
char mtext[1]; // 消息数据 ,用户自己定义,可以是任何类型;这里存放消息数据
};
msgrcv
从消息队列接收一条消息
cpp
ssize_t msgrcv(int msqid, void *msgp, size_t msqsz, long msqtyp, int msqflg);
- msqid:消息队列 ID
- msgp:接收消息的结构体
- msqsz:接收缓冲区大小
- msqtyp:要接收的消息类型
- 0:不区分类型,按队列顺序接收
- > 0:只接收该类型的消息
- msqflg:标志位
- 0:队列为空时阻塞等待
- IPC_NOWAIT:队列为空时不阻塞,直接返回
- 返回值:成功返回接收到的数据传递,失败返回 -1
msgctl
控制消息队列,最常用为删除消息队列
cpp
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- msqid:消息队列 ID
- cmd:控制命令
- IPC_RMID:删除消息队列
- buf:队列属性结构体,删除时传 NULL
- 返回值:成功返回 0,失败返回 -1
注意
消息队列创建后会一直存在内核中,直到:
- 使用 msgctl(IPC_RMID) 删除
- 系统重启
- 命令手动删除:ipcrm -q 队列ID
2、使用示例
进程 A 发送一条消息,进程 B 读取消息
基础版:单条消息收发
功能
- 进程 A(msga.c)向队列发送 某类型、内容 "hello" 的消息
- 进程 B(msgb.c)从队列接收 相同类型 的消息并打印
- 接收完成后由 B 删除队列
msga.c(发送端)
cpp
#include<stdio.h>
#include<string.h>
#include<sys/msg.h>
// 自定义结构体
struct msgbuf{
long mtype;// 消息类型(>0)
char mtext[128];// 消息数据缓存区
};
int main()
{
// /创建/获取消息队列
int msqid = msgget((key_t)123, IPC_CREAT | 0600);
if(msqid == -1)
{
printf("msg get failed\n");
return -1;
}
// 定义消息结构体变量
struct msgbuf msqp;
msqp.mtype = 1;// 设置消息类型
strcpy(msqp.mtext, "hello");// 向消息数据域拷贝内容"hello"
// 发送消息到消息队列
int res = msgsnd(msqid, &msqp, 128, 0);// 阻塞模式
if(res == -1)
{
printf("msg send failed\n");
return -1;
}
return 0;
}
msgb.c(接收端)
cpp
#include<stdio.h>
#include<string.h>
#include<sys/msg.h>
struct msgbuf{
long mtype;
char mtext[128];
};
int main()
{
int msqid = msgget((key_t)123, IPC_CREAT | 0600);
if(msqid == -1)
{
printf("msg get failed\n");
return -1;
}
struct msgbuf msqp;
// 从消息队列接收消息(只接受类型为1的消息)
msgrcv(msqid, &msqp, 128, 1, 0);
printf("read message:%s\n",msqp.mtext);
// 删除消息队列
if(msgctl(msqid, IPC_RMID, NULL) == -1)
{
printf("msg rm failed\n");
return -1;
}
return 0;
}
运行现象
1.A发送1类型,B接收1类型 → 正常输出 hello

- A 发送类型 2,B 仍接收类型 1 → B 会一直阻塞,无法接收

进阶版(命令行传参 + 多消息收发)
功能
- 通过命令行参数指定消息类型和内容
- 支持发送任意类型、任意内容
- 可连续收发多条消息
msg.h(统一结构体定义)
cpp
#ifndef MSG_H
#define MSG_H
#define TEXT_LENGTH 128
typedef struct Msg{
long mtype;
char mtext[TEXT_LENGTH];
}Mess;
#endif
msga.c(带参数发送端)
cpp
#include<stdio.h>
#include<string.h>
#include<sys/msg.h>
#include"msg.h"
int main(int argc, char* argv[])
{
// 创建/获取消息队列
int msqid = msgget((key_t)123, IPC_CREAT | 0600);
if(msqid == -1){
printf("msg get failed\n");
return -1;
}
// 定义消息结构体变量
Mess msqp;
memset(&msqp, 0, sizeof(msqp)); // 清空整个结构体
// 如果有输入数据类型
if(argc >= 2){
// 将消息类型设置为对应类型
sscanf(argv[1], "%ld", &msqp.mtype);
}
else{
// 反之,默认设为1
msqp.mtype = 1;
}
// 有输入消息内容
if(argc >= 3){
strcpy(msqp.mtext, argv[2]);
}
else{
// 默认为hello
strcpy(msqp.mtext, "hello");
}
// 发送消息到消息队列
int res = msgsnd(msqid, &msqp, strlen(msqp.mtext), 0); // 阻塞模式
if(res == -1){
printf("msg send failed\n");
return -1;
}
return 0;
}
msgb.c(接收端)
cpp
#include<stdio.h>
#include<string.h>
#include<sys/msg.h>
#include"msg.h"
int main()
{
int msqid = msgget((key_t)123, IPC_CREAT | 0600);
if(msqid == -1)
{
printf("msg get failed\n");
return -1;
}
Mess msqp;
memset(&msqp, 0, sizeof(msqp));
// 从消息队列接收消息(只接受类型为1的消息)
if(msgrcv(msqid, &msqp, TEXT_LENGTH, 1, 0) == -1)
{
printf("message receive failed\n");
return -1;
}
printf("type:%ld, text:%s\n", msqp.mtype, msqp.mtext);
// 删除消息队列
if(msgctl(msqid, IPC_RMID, NULL) == -1)
{
printf("msg rm failed\n");
return -1;
}
return 0;
}
运行结果


重要现象
- 发送多条消息后,接收端会取第一个匹配类型的消息
- 如果 msgb.c 中保留删除队列代码 → 只能收一次,队列被销毁
- 如果 删除 msgb.c 中的队列销毁代码 → 队列保留,可多次收发
注意事项
不能直接判断 argv[1] / argv[2] 是否为 NULL 来确认参数是否存在,直接访问非法下标会造成内存越界。
Linux 中进程参数后紧跟环境变量,非法访问会读到环境变量值,导致程序逻辑错误。
IPC机制对比表
| IPC 类型 | 速度 | 进程关系 | 数据形式 | 同步机制 | 优点 | 缺点 |
|---|---|---|---|---|---|---|
| 管道 | 中 | 父子 / 任意 | 字节流 | 自带阻塞 | 简单、无需同步 | 单向、效率一般 |
| 信号量 | - | 任意 | 同步控制 | 原子 PV | 解决竞态 | 不能传输数据 |
| 共享内存 | 最快 | 任意 | 直接内存 | 需信号量 | 速度无敌 | 需手动同步 |
| 消息队列 | 中 | 任意 | 带类型消息 | 自带阻塞 | 支持类型、易用 | 效率不如共享内存 |