目录
[一 命名管道](#一 命名管道)
[1 概念](#1 概念)
[2 创建一个命名管道](#2 创建一个命名管道)
[3 命名管道的特点](#3 命名管道的特点)
[4 命名管道和匿名管道的区别](#4 命名管道和匿名管道的区别)
[5 命名管道的打开规则](#5 命名管道的打开规则)
[二 共享内存](#二 共享内存)
[1 原理](#1 原理)
[2 代码](#2 代码)
[3 内核如何管理IPC资源(包括:shm,asgq,sem)](#3 内核如何管理IPC资源(包括:shm,asgq,sem))
[4 共享内存完整代码](#4 共享内存完整代码)
[三 理解消息队列基本原理](#三 理解消息队列基本原理)
一 命名管道
命名管道(Named Pipe / FIFO)是一种常用的跨进程通信(IPC)机制,核心是文件系统中的特殊文件,打破了匿名管道仅能用于亲缘进程的限制,让任意进程通过路径名即可实现通信
1 概念
-
全称:Named Pipe,又称 FIFO(First In First Out,先进先出)。
-
本质:文件系统中可见的特殊文件(ls -l 显示类型为 p),有路径、权限但不占用磁盘数据块,数据仅存于内核缓冲区,性能接近匿名管道。
-
核心优势:无亲缘关系的进程(跨会话、无关程序),只要知道管道路径,就能打开并通信。
2 创建一个命名管道
可以从命令行上创建
bash
$ mkfifo filename
也可以从程序里创建,相关函数有:
bash
int mkfifo(const char *filename,mode_t mode);
3 命名管道的特点
进程间通信的本质是:让不同的进程,看到同一份资源
命名管道有自己的inode,但是不会把自己的内存数据刷新到文件内部;命名管道在open的时候,就已经进行了让读写同步
通信方式:默认半双工(同一时间只能单向传输),双向通信需创建两个管道。
阻塞机制:默认阻塞(读空、写满时进程挂起等待),可通过 O_NONBLOCK 设为非阻塞。
4 命名管道和匿名管道的区别
| 特性 | 匿名管道(pipe) | 命名管道(FIFO) |
|---|---|---|
| 创建方式 | pipe() 系统调用 | mkfifo 命令 / mkfifo() 函数 |
| 可见性 | 无文件名,仅内核可见 | 文件系统可见,有路径名 |
| 适用进程 | 仅亲缘关系(父子/兄弟) | 任意进程(无关/跨会话) |
| 生命周期 | 随进程退出销毁 | 文件持久存在,需手动删除 |
5 命名管道的打开规则
如果当前打开操作是为读而打开 FIFO 时:
O_NONBLOCK 关闭(disable):阻塞直到有相应进程为写而打开该 FIFO
O_NONBLOCK 开启(enable):立刻返回成功
如果当前打开操作是为写而打开 FIFO 时:
O_NONBLOCK 关闭(disable):阻塞直到有相应进程为读而打开该 FIFO
O_NONBLOCK 开启(enable):立刻返回失败,错误码为 ENXIO
总结
核心区别:读打开非阻塞时直接成功,写打开非阻塞时直接失败;阻塞模式下,读写打开都会等待对应端进程打开。
但是我们使用管道的通信方式效率低,应用场景少,就需要有新的进程间通信
我们要规定标准**:System V (只能做一台主机上的进程间通信)**
我们基于System V这个标准,产生了三种进程间通信:共享内存,消息队列,信号量
我们下面来学习共享内存
二 共享内存
1 原理

进程的虚拟地址空间由mm_struct结构体统一管理。共享内存的核心实现机制是物理内存映射,具体流程如下:
内核在物理内存中划分出一段独立的物理内存空间,记录其起始地址和长度;
分别将该物理内存段映射到进程 A、进程 B 的虚拟地址空间中(为 A 和 B 分配各自的虚拟起始地址);
最终实现同一块物理内存映射到不同进程的虚拟地址空间,让进程 A 和进程 B "看到同一份内存数据",完成跨进程的数据共享。
这种映射方式打破了进程地址空间的独立性
在这个过程中,构建映射关系,这一步叫做attach-->挂接 ;不想玩了怎么办?detech--->去关联
两个问题:
(1):这整个过程是谁做的?操作系统,并提供系统调用
(2):共享区用户能直接访问吗?
共享内存被映射到进程的用户地址空间中,因此用户进程可以直接通过指针访问共享内存的内容,无需额外调用系统调用读写数据。创建和删除共享内存(shm)需要系统调用,使用shm不需要系统调用,类似于malloc(),创建和使用管道都用到系统调用
理解:
共享内存的存在是可以同时存在多份的,对于不同的共享内存操作系统是需要进行管理的-->先描述,再组织;内核中一定存在描述共享内存的描述体-->struct shmxx
例如:
cpp
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment
(bytes) */
__kernel_time_t shm_atime; /* last attach time
*/
__kernel_time_t shm_dtime; /* last detach time
*/
__kernel_time_t shm_ctime; /* last change time
*/
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current
attaches */
unsigned short shm_unused; /* compatibility */
void shm_unused2; / ditto - used by
DIPC */
void shm_unused3; / unused */
};
2 代码
.hpp文件后缀:可以在其他文件开头包括 #include"xxx.hpp",就能直接使用.hpp文件内创建的类等
系统调用:shmget:作用是创建或获取一个共享内存段
cpp
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key_t key:
作用:作为共享内存段在系统中的唯一标识符,用于让不同进程找到同一块共享内存。
特点:由程序员指定,通常通过ftok() 生成 ,确保多个进程能获取同一个 key。
补充:key 是内核层面的标识,进程无法直接使用它访问内存,只能用它获取 shmid
size_t size
作用:指定要创建的共享内存段的大小。
注意:实际分配的大小通常会向上取整为 4KB(一页)的整数倍,以适配内存管理机制
int shmflg
控制共享内存的创建行为,核心标志位
| 标志位组合 | 含义 | 用途 | |
|---|---|---|---|
IPC_CREAT |
若共享内存不存在则创建,若存在则直接获取 | 通用获取 / 创建 | |
| `IPC_CREAT | IPC_EXCL` | 若共享内存不存在则创建,若已存在则报错 | 确保创建全新的共享内存 |
0666 |
指定共享内存的读写权限(用户 / 组 / 其他均可读写) | 配合创建标志使用 |
IPC_EXCL 单独使用无意义,必须和 IPC_CREAT 一起使用,才能实现 "不存在则创建,存在则失败" 的互斥效果
返回值
成功:返回一个有效的共享内存标识符 shmid (后续操作共享内存都用这个值)。
失败:返回 -1,并设置 errno 来指示错误原因
A和B要访问同一个键值key创建的共享内存,A得到了key,但是B怎么知道key是多少?怎么知道内存中那么多共享内存哪一个是自己的?
怎么样创建key,同时能让A,B通信?
程序员自己设定key,在不让A,B通信的情况下,采用约定的方式,一个用来创建时设置,一个用来获取;所以shmget要自己设置key

ftok:作用是生成一个唯一的 key_t 类型键值
cpp
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
const char *pathname:指定一个已存在的文件路径名
int proj_id:自定义的 "项目 ID",通常取 1~255 之间的整数
为什么要用 ftok?
共享内存、消息队列等 IPC 资源需要一个全局唯一的key,如果手动指定容易冲突,ftok 可以基于文件路径和自定义 ID 稳定生成不冲突的key,让不同进程通过相同的pathname和proj_id拿到同一个key,从而访问同一个 IPC 资源。
3 内核如何管理IPC资源(包括:shm,asgq,sem)
共享内存的生命周期随内核,不随进程!!
程退出时,只会从自己的地址空间中 "卸载" 共享内存(shmdt),不会删除内核中的共享内存段 。
只要内核没有主动删除,共享内存就会一直存在,直到系统重启或被显式释放。
你可以用 ipcs -m 命令查看当前系统中所有未被释放的共享内存段,即使创建它的进程早已退出
查看共享内存
cpp
ipcs -m
手动释放共享内存
cpp
ipcrm -m <shmid>
系统调用:shmctl 共享内存的控制接口,支持删除、获取属性、设置属性等操作
cpp
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

shmdt函数:将共享内存段与当前进程脱离
cpp
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
共享内存是进程间通信,速度最快的
共享内存没有自带保护机制,任何挂接进程可以随时访问共享内存-->如果想保护,我们就要学到一个知识:信号量
4 共享内存完整代码
cpp
#ifndef __SHM_HPP
#define __SHM_HPP
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/shm.h>
#include <string>
// 共享内存相关配置常量
const std::string proj_name = "/home"; // ftok使用的路径
const int proj_id = 0x6666; // ftok使用的项目ID
const int g_size = 4096; // 默认共享内存大小(4KB)
// 辅助函数:将数字转为十六进制字符串输出
static std::string ToHex(long long data)
{
char hex[64];
snprintf(hex, sizeof(hex), "0x%llx", data);
return hex;
}
// 共享内存封装类
class Shm
{
public:
Shm(int size = g_size)
: _shmid(-1), _size(size), _key(0), _start(nullptr)
{
}
~Shm()
{}
private:
// 生成唯一的key值(用于标识共享内存)
key_t GetKey()
{
_key = ftok(proj_name.c_str(), proj_id);
if (_key < 0)
{
perror("ftok error");
}
return _key;
}
// 创建/获取共享内存核心函数
bool CreateCoreHelper(int flags)
{
// 1. 获取唯一key
key_t k = GetKey();
// 2. 创建/获取共享内存
_shmid = shmget(k, _size, flags);
if (_shmid < 0)
{
perror("shmget error");
return false;
}
return true;
}
public:
// 1. 创建全新的共享内存(若已存在则报错)
bool Create()
{
return CreateCoreHelper(IPC_CREAT | IPC_EXCL | 0666);
}
// 2. 获取已存在的共享内存
bool Get()
{
return CreateCoreHelper(IPC_CREAT | 0666);
}
// 3. 从内核中删除共享内存
bool Delete()
{
int n = shmctl(_shmid, IPC_RMID, nullptr);
return n != -1;
}
// 4. 打印共享内存内核属性
void GetShmAttr()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
if(n < 0)
{
perror("shmctl error");
return;
}
std::cout << "\n===== 共享内存属性信息 =====" << std::endl;
std::cout << "当前进程PID: " << getpid() << std::endl;
std::cout << "创建者PID: " << ds.shm_cpid << std::endl;
std::cout << "共享内存大小: " << ds.shm_segsz << " 字节" << std::endl;
std::cout << "唯一KEY值: " << ToHex(ds.shm_perm.__key) << std::endl;
std::cout << "==========================\n" << std::endl;
}
// 5. 将共享内存附加到当前进程地址空间
void* Attach()
{
_start = shmat(_shmid, nullptr, 0);
if(_start == (void*)-1)
{
perror("shmat error");
_start = nullptr;
return nullptr;
}
return _start;
}
// 6. 将共享内存从当前进程地址空间分离
void Detach()
{
if(_start != nullptr)
{
shmdt(_start);
_start = nullptr;
}
}
// 调试打印:key与shmid
void Debug()
{
std::cout << "===== 调试信息 =====" << std::endl;
std::cout << "key: " << ToHex(_key) << std::endl;
std::cout << "shmid: " << _shmid << std::endl;
std::cout << "====================\n" << std::endl;
}
private:
key_t _key; // 共享内存唯一标识
int _shmid; // 共享内存操作ID
int _size; // 共享内存大小
void* _start; // 映射到进程地址空间的起始地址
};
// 共享内存中存储的数据结构(可自定义)
typedef struct Data
{
int count; // 计数变量
char buffer[26 * 2]; // 数据缓冲区
} buffer_t;
#endif
三 理解消息队列基本原理
如果队列里面只有数据,操作系统怎么知道数据是A的还是B的?
A把数据给B,就必须保证这个数据块是有类型数据块
消息队列是一个进程,给另一个进程发送有类型数据块的方式(支持互相发消息)
如何进行通信?-->系统调用:msgsnd
cpp
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
| 参数名 | 说明 |
|---|---|
| msqid | 消息队列 ID,由 msgget 获取 |
| msgp | 指向消息结构体的指针,必须以 long mtype 开头 |
| msgsz | 消息数据部分长度,不包含 mtype |
| msgflg | 0:阻塞;IPC_NOWAIT:非阻塞,队列满立即出错 |
| 返回值 | 成功:0失败:-1 |
消息队列其他的内容我们就不在这里谈了