前言
管道通信的路径是:
进程 A 用户空间
↓ write()
内核管道缓冲区
↓ read()
进程 B 用户空间
每次发送和接收都要经过系统调用,并在用户空间与内核空间之间传递 数据。 操作系统
System V 共享内存换了一个思路:
同一批共享物理页
↙ ↘
进程 A 进程 B
地址空间 地址空间
只要映射建立完成,两个进程就可以通过各自的虚拟地址直接访问这份共享区域。
共享内存快,关键就在这里:映射建立后,进程直接访问共享区域,不需要每次收发数据都走一遍管道式的数据搬运。
一、System V IPC 先认识一下
传统 System V IPC 主要包括三类:
共享内存
消息队列
信号量
它们都是由内核维护的 IPC 对象。
和匿名管道最大的感觉区别之一是:
进程退出,不代表 System V IPC 对象一定自动消失。
例如共享内存没有执行删除操作时,可以在进程结束后继续通过: 计算机内存
ipcs -m
看到它。
所以写 System V IPC 时,除了“创建”和“使用”,还要把“谁负责清理”设计进去。
二、共享内存到底共享了什么
共享内存不是“两个进程拥有同一个虚拟地址”。
更准确的说法是:
进程 A 某段虚拟地址 ─┐
├── 映射到同一份共享内存页
进程 B 某段虚拟地址 ─┘
两边的虚拟地址值甚至可以不同。
重要的是最后落到同一份共享内存对象。
映射以后:
mem[0] = 'A';
映射成功以后,对这段地址的普通读写就是内存访问,不需要每次再调用 write() / read() 完成 IPC 数据搬运。 网络
共享内存快,不是因为“没有内核”,而是建立映射后,每次数据读写不再通过内核 IPC 系统调用做一次额外的数据搬运。
创建、映射、解除映射、删除资源本身仍然需要系统调用。
三、顺手修正一个容易混淆的内存知识
原稿中把 malloc() 后第一次访问内存写成了“写时拷贝”。
这两个概念不要混在一起。
malloc() 取得一段虚拟地址后,操作系统可能采用按需分配 / 缺页分配:
先保留虚拟地址范围
↓
第一次真正访问
↓
触发缺页
↓
内核准备物理页并建立映射
而 Copy-On-Write,写时拷贝 更典型的场景是 fork() 后父子进程暂时共享只读映射页,某一方写入时再复制物理页。
所以:
malloc 首次触碰页面 ≠ fork 后 COW
四、key 和 shmid 分别解决什么问题
这一块最容易把 key 和 shmid 混在一起。
4.1 key:不同进程约定“我要找哪一个对象”
假设 Server 和 Client 都需要连接同一块共享内存。 计算机服务器
在任何通信发生之前,它们就要有一个共同约定。
System V IPC 使用:
key_t key;
作为这种 rendezvous key。
常见做法是通过:
ftok(pathname, proj_id);
生成
ftok():
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
注意: ftok() 不能保证全局绝对不冲突。
它只是根据文件信息和 proj_id 组合一个 key_t。所以工程里要选择稳定、双方都能访问的 pathname,并做好创建冲突处理。
4.2 shmid:内核返回给进程的操作句柄
调用:
int shmid = shmget(key, size, flags);
成功后返回的整数才是后续:
shmat
shmctl
等接口使用的共享内存标识符。 计算机内存
所以可以这样记:
key → 用来“找”
shmid → 找到以后用来“操作”
共享内存对象创建后,内核会返回 shmid 供后续接口使用。
原稿里“不能由 OS 分配 id”这个说法需要删掉。
五、shmget():创建和获取共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
常见创建方式:
int shmid = shmget(
key,
4096,
IPC_CREAT | IPC_EXCL | 0666
);
含义:
IPC_CREAT
不存在就创建
IPC_EXCL
和 IPC_CREAT 一起使用时:
如果已经存在则失败
0666
创建时的访问权限
如果 Server 要保证“我创建到的一定是一块新的共享内存”,用: 计算机服务器
IPC_CREAT | IPC_EXCL | 0666
很合适。
Client 只想获取已经存在的对象时,不需要再写:
IPC_CREAT
可以直接:
int shmid = shmget(key, 4096, 0);
注意:“获取共享内存就使用 IPC_CREAT”这个说法不严谨。
IPC_CREAT 的语义是“不存在就创建”,并不是“只获取”。
六、为什么程序退出了,共享内存还在
创建:
shmget(...)
然后程序直接退出。
再次运行创建者时,如果使用:
IPC_CREAT | IPC_EXCL
可能看到:
File exists
查看:
ipcs -m
这个现象正好说明 System V IPC 的生命周期 和普通进程内资源不一样: Linux与 Unix
进程退出
≠
共享内存对象自动删除
测试时可以:
ipcrm -m <shmid>
手动删除。
正式程序应该由拥有资源生命周期管理职责的一方调用:
shmctl(shmid, IPC_RMID, nullptr);
七、shmat():把共享内存接进自己的地址空间
void *shmat(
int shmid,
const void *shmaddr,
int shmflg
);
最常见:
void* addr = shmat(shmid, nullptr, 0);
让内核选择映射地址。 操作系统
shmat() 的失败判断按接口返回值直接写:
if (addr == (void*)-1)
{
perror("shmat");
}
不要写:
if ((int)addr < 0)
也不要为了 64 位指针改成:
if ((long long)addr < 0)
shmat() 的失败返回值就是 (void*)-1,直接按接口约定比较最稳。
八、shmdt() 和 shmctl(IPC_RMID) 不是一回事
解除当前进程的映射: 操作系统
shmdt(addr);
它只表示:
当前进程不再挂接这块共享内存
并不等于:
删除共享内存对象
真正请求删除:
shmctl(shmid, IPC_RMID, nullptr);
在 Linux 上,IPC_RMID 会把共享内存段标记为删除;如果仍有进程挂接,实际资源会等最后一个挂接解除后再完成回收。
把创建、使用和清理串起来,就是:
shmget
↓
shmat
↓
读写共享内存
↓
shmdt
↓
拥有者 shmctl(... IPC_RMID ...)
九、写一个最小 Server / Client
9.1 公共代码 comm.hpp
#pragma once
#include <cstdio>
#include <cstdlib>
#include <sys/ipc.h>
#include <sys/shm.h>
constexpr const char* PATHNAME = ".";
constexpr int PROJ_ID = 0x66;
constexpr std::size_t SHM_SIZE = 4096;
inline key_t MakeKey()
{
key_t key = ftok(PATHNAME, PROJ_ID);
if (key == -1)
{
perror("ftok");
std::exit(1);
}
return key;
}
9.2 Server:创建、读取、删除
#include "comm.hpp"
#include <cstdio>
#include <cstring>
#include <unistd.h>
int main()
{
key_t key = MakeKey();
int shmid = shmget(
key,
SHM_SIZE,
IPC_CREAT | IPC_EXCL | 0666
);
if (shmid < 0)
{
perror("shmget");
return 1;
}
char* mem =
static_cast<char*>(shmat(shmid, nullptr, 0));
if (mem == (void*)-1)
{
perror("shmat");
shmctl(shmid, IPC_RMID, nullptr);
return 2;
}
while (true)
{
std::printf("client# %s\n", mem);
if (std::strcmp(mem, "quit") == 0)
{
break;
}
sleep(1);
}
shmdt(mem);
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
9.3 Client:获取并写入
#include "comm.hpp"
#include <cstring>
#include <iostream>
#include <string>
int main()
{
key_t key = MakeKey();
int shmid = shmget(key, SHM_SIZE, 0);
if (shmid < 0)
{
perror("shmget");
return 1;
}
char* mem =
static_cast<char*>(shmat(shmid, nullptr, 0));
if (mem == (void*)-1)
{
perror("shmat");
return 2;
}
while (true)
{
std::string message;
std::cout << "Please Enter# ";
if (!std::getline(std::cin, message))
{
break;
}
std::snprintf(
mem,
SHM_SIZE,
"%s",
message.c_str()
);
if (message == "quit")
{
break;
}
}
shmdt(mem);
return 0;
}
原稿里的实验让 Client 逐个写入 A-Z,Server 能直接看到共享区内容变化: 计算机服务器
十、为什么共享内存快,但也最容易把 数据读乱
共享内存映射成功后:
mem[0] = 'A';
写入会直接作用在共享区域。
它没有像管道一样天然提供:
“没有数据时 read 阻塞”
“写完一段以后读端再消费”
这意味着:
Client 正写到一半
↓
Server 同时开始读取
↓
Server 可能看到一个中间状态
共享内存解决的是“共享数据放在哪里”,不是自动解决“什么时候读、什么时候写”。 计算机内存
所以通常还需要:
信号量
互斥锁
条件变量
其他通知机制
配合使用。
10.1 用 FIFO 做一次“通知实验”
你原稿里有一个很有价值的实验:
共享内存:负责真正的数据
命名管道:只负责通知“现在可以读了”
例如 Client 先把一批数据写完整,再:
write(fifo_fd, &token, sizeof(token));
Server 先阻塞:
read(fifo_fd, &token, sizeof(token));
收到 token 后才读共享内存。 计算机内存
这个实验很好地说明了:
数据通道
和
同步/通知通道
可以是两套机制
但它只解决当前一写一读场景里的顺序通知。
如果以后有多个写者同时修改同一片共享区,还需要真正的互斥保护。
这就会自然过渡到下一篇的信号量。
十一、共享内存大小为什么经常和 4 KiB 一起出现
你的实验里申请:
4097 字节
ipcs -m 可能仍显示用户请求的:
4097
但底层内存管理以页为基本单位,实际映射和分配会涉及页粒度向上取整。
所以不要写成:
“shmget 的 size 必须是 4096 的整数倍。”
API 接受的 size 不要求调用者必须写成整页大小。
更准确的说法是:
用户请求大小可以不是页大小整数倍,但内核底层建立映射和准备页时仍然受页粒度约束。 操作系统
十二、shmid_ds 让我们看到内核需要记录什么
用户空间可以通过:
shmctl(shmid, IPC_STAT, &ds);
取得共享内存状态。
典型结构包括:
struct shmid_ds {
struct ipc_perm shm_perm;
size_t shm_segsz;
time_t shm_atime;
time_t shm_dtime;
time_t shm_ctime;
pid_t shm_cpid;
pid_t shm_lpid;
shmatt_t shm_nattch;
// ...
};
其中:
shm_segsz 请求的共享内存大小
shm_atime 最近一次 attach 时间
shm_dtime 最近一次 detach 时间
shm_cpid 创建者 PID
shm_lpid 最近操作进程 PID
shm_nattch 当前挂接数量
ipc_perm 又把: 网络
key
uid/gid
creator uid/gid
mode
sequence
5
这些 System V IPC 公共管理信息抽了出来。
这个“公共头”在消息队列 和信号量里还会再次出现。
总结
共享内存要理解的是两层:
第一层是:
key
↓
shmget
↓
shmid
它解决两个进程如何找到同一个 System V 共享内存对象。
第二层是:
shmid
↓
shmat
↓
虚拟地址
↓
普通内存访问
它解决进程怎样真正读写共享区域。 操作系统
最后把几个容易混淆的结论放在一起:
IPC_CREAT 不是“只获取”,它表示不存在时创建;
Client 只获取已存在对象时可以使用 shmget(key, size, 0);
ftok() 可能碰撞,不提供绝对唯一性保证;
shmat() 失败要和 (void*)-1 比较;
shmdt() 只是解除当前进程映射,不是删除;
IPC_RMID 负责请求删除共享内存;
共享内存本身不提供互斥和同步。
————————————————
版权声明:本文为CSDN博主「爱和冰阔落」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2402_87731470/article/details/163595994