Linux 进程间通信 IPC 总结:管道 + 信号量 + 共享内存 + 消息队列(附代码)

学习操作系统和 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、有名管道

支持任意两个无关进程之间通信,以文件形式存在于文件系统中(仅作为标识,不占磁盘空间)

常用接口
  1. 创建:mkfifo
  2. 打开:open()
  3. 读数据:read()
  4. 写数据:write()
  5. 关闭: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;
}

管道核心特点

  1. 必须读写端同时open,否则会阻塞
  2. 管道没有数据,read会阻塞
  3. 写端关闭,读端 read 返回值为0
  4. 管道打开的方式只有只读和只写两种,读写方式打开是未定义的
  5. 管道的数据存放在内存中,管道的大小永远为0
  6. 管道是一种半双工通信方式:数据单向流动
  7. 读端关闭,写端会触发异常(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)存在以下问题:

  1. 每次操作需手动构建 sembuf/semun 结构体,代码冗余;
  2. 易因结构体初始化错误导致逻辑 bug;
  3. 创建与初始化非原子,可能出现 "进程 A 创建信号量未初始化,进程 B 直接使用" 的崩溃场景;
  4. 接口命名不直观,不符合开发者的使用习惯。

因此,我们需要对原生接口进行封装,提供初始化、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);
}

封装信号量的好处

  1. 简化代码:不用每次都写复杂的 semget/semctl/semop
  2. 不用手写结构体:P/V 操作内部封装 sembuf,外部一行调用
  3. 名字直观易懂:sem_p / sem_v 符合操作系统经典命名
  4. 不易写错:统一封装,减少重复代码导致的 bug
  5. 可直接用于项目:接口稳定、通用性强
  6. 便于维护:修改信号量逻辑只需改封装层

2.单个信号量------竞争/互斥

多个进程抢同一个资源,只能一个进程使用,比如打印机。

创建互斥信号量 mutex 来实现竞争的关系

  1. 初始值 = 1
  2. 进入前:P 操作(拿锁)
  3. 离开后: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,对信号量进行删除,这样一来,就可以保证每次运行不受之前没被删除的信号量的影响

cmd.sh

为了使两个进程并发执行,写一个脚本实现

cpp 复制代码
#/bin/bash

./maina &
./mainb &

wait

echo "over"

运行结果

AA 和 BB 永远不会交叉打印,保证了互斥访问

连续的A之间的打印有时间间隔;AA和BB之间的打印也有时间间隔


3.多个信号量------协作/同步

多个进程一起工作,必须按顺序执行

使用合作信号量:

  1. 初始值 = 0
  2. A 做完:V 操作(发通知)
  3. B 等待:P 操作(等通知)

例子:三个进程a、b、c分别输出"A"、"B"、"C",要求输出的结果必须是"ABCABCABC..."循环输出,不能打乱顺序、不能跳着来。

核心逻辑

  1. 初始化信号量:使用sem_init函数一次性创建3个信号量,且初始值分别为1、0、0
  2. P/V操作:
    1. 进程a:对semA做P操作,然后输出A,再对semB做V操作
    2. 进程b:对semB做P操作,然后输出B,再对semC做V操作
    3. 进程c:对semC做P操作,然后输出C,再对semA做V操作
  3. 循环执行:每个进程重复操作,就可以实现循环输出

初始化信号量

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;
}

cmd.sh

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 就疯狂读取旧数据
  • 数据重复打印、乱序打印
  • 无法做到 "写一次、读一次"

根本原因 :**共享内存本身没有任何同步机制。**它只管提供内存,不管谁在写、谁在读、什么时候读

解决方案

使用前面学过的 信号量 来做同步:

  1. 创建两个信号量:

    • sem_write:初始值 1(A 可以先写)
    • sem_read:初始值 0(B 必须等待)
  2. 流程:

    • 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;
}
运行现象
  1. 先运行 shmb: 进程 B 会直接阻塞,等待 A 写入
  1. 运行 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

  1. 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 解决竞态 不能传输数据
共享内存 最快 任意 直接内存 需信号量 速度无敌 需手动同步
消息队列 任意 带类型消息 自带阻塞 支持类型、易用 效率不如共享内存
相关推荐
Ujimatsu6 分钟前
虚拟机安装Debian 13.x及其常用软件(2026.4)
linux·运维·ubuntu
千百元7 分钟前
zookeeper启不来了
linux·zookeeper·debian
AnalogElectronic2 小时前
linux 测试网络和端口是否连通的命令详解
linux·网络·php
智者知已应修善业2 小时前
【51单片机按键调节占空比3位数码管显示】2023-8-24
c++·经验分享·笔记·算法·51单片机
JasmineX-13 小时前
数据结构(笔记)——双向链表
c语言·数据结构·笔记·链表
Edward111111113 小时前
4月28日防火墙问题
linux·运维·服务器
.5483 小时前
## Sorting(排序算法)
python·算法·排序算法
wuweijianlove3 小时前
算法的平均复杂度建模与性能回归分析的技术7
算法·数据挖掘·回归
子琦啊3 小时前
【算法复习】字符串 | 两个底层直觉,吃透高频题
linux·运维·算法
AOwhisky4 小时前
Kubernetes 学习笔记:集群管理、命名空间与 Pod 基础
linux·运维·笔记·学习·云原生·kubernetes