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 解决竞态 不能传输数据
共享内存 最快 任意 直接内存 需信号量 速度无敌 需手动同步
消息队列 任意 带类型消息 自带阻塞 支持类型、易用 效率不如共享内存
相关推荐
小年糕是糕手2 小时前
【35天从0开始备战蓝桥杯 -- 补充包】
开发语言·前端·数据结构·数据库·c++·算法·蓝桥杯
Tisfy2 小时前
LeetCode 1878.矩阵中最大的三个菱形和:斜向前缀和
算法·leetcode·矩阵
budingxiaomoli2 小时前
优选算法-队列+宽搜
算法
lucia_zl2 小时前
linux收集进程性能数据
linux·运维·chrome
Byte不洛2 小时前
手写一个C++ TCP服务器实现自定义协议(顺便解决粘包问题)
linux·c++·操作系统·网络编程·tcp
道亦无名4 小时前
Linux下是STM32的编译修改配置文件tensorflow
linux·运维
张李浩10 小时前
Leetcode 054螺旋矩阵 采用方向数组解决
算法·leetcode·矩阵
big_rabbit050210 小时前
[算法][力扣101]对称二叉树
数据结构·算法·leetcode
炸膛坦客10 小时前
Linux - Ubuntu - PC端:(三)切换中英文,Fcitx5
linux·ubuntu