文章目录
-
- [进程间通信(三):共享内存深度剖析与System V IPC机制](#进程间通信(三):共享内存深度剖析与System V IPC机制)
- 一、为什么需要共享内存
-
- [1.1 管道的性能瓶颈](#1.1 管道的性能瓶颈)
- [1.2 共享内存的优势](#1.2 共享内存的优势)
- 二、共享内存原理
-
- [2.1 共享内存示意图](#2.1 共享内存示意图)
- [2.2 共享内存的工作流程](#2.2 共享内存的工作流程)
- [三、System V共享内存函数](#三、System V共享内存函数)
-
- [3.1 共享内存数据结构](#3.1 共享内存数据结构)
- [3.2 shmget - 创建/获取共享内存](#3.2 shmget - 创建/获取共享内存)
- [3.3 shmat - 挂接共享内存](#3.3 shmat - 挂接共享内存)
- [3.4 shmdt - 解除挂接](#3.4 shmdt - 解除挂接)
- [3.5 shmctl - 控制共享内存](#3.5 shmctl - 控制共享内存)
- 四、共享内存实战
-
- [4.1 封装共享内存操作](#4.1 封装共享内存操作)
- [4.2 Server进程 - 读取共享内存](#4.2 Server进程 - 读取共享内存)
- [4.3 Client进程 - 写入共享内存](#4.3 Client进程 - 写入共享内存)
- [4.4 Makefile](#4.4 Makefile)
- [4.5 运行测试](#4.5 运行测试)
- [4.6 查看系统中的共享内存](#4.6 查看系统中的共享内存)
- 五、共享内存的并发问题
-
- [5.1 共享内存缺乏访问控制](#5.1 共享内存缺乏访问控制)
- [5.2 解决方案 - 借助管道实现访问控制](#5.2 解决方案 - 借助管道实现访问控制)
- [5.3 实战:带访问控制的共享内存](#5.3 实战:带访问控制的共享内存)
- [5.4 运行测试](#5.4 运行测试)
- [六、System V IPC机制补充](#六、System V IPC机制补充)
-
- [6.1 消息队列(了解)](#6.1 消息队列(了解))
- [6.2 信号量(了解)](#6.2 信号量(了解))
- 七、内核如何管理IPC资源
-
- [7.1 IPC资源的特点](#7.1 IPC资源的特点)
- [7.2 内核数据结构(参考Linux 2.6.11)](#7.2 内核数据结构(参考Linux 2.6.11))
- 八、总结
进程间通信(三):共享内存深度剖析与System V IPC机制
💬 欢迎讨论:前两篇我们学习了管道机制,管道虽然简单易用,但性能并不是最优的。每次通信都需要经过内核缓冲区,涉及两次数据拷贝。有没有更快的IPC方式呢?答案是共享内存------最快的进程间通信方式!本篇将带你深入理解共享内存的原理、System V IPC机制,以及如何解决共享内存的并发问题。
👍 点赞、收藏与分享:这篇文章包含了共享内存的底层原理、完整代码实现、访问控制机制、以及System V IPC的管理方式,内容深入且实用,如果对你有帮助,请点赞、收藏并分享!
🚀 循序渐进:建议先掌握前两篇的管道知识,这样理解共享内存的优势会更清晰。
一、为什么需要共享内存
1.1 管道的性能瓶颈
我们先回顾一下管道的数据流向:
bash
进程A写数据到管道:
用户空间(进程A) ──拷贝1──→ 内核缓冲区(管道)
进程B从管道读数据:
内核缓冲区(管道) ──拷贝2──→ 用户空间(进程B)
完整流程:
进程A的buf → 内核pipe缓冲区 → 进程B的buf
↑ ↑
拷贝1 拷贝2
问题:
bash
✗ 两次数据拷贝(效率低)
✗ 需要进入内核(系统调用开销)
✗ 管道容量有限(默认64KB)
场景:
假设两个进程需要共享一个100MB的视频缓冲区,用管道传输需要:
bash
1. 进程A写100MB到管道 → 多次write,因为管道只有64KB
2. 内核拷贝100MB到管道缓冲区
3. 进程B从管道读100MB → 多次read
4. 内核拷贝100MB到进程B
总共拷贝了200MB数据!
1.2 共享内存的优势
共享内存的核心思想:让多个进程直接访问同一块物理内存!
bash
传统方式(独立地址空间):
进程A虚拟地址 → 物理内存页A
进程B虚拟地址 → 物理内存页B
↑
数据需要拷贝
共享内存方式:
进程A虚拟地址 ──┐
├──→ 同一块物理内存
进程B虚拟地址 ──┘
↑
零拷贝!直接访问!
优势:
bash
✓ 零拷贝 (最快的IPC方式)
✓ 不涉及内核操作 (进程直接读写内存)
✓ 容量大 (可以申请几GB)
✓ 适合大数据量传输
性能对比:
| IPC方式 | 速度 | 数据拷贝次数 | 适用场景 |
|---|---|---|---|
| 管道 | 中等 | 2次 | 小数据流式传输 |
| 消息队列 | 较慢 | 2次 | 带优先级的消息 |
| 共享内存 | 最快 | 0次 | 大数据量传输 |
二、共享内存原理
2.1 共享内存示意图
bash
进程A的地址空间 物理内存 进程B的地址空间
┌─────────────┐ ┌─────────────┐
│ 栈 │ │ 栈 │
├─────────────┤ ├─────────────┤
│ │ │ │
│ ↓ │ │ ↓ │
│ │ │ │
│ ... │ │ ... │
│ │ │ │
│ ↑ │ │ ↑ │
│ │ │ │
├─────────────┤ ┌─────────────┐ ├─────────────┤
│ 共享内存区 │────────→ │ 共享内存页 │ ←─────────│ 共享内存区 │
│ 0x7f00000 │ 映射 │ (物理内存) │ 映射 │ 0x7f80000 │
├─────────────┤ └─────────────┘ ├─────────────┤
│ 堆 │ │ 堆 │
├─────────────┤ ├─────────────┤
│ 数据段 │ │ 数据段 │
├─────────────┤ ├─────────────┤
│ 代码段 │ │ 代码段 │
└─────────────┘ └─────────────┘
关键理解:
bash
1. 两个进程的虚拟地址可能不同
进程A看到的地址: 0x7f000000
进程B看到的地址: 0x7f800000
2. 但它们映射到同一块物理内存
物理地址: 0x12345000
3. 进程A写入数据,进程B立即可见
A: *ptr = 100;
B: printf("%d", *ptr); // 输出100
2.2 共享内存的工作流程
bash
步骤1: 创建共享内存
shmget() → 在内核中分配一块物理内存
返回共享内存标识符(shmid)
步骤2: 挂接到进程地址空间
shmat() → 修改进程页表,建立虚拟地址到物理地址的映射
返回虚拟地址指针
步骤3: 使用共享内存
进程A: *ptr = data; // 直接写入
进程B: data = *ptr; // 直接读取
步骤4: 解除挂接
shmdt() → 从进程地址空间中解除映射
(物理内存还在!)
步骤5: 删除共享内存
shmctl(IPC_RMID) → 释放物理内存
完整流程图:
bash
进程A 内核 进程B
│ │ │
├─ shmget(key, size) │ │
│ │ │ │
│ └────────→ 创建共享内存对象 │
│ 分配物理内存 │
│ 返回shmid │
│ │ │
├─ shmat(shmid) │ │
│ │ │ │
│ └────────→ 映射到进程A地址空间 │
│ 返回虚拟地址ptr_a │
│ │ │
│ │ shmget(key) ←┤
│ │ │ │
│ 获取已存在的shmid ←────────────────┘ │
│ 返回shmid │
│ │ │
│ │ shmat(shmid) ←┤
│ │ │ │
│ 映射到进程B地址空间 ←────────────────┘ │
│ 返回虚拟地址ptr_b │
│ │ │
├─ *ptr_a = 100 │ │
│ │ │ │
│ └────────→ 写入物理内存 ────────────────────→ │
│ │ │
│ │ *ptr_b ←┤
│ │ 读到100 │
│ │ │
├─ shmdt(ptr_a) │ │
│ │ │
│ │ shmdt(ptr_b) ←┤
│ │ │
│ │ │
├─ shmctl(IPC_RMID) │ │
│ │ │ │
│ └────────→ 删除共享内存 │
│ 释放物理内存 │
三、System V共享内存函数
3.1 共享内存数据结构
内核用shmid_ds结构管理每一块共享内存:
c
struct shmid_ds {
struct ipc_perm shm_perm; // 权限信息
int shm_segsz; // 共享内存大小(字节)
__kernel_time_t shm_atime; // 最后一次attach时间
__kernel_time_t shm_dtime; // 最后一次detach时间
__kernel_time_t shm_ctime; // 最后一次改变时间
__kernel_ipc_pid_t shm_cpid; // 创建者PID
__kernel_ipc_pid_t shm_lpid; // 最后操作者PID
unsigned short shm_nattch; // 当前attach的进程数
unsigned short shm_unused;
void *shm_unused2;
void *shm_unused3;
};
3.2 shmget - 创建/获取共享内存
函数原型:
c
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
// 参数:
// key: 共享内存的键值(名字)
// size: 共享内存大小(字节)
// shmflg: 标志位
//
// 返回值:
// 成功: 共享内存标识符(shmid)
// 失败: -1
关键参数说明:
1. key的作用
bash
key就像是共享内存的"名字"
不同进程通过相同的key找到同一块共享内存
进程A: shmget(0x1234, 4096, ...)
进程B: shmget(0x1234, 4096, ...)
↑
相同的key → 访问同一块共享内存
如何生成key? 使用ftok()函数:
c
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
// pathname: 一个存在的文件路径
// proj_id: 项目ID(1-255)
//
// 返回: 生成的key值
// 示例
key_t key = ftok("/home/user", 0x66);
// 只要pathname和proj_id相同,返回的key就相同
2. shmflg标志位
| 标志 | 含义 |
|---|---|
IPC_CREAT |
如果不存在则创建,存在则获取 |
IPC_EXCL |
与IPC_CREAT配合,存在则报错 |
0666 |
权限(类似文件权限) |
常用组合:
c
// 组合1: 创建或获取
int shmid = shmget(key, size, IPC_CREAT | 0666);
// 不存在→创建
// 已存在→获取
// 组合2: 必须创建新的
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
// 不存在→创建
// 已存在→返回-1, errno=EEXIST
// 组合3: 只获取
int shmid = shmget(key, 0, 0);
// 存在→获取
// 不存在→返回-1
3.3 shmat - 挂接共享内存
函数原型:
c
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 参数:
// shmid: 共享内存标识符
// shmaddr: 指定挂接地址(通常传NULL,让内核自动选择)
// shmflg: 标志位
//
// 返回值:
// 成功: 共享内存的虚拟地址
// 失败: (void *)-1
shmaddr参数说明:
c
// 情况1: shmaddr = NULL (推荐)
void *addr = shmat(shmid, NULL, 0);
// 内核自动选择合适的地址
// 情况2: shmaddr != NULL 且 shmflg无SHM_RND
void *addr = shmat(shmid, (void *)0x50000000, 0);
// 精确挂接到0x50000000
// 如果该地址不可用,返回失败
// 情况3: shmaddr != NULL 且 shmflg有SHM_RND
void *addr = shmat(shmid, (void *)0x50001234, SHM_RND);
// 向下对齐到SHMLBA的整数倍
// 实际地址 = 0x50001234 - (0x50001234 % SHMLBA)
shmflg标志:
c
// SHM_RDONLY: 只读挂接
void *addr = shmat(shmid, NULL, SHM_RDONLY);
// 只能读,不能写
// 默认(0): 可读可写
void *addr = shmat(shmid, NULL, 0);
3.4 shmdt - 解除挂接
函数原型:
c
int shmdt(const void *shmaddr);
// 参数:
// shmaddr: shmat返回的地址
//
// 返回值:
// 成功: 0
// 失败: -1
重要提示:
bash
shmdt只是解除挂接,不会删除共享内存!
进程A: shmat() → shmdt()
进程B: shmat() → 仍然可以访问
只有调用shmctl(IPC_RMID)才会真正删除
3.5 shmctl - 控制共享内存
函数原型:
c
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 参数:
// shmid: 共享内存标识符
// cmd: 操作命令
// buf: 指向shmid_ds结构的指针
//
// 返回值:
// 成功: 0(或其他值,取决于cmd)
// 失败: -1
常用cmd:
c
// IPC_STAT: 获取共享内存信息
struct shmid_ds info;
shmctl(shmid, IPC_STAT, &info);
printf("大小: %d\n", info.shm_segsz);
printf("attach数: %d\n", info.shm_nattch);
// IPC_SET: 设置共享内存属性
struct shmid_ds new_info;
new_info.shm_perm.mode = 0644; // 修改权限
shmctl(shmid, IPC_SET, &new_info);
// IPC_RMID: 删除共享内存(最常用)
shmctl(shmid, IPC_RMID, NULL);
IPC_RMID的特殊行为:
bash
标记删除,而非立即删除!
时间线:
t1: shmctl(shmid, IPC_RMID, NULL) ← 标记为删除
此时如果有进程已attach,共享内存不会立即释放
t2: 进程A继续使用共享内存 ← 仍然可以访问
t3: 进程A调用shmdt() ← 解除挂接
t4: 最后一个进程shmdt()后,共享内存才真正释放
四、共享内存实战
4.1 封装共享内存操作
comm.h - 头文件
c
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "/tmp" // ftok的路径
#define PROJ_ID 0x6666 // 项目ID
// 创建共享内存
int createShm(int size);
// 获取已存在的共享内存
int getShm(int size);
// 删除共享内存
int destroyShm(int shmid);
#endif
comm.c - 实现
c
#include "comm.h"
// 通用函数:创建或获取共享内存
static int commShm(int size, int flags) {
// 1. 生成key
key_t key = ftok(PATHNAME, PROJ_ID);
if (key < 0) {
perror("ftok");
return -1;
}
// 2. 创建/获取共享内存
int shmid = shmget(key, size, flags);
if (shmid < 0) {
perror("shmget");
return -2;
}
return shmid;
}
// 创建新的共享内存
int createShm(int size) {
return commShm(size, IPC_CREAT | IPC_EXCL | 0666);
}
// 获取已存在的共享内存
int getShm(int size) {
return commShm(size, IPC_CREAT);
}
// 删除共享内存
int destroyShm(int shmid) {
if (shmctl(shmid, IPC_RMID, NULL) < 0) {
perror("shmctl");
return -1;
}
return 0;
}
4.2 Server进程 - 读取共享内存
c
// server.c
#include "comm.h"
#include <unistd.h>
#include <string.h>
int main() {
// 1. 创建共享内存(4096字节)
int shmid = createShm(4096);
if (shmid < 0) {
return 1;
}
printf("Server: 创建共享内存成功, shmid=%d\n", shmid);
// 2. 挂接共享内存
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (void *)-1) {
perror("shmat");
return 1;
}
printf("Server: 挂接共享内存成功, addr=%p\n", shmaddr);
// 3. 使用共享内存
sleep(2); // 等待Client连接
int i = 0;
while (i++ < 26) {
printf("Client写入: %s\n", shmaddr);
sleep(1);
}
// 4. 解除挂接
shmdt(shmaddr);
sleep(2);
// 5. 删除共享内存
destroyShm(shmid);
printf("Server: 删除共享内存\n");
return 0;
}
4.3 Client进程 - 写入共享内存
c
// client.c
#include "comm.h"
#include <unistd.h>
#include <string.h>
int main() {
// 1. 获取共享内存
int shmid = getShm(4096);
if (shmid < 0) {
return 1;
}
printf("Client: 获取共享内存成功, shmid=%d\n", shmid);
sleep(1);
// 2. 挂接共享内存
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (void *)-1) {
perror("shmat");
return 1;
}
printf("Client: 挂接共享内存成功, addr=%p\n", shmaddr);
sleep(2);
// 3. 写入数据
int i = 0;
while (i < 26) {
shmaddr[i] = 'A' + i;
i++;
shmaddr[i] = '\0'; // 字符串结束符
sleep(1);
}
// 4. 解除挂接
shmdt(shmaddr);
sleep(2);
return 0;
}
4.4 Makefile
makefile
.PHONY:all
all:server client
server:server.c comm.c
gcc -o $@ $^
client:client.c comm.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f server client
4.5 运行测试
bash
# 编译
$ make
gcc -o server server.c comm.c
gcc -o client client.c comm.c
# 终端1: 启动Server
$ ./server
Server: 创建共享内存成功, shmid=32768
Server: 挂接共享内存成功, addr=0x7f1234000000
Client写入: A
Client写入: AB
Client写入: ABC
...
Client写入: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Server: 删除共享内存
# 终端2: 启动Client
$ ./client
Client: 获取共享内存成功, shmid=32768
Client: 挂接共享内存成功, addr=0x7f5678000000
(Client写入数据...)
注意观察:
bash
1. Server和Client的shmid相同(32768)
→ 说明它们访问同一块共享内存
2. Server和Client的虚拟地址不同
Server: 0x7f1234000000
Client: 0x7f5678000000
→ 但映射到同一块物理内存
3. Client写入后,Server立即能读到
→ 零拷贝,直接内存访问
4.6 查看系统中的共享内存
ipcs命令:
bash
# 查看所有IPC资源
$ ipcs
# 只查看共享内存
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x66007f00 32768 user 666 4096 2
# 字段说明:
# key: 共享内存的键值
# shmid: 共享内存标识符
# owner: 所有者
# perms: 权限
# bytes: 大小
# nattch: 当前attach的进程数
ipcrm命令 - 删除IPC资源:
bash
# 删除共享内存
$ ipcrm -m 32768
# 或者通过key删除
$ ipcrm -M 0x66007f00
注意事项:
bash
如果程序异常退出,共享内存可能没有被删除
$ ./server
^C ← Ctrl+C强制退出
$ ipcs -m
... 共享内存还在 ...
需要手动删除:
$ ipcrm -m 32768
五、共享内存的并发问题
5.1 共享内存缺乏访问控制
问题场景:
bash
进程A正在写数据:
shmaddr[0] = 'H';
shmaddr[1] = 'e';
shmaddr[2] = 'l'; ← 写到一半...
进程B开始读取:
printf("%s", shmaddr); ← 读到"Hel"(不完整!)
根本原因:
c
共享内存只负责"共享",不负责"同步"!
管道自带同步:
write() → read() 顺序执行
共享内存没有同步:
进程A写 ---|
|--- 可能同时发生!
进程B读 ---|
5.2 解决方案 - 借助管道实现访问控制
思路:用管道作为"信号量"
bash
规则:
1. 进程B先阻塞在管道read上
2. 进程A写完数据后,往管道写一个字节
3. 进程B收到信号,开始读取共享内存
完整架构:
bash
进程A(writer) 管道 进程B(reader)
│ │ │
├─ 写共享内存 │ │
├─ write(pipe, "GO") ──→ │
│ │ ├─ read(pipe)阻塞
│ │ ├─ 收到信号
│ │ ├─ 读共享内存
│ │ ├─ 处理数据
5.3 实战:带访问控制的共享内存
Comm.hpp - 封装管道和共享内存操作
cpp
#pragma once
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cassert>
#include <iostream>
#include <cstring>
#define PATH_NAME "/home/user"
#define PROJ_ID 0x66
#define SHM_SIZE 4096
#define FIFO_NAME "./fifo"
#define READ O_RDONLY
#define WRITE O_WRONLY
// 打开FIFO
int OpenFIFO(const char *pathname, int flags) {
int fd = open(pathname, flags);
assert(fd >= 0);
return fd;
}
void CloseFifo(int fd) {
close(fd);
}
// 等待信号(读管道)
void Wait(int fd) {
std::cout << "[等待中...]\n";
uint32_t temp = 0;
ssize_t s = read(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
}
// 发送信号(写管道)
void Signal(int fd) {
uint32_t temp = 1;
ssize_t s = write(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
std::cout << "[唤醒对方...]\n";
}
// 初始化类:创建FIFO
class Init {
public:
Init() {
umask(0);
int n = mkfifo(FIFO_NAME, 0666);
assert(n == 0);
std::cout << "[创建FIFO成功]\n";
}
~Init() {
unlink(FIFO_NAME);
std::cout << "[删除FIFO成功]\n";
}
};
ShmServer.cc - Server端(读取共享内存)
cpp
#include "Comm.hpp"
Init init; // 创建FIFO
int main() {
// 1. 创建key
key_t k = ftok(PATH_NAME, PROJ_ID);
assert(k != -1);
std::cout << "Server创建key: 0x" << std::hex << k << std::dec << "\n";
// 2. 创建共享内存
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
std::cout << "Server创建共享内存, shmid=" << shmid << "\n";
// 3. 挂接共享内存
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
assert(shmaddr != (void *)-1);
std::cout << "Server挂接共享内存, addr=" << (void *)shmaddr << "\n";
// 4. 打开FIFO(读端)
int fd = OpenFIFO(FIFO_NAME, READ);
// 5. 循环读取
while (true) {
// 等待Client写入
Wait(fd);
// 临界区:读取共享内存
printf("Client写入: %s\n", shmaddr);
// 检查退出条件
if (strcmp(shmaddr, "quit") == 0) {
break;
}
}
CloseFifo(fd);
// 6. 解除挂接
int n = shmdt(shmaddr);
assert(n != -1);
std::cout << "Server解除挂接\n";
// 7. 删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
std::cout << "Server删除共享内存\n";
return 0;
}
ShmClient.cc - Client端(写入共享内存)
cpp
#include "Comm.hpp"
int main() {
// 1. 创建key
key_t k = ftok(PATH_NAME, PROJ_ID);
if (k < 0) {
std::cout << "Client创建key失败\n";
exit(1);
}
std::cout << "Client创建key: 0x" << std::hex << k << std::dec << "\n";
// 2. 获取共享内存
int shmid = shmget(k, SHM_SIZE, 0);
if (shmid < 0) {
std::cout << "Client获取共享内存失败\n";
exit(2);
}
std::cout << "Client获取共享内存, shmid=" << shmid << "\n";
// 3. 挂接共享内存
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == nullptr) {
std::cout << "Client挂接失败\n";
exit(3);
}
std::cout << "Client挂接共享内存, addr=" << (void *)shmaddr << "\n";
// 4. 打开FIFO(写端)
int fd = OpenFIFO(FIFO_NAME, WRITE);
// 5. 循环写入
while (true) {
// 从标准输入读取
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if (s > 0) {
shmaddr[s - 1] = '\0'; // 去掉换行符
// 通知Server
Signal(fd);
if (strcmp(shmaddr, "quit") == 0) {
break;
}
}
}
CloseFifo(fd);
// 6. 解除挂接
int n = shmdt(shmaddr);
assert(n != -1);
std::cout << "Client解除挂接\n";
return 0;
}
Makefile
makefile
.PHONY:all
all:ShmServer ShmClient
ShmServer:ShmServer.cc
g++ -o $@ $^ -std=c++11
ShmClient:ShmClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f ShmServer ShmClient fifo
5.4 运行测试
bash
# 编译
$ make
g++ -o ShmServer ShmServer.cc -std=c++11
g++ -o ShmClient ShmClient.cc -std=c++11
# 终端1: 启动Server
$ ./ShmServer
[创建FIFO成功]
Server创建key: 0x66001f48
Server创建共享内存, shmid=32769
Server挂接共享内存, addr=0x7f8a12345000
[等待中...]
# 终端2: 启动Client
$ ./ShmClient
Client创建key: 0x66001f48
Client获取共享内存, shmid=32769
Client挂接共享内存, addr=0x7f9b23456000
hello ← 输入
# 终端1显示:
[唤醒对方...]
Client写入: hello
[等待中...]
# 终端2继续输入:
world
# 终端1显示:
Client写入: world
[等待中...]
# 终端2输入quit退出:
quit
# 终端1显示:
Client写入: quit
Server解除挂接
Server删除共享内存
[删除FIFO成功]
流程分析:
bash
时间轴:
t1: Server创建FIFO和共享内存
t2: Server打开FIFO读端,阻塞在read
t3: Client连接FIFO写端,挂接共享内存
t4: Client写"hello"到共享内存
t5: Client发送信号(write 4字节到FIFO)
t6: Server收到信号,解除阻塞
t7: Server读取共享内存,打印"hello"
t8: Server再次阻塞在read
... 循环 ...
六、System V IPC机制补充
6.1 消息队列(了解)
特点:
bash
✓ 消息有类型和优先级
✓ 可以按类型接收消息
✓ 消息有边界(不像管道是字节流)
✗ 性能比共享内存差(需要拷贝)
基本函数:
c
// 创建消息队列
int msgget(key_t key, int msgflg);
// 发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 控制消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
使用场景:
bash
适合:
- 带优先级的任务调度
- 需要消息分类的系统
不适合:
- 大数据量传输(用共享内存)
- 高频率通信(用管道或共享内存)
6.2 信号量(了解)
并发编程基础概念:
bash
临界资源:
多个进程共享的资源(如共享内存、文件等)
临界区:
访问临界资源的代码段
互斥:
任何时刻只允许一个进程访问临界资源
同步:
多个进程访问临界资源具有一定的顺序性
信号量的本质:
bash
信号量 = 一个计数器
初始值表示资源数量:
sem = 3 → 有3个资源可用
P操作(申请资源):
sem--
if (sem < 0) 阻塞;
V操作(释放资源):
sem++
if (有进程在等待) 唤醒一个;
生活类比:电影院订票
bash
电影院有100个座位 → sem = 100
观众A: P操作 → sem=99, 进入
观众B: P操作 → sem=98, 进入
...
观众100: P操作 → sem=0, 进入
观众101: P操作 → sem=-1, 阻塞(没票了)
观众A离开: V操作 → sem=0, 唤醒观众101
观众101: 进入
基本函数:
c
// 创建信号量集
int semget(key_t key, int nsems, int semflg);
// P/V操作
int semop(int semid, struct sembuf *sops, size_t nsops);
// 控制信号量
int semctl(int semid, int semnum, int cmd, ...);
使用场景:
bash
适合:
- 进程同步
- 资源计数
- 访问控制
实际开发中:
- System V信号量较复杂
- 推荐使用POSIX信号量
- 或者用互斥锁(pthread_mutex)
七、内核如何管理IPC资源
7.1 IPC资源的特点
bash
System V IPC资源的生命周期:
✗ 不随进程 (进程退出,IPC资源不会自动释放)
✓ 随内核 (系统重启才释放)
这就是为什么需要手动删除:
shmctl(shmid, IPC_RMID, NULL);
msgctl(msgid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
7.2 内核数据结构(参考Linux 2.6.11)
所有IPC资源都有一个共同的基类:
c
struct kern_ipc_perm {
key_t key; // IPC键值
uid_t uid; // 所有者UID
gid_t gid; // 所有者GID
uid_t cuid; // 创建者UID
gid_t cgid; // 创建者GID
mode_t mode; // 权限
unsigned long seq; // 序列号
};
共享内存结构:
c
struct shmid_kernel {
struct kern_ipc_perm shm_perm; // 基类
struct file *shm_file; // 关联的文件
unsigned long shm_nattch; // attach次数
unsigned long shm_segsz; // 大小
time_t shm_atim; // attach时间
time_t shm_dtim; // detach时间
time_t shm_ctim; // change时间
pid_t shm_cprid; // 创建者PID
pid_t shm_lprid; // 最后操作者PID
// ...
};
这就是C语言实现多态的方式!
bash
kern_ipc_perm (基类)
│
┌──────────────┼──────────────┐
│ │ │
shmid_kernel msqid_kernel semid_kernel
(共享内存) (消息队列) (信号量)
八、总结
本篇我们深入学习了共享内存和System V IPC机制。
核心知识点:
-
共享内存原理
- 多个进程映射到同一块物理内存
- 零拷贝,最快的IPC方式
- 适合大数据量传输
-
共享内存函数族
- shmget: 创建/获取
- shmat: 挂接到进程地址空间
- shmdt: 解除挂接
- shmctl: 控制(删除)
-
共享内存的并发问题
- 缺乏访问控制
- 需要额外的同步机制
- 可以用管道、信号量等实现
-
System V IPC特点
- 生命周期随内核
- 需要手动删除
- 用key标识资源
-
IPC性能对比
| IPC方式 | 速度 | 复杂度 | 适用场景 |
|---|---|---|---|
| 管道 | 中等 | 简单 | 小数据流式传输 |
| 命名管道 | 中等 | 简单 | 无亲缘关系进程 |
| 消息队列 | 较慢 | 中等 | 消息分类 |
| 共享内存 | 最快 | 需配合同步 | 大数据传输 |
| 信号量 | - | 复杂 | 进程同步 |
共享内存完整流程图:
bash
进程A 进程B
│ │
├─ shmget(key, size) │
├─ shmat(shmid) │
│ ├─ shmget(key, size)
│ ├─ shmat(shmid)
│ │
├─ Wait(pipe)阻塞 ├─ 写共享内存
│ ├─ Signal(pipe)唤醒
├─ 读共享内存 │
│ │
├─ shmdt() ├─ shmdt()
├─ shmctl(IPC_RMID) │
💡 思考题
为什么共享内存需要配合同步机制?
如果多个进程同时写共享内存会发生什么?
除了管道,还能用什么方式实现共享内存的访问控制?
📚 扩展学习POSIX共享内存(shm_open)
内存映射文件(mmap)
POSIX信号量(sem_open)
多线程中的互斥锁和条件变量
至此,进程间通信系列完结!