一、为什么需要进程通信?
Linux 中不同进程通常拥有彼此独立的:
Virtual Address Space(虚拟地址空间)
例如:
Process A
┌──────────────────────┐
│ Virtual Address Space│
│ Heap │
│ Stack │
│ Global Data │
└──────────────────────┘
Process B
┌──────────────────────┐
│ Virtual Address Space│
│ Heap │
│ Stack │
│ Global Data │
└──────────────────────┘
正常情况下:
Process A
不能直接访问
Process B 的普通用户空间内存
这保证了:
进程隔离。
但是现实程序中,进程之间经常需要:
传输数据
交换消息
同步执行顺序
通知事件
共享资源
因此操作系统提供:
IPC
即:
Inter-Process Communication
中文:
进程间通信。
二、Linux IPC 有哪些方式?
Linux IPC 并不只有三种。
常见方式包括:
Pipe
Named Pipe / FIFO
Signal
System V Message Queue
System V Shared Memory
System V Semaphore
POSIX Message Queue
POSIX Shared Memory
POSIX Semaphore
Socket
Unix Domain Socket
mmap
eventfd
...
其中经典 Unix/Linux 教材经常重点介绍:
System V IPC
System V IPC 有三个非常重要的机制:
① Message Queue
消息队列
② Shared Memory
共享内存
③ Semaphore Set
信号量集
因此:
消息队列、共享内存、信号量集不是 Linux IPC 的全部,而是经典 System V IPC 的三大机制。
三、System V IPC 的整体模型
System V IPC 通常遵循这样的使用模式:
生成 key
↓
获取 / 创建 IPC 对象
↓
得到 IPC ID
↓
通过 IPC ID 操作 IPC 对象
↓
使用完成
↓
删除 IPC 对象
三个 IPC 机制分别对应:
Message Queue
key
↓
msgget()
↓
msqid
↓
msgsnd() / msgrcv()
↓
msgctl()
Shared Memory
key
↓
shmget()
↓
shmid
↓
shmat()
↓
读写内存
↓
shmdt()
↓
shmctl()
Semaphore
key
↓
semget()
↓
semid
↓
semop()
↓
semctl()
四、System V IPC 中最容易混淆的三个概念
必须区分:
pathname
key
IPC ID
例如:
/tmp/myapp
│
│ ftok()
▼
key_t key
│
│ msgget()
▼
int msqid
因此:
ftok()
返回的不是:
消息队列 ID
共享内存 ID
信号量 ID
而是:
key_t
类型的:
IPC Key。
五、ftok()
函数:
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
例如:
key_t key = ftok("/tmp/myapp", 1);
参数:
pathname
→ 一个能够被访问、通常必须存在的文件路径
proj_id
→ 项目标识值
常见写法:
key_t key = ftok("/tmp/myapp", 'A');
六、ftok() 的 pathname 不要求是可执行文件
你原笔记写:
ftok 参数是一个可执行文件名称。
这不准确。
pathname:
不要求是可执行文件。
它可以是一个普通存在的文件路径。
例如:
ftok("/tmp/myapp", 'A');
或者:
ftok("/etc/passwd", 'A');
重点是:
pathname 指向的文件必须满足
ftok()的要求并能够被stat()等方式识别。
七、ftok() 不能保证绝对唯一
这是非常重要的一点。
不能把:
ftok()
理解成:
“生成一个全球唯一或者系统绝对唯一的 ID。”
准确来说:
ftok() 根据文件相关信息和 proj_id 生成一个 key_t 值,但不同输入存在产生相同 key 的可能。
即:
不同 pathname / proj_id
↓
理论上可能
↓
得到相同 key
所以:
ftok()
≠
UUID
八、key 与 IPC ID
假设:
key_t key = ftok("/tmp/myapp", 'A');
然后:
int msqid = msgget(key, IPC_CREAT | 0666);
这里:
key
主要用于:
定位 / 创建 IPC 对象。
而:
msqid
才是当前系统中用于操作这个消息队列的:
Message Queue Identifier。
因此:
ftok()
↓
key_t key
msgget()
↓
msqid
不能混为一谈。
九、也可以不使用 ftok()
ftok() 不是创建 System V IPC 的强制步骤。
例如可以使用:
IPC_PRIVATE
例如:
msgget(IPC_PRIVATE, 0600);
所以更准确的说法是:
ftok() 是让相关进程根据约定的 pathname 和 proj_id 生成相同 IPC key 的常见方法之一,而不是 System V IPC 必须调用的注册函数。
十、System V 消息队列
Message Queue:
消息队列
可以理解成由内核维护的一个:
消息容器
例如:
Process A
│
│ msgsnd()
▼
┌───────────────────────┐
│ Kernel │
│ │
│ Message Queue │
│ │
│ Message 1 │
│ Message 2 │
│ Message 3 │
└──────────┬────────────┘
│
│ msgrcv()
▼
Process B
发送方和接收方不需要直接共享地址空间。
十一、创建或获取消息队列
函数:
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
例如:
key_t key = ftok("/tmp/myapp", 'A');
int msqid = msgget(key, IPC_CREAT | 0666);
成功:
返回消息队列 ID
失败:
返回 -1
十二、msgget() 中的 msgflg
例如:
msgget(key, IPC_CREAT | 0666);
其中:
IPC_CREAT
表示:
如果对应 key 的消息队列不存在,则创建。
如果已经存在:
返回现有消息队列的 ID,前提是权限等条件允许。
十三、IPC_EXCL
例如:
msgget(key, IPC_CREAT | IPC_EXCL | 0666);
其中:
IPC_CREAT
+
IPC_EXCL
表示:
要求创建一个新的消息队列;如果对应 key 的消息队列已经存在,则调用失败。
因此:
IPC_CREAT
→ 没有就创建
IPC_CREAT | IPC_EXCL
→ 必须创建新的
→ 已经存在就失败
这和:
open(..., O_CREAT | O_EXCL)
的思想类似。
十四、消息队列权限
例如:
msgget(key, IPC_CREAT | 0660);
其中:
0660
指定 IPC 对象的访问权限。
类似文件权限:
Owner
Group
Other
例如:
0660
Owner → rw
Group → rw
Other → ---
但是:
System V IPC 权限位主要控制读/写等 IPC 权限,并不能把它完全等同于普通文件的
rwx行为。
十五、消息结构
System V 消息队列中的消息通常定义为:
struct msgbuf {
long mtype;
char mtext[1024];
};
其中:
mtype
→ 消息类型
mtext
→ 消息内容
需要注意:
mtype > 0
十六、发送消息 msgsnd()
函数:
int msgsnd(
int msqid,
const void *msgp,
size_t msgsz,
int msgflg
);
例如:
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "hello");
msgsnd(
msqid,
&msg,
strlen(msg.mtext) + 1,
0
);
执行:
Process A
↓
msgsnd()
↓
Kernel Message Queue
十七、msgsnd() 的阻塞
如果消息队列有足够空间:
msgsnd()
↓
消息进入队列
↓
返回
如果队列空间不足,默认情况下:
msgsnd()
↓
等待空间
↓
进程阻塞
直到满足条件或者调用被其他情况打断。
十八、IPC_NOWAIT
如果不希望等待,可以指定:
IPC_NOWAIT
例如:
msgsnd(
msqid,
&msg,
size,
IPC_NOWAIT
);
如果当前无法立即发送:
不阻塞
↓
立即失败返回
↓
errno 设置相应错误
所以:
msgflg = 0
→ 必要时阻塞
msgflg = IPC_NOWAIT
→ 不等待
→ 无法立即完成就返回错误
十九、接收消息 msgrcv()
函数:
ssize_t msgrcv(
int msqid,
void *msgp,
size_t msgsz,
long msgtyp,
int msgflg
);
例如:
msgrcv(
msqid,
&msg,
sizeof(msg.mtext),
1,
0
);
表示从消息队列接收:
type = 1
的消息。
二十、msgrcv() 也可以阻塞
如果没有符合条件的消息:
msgrcv()
↓
默认等待
↓
Process Blocked
如果指定:
IPC_NOWAIT
那么:
没有符合条件的消息
↓
立即返回错误
因此:
IPC_NOWAIT
既可以影响消息发送,也可以影响消息接收的等待行为。
二十一、管理消息队列 msgctl()
函数:
int msgctl(
int msqid,
int cmd,
struct msqid_ds *buf
);
常见命令:
IPC_STAT
→ 获取消息队列状态
IPC_SET
→ 修改部分属性
IPC_RMID
→ 删除消息队列
例如:
msgctl(msqid, IPC_RMID, NULL);
用于:
标记/删除消息队列。
二十二、System V 共享内存
Shared Memory:
共享内存
核心思想是:
让不同进程的虚拟地址空间映射到同一组共享物理内存页。
例如:
Process A Process B
Virtual Address A Virtual Address B
│ │
│ │
└────────────┐ ┌────────────┘
▼ ▼
Shared Memory
Physical Pages
因此:
Process A 写入
↓
Shared Memory
↓
Process B 可以读取
不需要像消息队列那样把每一条数据作为消息发送给内核队列再接收。
二十三、共享内存的优势
共享内存通常非常高效。
因为建立映射以后:
Process A
↓
直接访问共享映射
Process B
↓
直接访问共享映射
大量数据交换不需要每次都通过:
msgsnd()
msgrcv()
这样的消息复制接口。
因此:
共享内存特别适合大量数据、高性能的进程间通信。
二十四、共享内存最大的问题:同步
假设:
Process A
↓
Shared Memory
↑
Process B
如果:
A 正在写
同时:
B 也正在写
就可能发生:
Race Condition
因此:
共享内存解决的是“如何共享数据”,并没有自动解决“什么时候谁可以访问数据”。
所以共享内存经常与:
Semaphore
Mutex
其他同步机制
配合使用。
二十五、创建共享内存 shmget()
函数:
int shmget(
key_t key,
size_t size,
int shmflg
);
例如:
key_t key = ftok("/tmp/myapp", 'A');
int shmid = shmget(
key,
4096,
IPC_CREAT | 0666
);
表示:
根据 key
↓
获取 / 创建
↓
4096 Bytes Shared Memory
↓
返回 shmid
二十六、shmget() 只是得到共享内存 ID
调用:
shmget()
以后得到:
shmid
但这时程序还不能简单地把 shmid 当内存地址使用。
必须:
Attach
到当前进程地址空间。
二十七、关联共享内存 shmat()
函数:
void *shmat(
int shmid,
const void *shmaddr,
int shmflg
);
例如:
void *addr = shmat(
shmid,
NULL,
0
);
成功以后:
Process Virtual Address Space
┌──────────────────────┐
│ │
│ Shared Memory Mapping│
│ ↑ │
└────────┼─────────────┘
│
│ shmat()
│
Shared Memory Object
此时:
addr
就是当前进程中对应共享内存映射的地址。
二十八、读写共享内存
例如:
char *p = shmat(shmid, NULL, 0);
strcpy(p, "hello");
另一个进程也:
char *p = shmat(shmid, NULL, 0);
printf("%s\n", p);
就可以访问同一个共享内存中的数据。
二十九、解除映射 shmdt()
使用完共享内存以后:
shmdt(addr);
表示:
Detach Shared Memory
即:
当前进程
↓
解除
↓
Shared Memory Mapping
但是需要特别注意:
shmdt() 只是当前进程解除映射,并不等于删除整个 System V 共享内存对象。
三十、删除共享内存 shmctl()
如果需要删除共享内存对象:
shmctl(
shmid,
IPC_RMID,
NULL
);
所以:
shmdt()
→ 当前进程不再映射它
shmctl(IPC_RMID)
→ 将共享内存对象标记为删除
这是两个不同概念。
如果仍有进程附加着该共享内存,实际资源释放还涉及现有 attach 的生命周期。
三十一、共享内存完整流程
因此:
ftok()
↓
得到 key
shmget()
↓
得到 shmid
shmat()
↓
映射进当前进程
read / write
↓
直接访问共享内存
shmdt()
↓
当前进程解除映射
shmctl(IPC_RMID)
↓
删除 / 标记删除共享内存对象
三十二、System V 信号量
Semaphore:
信号量
和消息队列、共享内存有一个本质区别:
信号量主要用于进程/线程之间的同步与互斥,而不是主要用来传输业务数据。
例如:
Process A
↓
Shared Memory
↑
Process B
为了避免:
A、B 同时修改共享数据
可以加入:
Semaphore
三十三、信号量的基本思想
可以把信号量简单理解成一个由内核维护的:
Counter
例如:
Semaphore = 1
表示某资源当前允许一个执行者进入。
Process A:
P / wait / decrement
↓
Semaphore: 1 → 0
↓
进入临界区
Process B 此时也尝试进入:
Semaphore = 0
↓
不能进入
↓
等待
A 完成:
V / post / increment
↓
Semaphore: 0 → 1
B 才有机会继续。
三十四、什么是临界区?
Critical Section:
临界区
指:
访问共享资源、需要防止多个执行者不受控制地同时进入的代码区域。
例如:
Process A ──┐
│
▼
Shared Memory
▲
│
Process B ──┘
如果都执行:
shared->counter++;
就可能产生竞争。
因此:
Semaphore
↓
保护
↓
Critical Section
三十五、为什么叫“信号量集”?
System V Semaphore 一个比较特殊的地方是:
semget()
创建的可以不是单独一个 Semaphore,而是:
Semaphore Set
即:
Semaphore Set
│
├── Semaphore 0
├── Semaphore 1
├── Semaphore 2
└── Semaphore 3
因此中文经常称:
信号量集。
三十六、创建信号量集 semget()
函数:
int semget(
key_t key,
int nsems,
int semflg
);
例如:
int semid = semget(
key,
1,
IPC_CREAT | 0666
);
表示:
创建 / 获取
一个包含 1 个 semaphore 的
Semaphore Set
返回:
semid
三十七、操作信号量 semop()
System V Semaphore 的核心操作函数:
int semop(
int semid,
struct sembuf *sops,
size_t nsops
);
其中 struct sembuf 大致包含:
struct sembuf {
unsigned short sem_num;
short sem_op;
short sem_flg;
};
分别表示:
sem_num
→ 操作信号量集中的哪一个 Semaphore
sem_op
→ 执行什么操作
sem_flg
→ 操作标志
三十八、sem_op 的基本意义
概念上:
sem_op < 0
→ 请求 / 获取资源
→ 可能等待
sem_op > 0
→ 释放 / 增加资源
sem_op == 0
→ 等待信号量值变为 0
例如经典互斥场景:
初始:
Semaphore = 1
Process A:
-1
↓
Semaphore = 0
↓
进入临界区
Process B:
-1
↓
等待
Process A:
+1
↓
Semaphore = 1
↓
Process B 可以继续
三十九、IPC_NOWAIT 与信号量
System V Semaphore 操作也可以指定:
IPC_NOWAIT
如果当前操作无法立即完成:
默认
→ 等待
IPC_NOWAIT
→ 不等待
→ 立即失败返回
因此:
IPC_NOWAIT
是 System V IPC 中一个很重要的“不要阻塞”思想。
四十、管理信号量 semctl()
使用:
semctl()
可以:
获取状态
设置信号量值
获取信号量值
删除信号量集
例如删除:
semctl(
semid,
0,
IPC_RMID
);
四十一、消息队列、共享内存、信号量的关系
三个 System V IPC 可以这样理解:
Message Queue
↓
传消息
Shared Memory
↓
共享数据
Semaphore
↓
控制访问顺序
同步 / 互斥
例如两个进程大量交换数据:
Semaphore
│
▼
Process A → Shared Memory ← Process B
其中:
Shared Memory
→ 真正保存数据
Semaphore
→ 控制谁什么时候访问数据
这是一种经典组合。
四十二、消息队列与共享内存的区别
Message Queue
Process A
↓
msgsnd()
↓
Kernel Queue
↓
msgrcv()
↓
Process B
优点:
使用相对简单
消息天然有边界
支持消息类型
同步逻辑相对容易理解
缺点:
需要通过内核消息队列传递
大量数据通信通常不如共享内存直接
Shared Memory
Process A ─────┐
▼
Shared Pages
▲
Process B ─────┘
优点:
建立共享映射后数据访问非常直接
适合大量数据
性能高
缺点:
需要自己处理同步
容易出现 Race Condition
因此:
Message Queue
→ 数据交换 + 一定程度的消息组织
Shared Memory
→ 高性能共享数据
Semaphore
→ 同步 / 互斥
四十三、查看 System V IPC
Linux 可以使用:
ipcs
查看 System V IPC 对象。
例如:
ipcs -q
查看:
Message Queues
ipcs -m
查看:
Shared Memory Segments
ipcs -s
查看:
Semaphore Arrays
全部查看:
ipcs -a
四十四、删除 System V IPC
命令行可以使用:
ipcrm
例如删除消息队列:
ipcrm -q <msqid>
删除共享内存:
ipcrm -m <shmid>
删除信号量:
ipcrm -s <semid>
这对于实验过程中忘记删除 IPC 对象非常有用。
四十五、System V IPC 对象具有内核生命周期
这一点和普通局部变量完全不同。
例如程序:
创建 Message Queue
↓
程序退出
消息队列不一定因此自动消失。
因为它是:
Kernel IPC Object
类似地:
Shared Memory
Semaphore Set
也可能继续存在,直到显式删除或系统重启等生命周期事件。
因此做实验时经常会出现:
程序早就结束了
↓
ipcs
↓
IPC 对象还存在
所以需要:
msgctl(... IPC_RMID ...)
shmctl(... IPC_RMID ...)
semctl(... IPC_RMID ...)
或者:
ipcrm
进行清理。
四十六、System V IPC 三件套 API 对照
最值得背的是这张关系:
Message Queue
─────────────
创建/获取 msgget()
发送 msgsnd()
接收 msgrcv()
管理/删除 msgctl()
Shared Memory
─────────────
创建/获取 shmget()
映射 shmat()
直接读写 memory access
解除映射 shmdt()
管理/删除 shmctl()
Semaphore
─────────
创建/获取 semget()
操作 semop()
管理/删除 semctl()
再配合:
ftok()
↓
生成 IPC key
就构成经典 System V IPC 编程模型。
四十七、Linux IPC 整体知识体系
学完 System V IPC 后,不要形成:
IPC
=
消息队列 + 共享内存 + 信号量
这种错误认识。
完整一些的知识树应该是:
Linux IPC
│
├── Signal
│
├── Pipe
│ ├── Anonymous Pipe
│ └── FIFO / Named Pipe
│
├── System V IPC
│ ├── Message Queue
│ ├── Shared Memory
│ └── Semaphore Set
│
├── POSIX IPC
│ ├── Message Queue
│ ├── Shared Memory
│ └── Semaphore
│
├── mmap
│
├── Socket
│ ├── TCP Socket
│ ├── UDP Socket
│ └── Unix Domain Socket
│
└── 其他 Linux IPC 机制
四十八、不同 IPC 的典型用途
可以简单记:
Signal
→ 通知事件
Pipe
→ 有亲缘关系进程之间的字节流通信
FIFO
→ 无亲缘关系的本机进程字节流通信
Message Queue
→ 结构化消息传递
Shared Memory
→ 高性能、大量数据共享
Semaphore
→ 同步 / 互斥
Unix Domain Socket
→ 本机进程间双向通信
TCP / UDP Socket
→ 跨主机网络通信
四十九、最终完整模型
System V IPC 最核心的结构:
ftok()
│
▼
key_t
│
┌──────────┼──────────┐
▼ ▼ ▼
msgget() shmget() semget()
│ │ │
▼ ▼ ▼
msqid shmid semid
│ │ │
▼ ▼ ▼
msgsnd() shmat() semop()
msgrcv() │
│ Memory
│ Access
│ │
▼ shmdt() │
msgctl() │ │
│ ▼ ▼
IPC_RMID shmctl() semctl()
│ │
IPC_RMID IPC_RMID
三个机制的本质:
消息队列
=
Kernel 帮进程保存和传递消息
共享内存
=
多个进程映射同一组共享内存页
信号量
=
Kernel 帮进程协调资源访问和执行顺序
五十、一句话总结
Linux 进程间通信(IPC)有多种机制,其中 System V IPC 主要包括消息队列、共享内存和信号量集。相关进程可以通过约定的 key 找到同一个 IPC 对象;
ftok()常用于根据 pathname 和 proj_id 生成 key,但不能保证绝对唯一。消息队列通过msgget/msgsnd/msgrcv/msgctl实现消息传递;共享内存通过shmget/shmat/shmdt/shmctl将同一共享内存映射到多个进程地址空间,适合高性能数据交换;信号量通过semget/semop/semctl实现同步和互斥,常与共享内存配合使用。
浙公网安备 33010602011771号