引言
本文深入探讨System V IPC中的共享内存技术,涵盖其原理、操作步骤、实现细节及与其他IPC机制的关系,助力读者全面掌握这一高效进程间通信方式。
📝 文章总结:
共享内存原理
System V共享内存通过让多个进程共享同一物理内存区域实现高效通信。操作系统开辟共享内存空间,各进程保持独立的内核数据结构、代码和数据,通过页表映射共享内存到各自虚拟地址空间,从而直接访问和修改数据,减少数据复制开销。
共享内存数据结构
共享内存相关数据结构如
struct shmid_ds
,包含操作权限、段大小、时间戳、创建者和最后操作者进程ID、当前挂载数等信息,用于管理和控制共享内存段。操作步骤
获取key值:通过
ftok
系统调用,以文件路径名和项目标识符生成唯一key值,供创建或访问共享内存段使用。创建共享内存段:使用
shmget
系统调用,传入key值、大小和标志位,创建新共享内存段或获取已有段。映射共享内存:通过
shmat
系统调用,将共享内存段映射到进程虚拟地址空间,使进程能通过指针访问。使用共享内存:进程像访问普通内存一样直接操作共享内存,多个进程可同时读写。
解除映射与删除:使用
shmdt
解除映射,shmctl
删除不再需要的共享内存段。实现进程间通信的准备工作
创建与获取共享内存:利用
shmget
创建或获取共享内存段,标志位组合实现不同创建与访问策略。生成唯一key值:
ftok
函数生成key值,确保不同进程能获取同一共享内存段。封装代码:创建
Shm
类管理共享内存,包含获取key、创建、映射、解除映射、删除等操作,方便服务端和客户端使用。挂接与解除共享内存:使用
shmat
和shmdt
实现共享内存的挂接和解除映射,确保正确管理内存映射。进行通信
客户端与服务端通信实现:客户端写入数据,服务端读取并打印,通过循环和睡眠函数模拟实际通信场景。
共享内存清空:提供
Zero
函数清空共享内存内容,确保通信开始时内存无残留数据。数据一致性问题:指出共享内存不提供保护机制,可能导致数据不一致,需额外同步机制保障。
共享内存的保护机制
共享内存本身不提供保护,易因多进程同时读写出现数据不一致。可借助管道等其他IPC机制实现同步与互斥,如服务端创建管道,客户端写入数据后发信号,服务端接收到信号后读取共享内存,确保数据一致性。
其他相关内容
共享内存大小设置:建议以4096的倍数设置共享内存大小,如4096*n,以适应操作系统内存分配策略。
共享内存属性获取:使用
shmctl
函数的IPC_STAT
命令获取共享内存段状态,如挂载数等信息,辅助调试与监控。共享内存与其他IPC机制关系:System V IPC包含共享内存、消息队列、信号量,共享内存效率最高,但需与其他机制配合解决同步互斥问题。
目录
引言
📝 文章总结:
共享内存的原理
基本原理
图解--对于共享内存的管理-->OS对链表的增删改查
操作步骤
实现进程间通信的准备工作
1. shmget 函数 -- 创建|获取共享内存段
2. ftok 函数--生成一个唯一的key值
3.创建共享内存
4.删除共享内存(指令)
IPC指令以及key和shmid的区别
ipcs -m ---- 查到共享内存
ipcrm -m shmid --- 删除共享内存:
5.封装以上代码
6.User和Creater对于共享内存的访问与创建
构造函数
7. shmctl 函数--对共享内存段进行控制操作
8.析构函数
9. shmat 函数-将共享内存段映射到调用进程的地址空间。
10. shmdt 函数--解除共享内存段与进程地址空间的映射
11.挂接共享内存
12.解除共享内存段与进程地址空间的映射
优化以上部分代码:
进行通信:
共享内存不提供对共享内存的任何保护机制,会导致数据不一致问题。
如何保护共享内存
IPC_STAT:获取共享内存段的状态。
共享内存的原理
System V共享内存是一种高效的进程间通信(IPC)机制,它允许多个进程共享一块物理内存区域,从而避免了数据的复制,显著提高了数据传输速率。以下是System V共享内存的原理及操作步骤:
基本原理
- 共享内存空间:由OS在物理内存当中开辟的内存空间(是由某个进程创建,属于操作系统),而每个进程的内核数据结构,代码和数据依旧保持独立,具有独立性。
共享内存的核心思想:同一块物理内存被页表映射到多个进程的虚拟地址空间(在虚拟地址空间中的堆和栈之间的共享区中申请的一段空间)中。每个进程通过自己的虚拟地址再通过页表访问这块物理内存,再将起始的虚拟地址返回给上层用户,由于多个进程访问的是同一块物理内存,因此它们可以看到彼此写入的数据。
图解--对于共享内存的管理-->OS对链表的增删改查
- 共享内存数据结构
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 */}
高效性:通过共享内存,不同进程可以直接访问和修改数据,而不需要经过数据复制的过程,减少了开销。
持久性:共享内存段在进程退出后仍然存在,直到显式地由进程删除。
权限控制:共享内存段可以设定访问权限,控制哪些进程可以读写共享内存。
操作步骤
获取key值:进程通过
ftok
系统调用来计算出key值。ftok
函数的参数包括一个文件路径名和一个项目标识符,用于生成一个唯一的标识符。创建共享内存段:进程使用
shmget
系统调用来创建或访问共享内存段。shmget
函数需要三个参数,分别是key值、共享内存段的大小和标志位。映射共享内存:通过
shmat
系统调用,将共享内存段映射到进程的虚拟地址空间中。这样,进程就可以通过指针访问共享内存。使用共享内存:进程可以直接通过指针访问共享内存,就像访问普通内存一样。多个进程可以同时对共享内存进行读写操作。
解除映射:进程使用
shmdt
来解除对共享内存段的映射。当进程不再需要访问共享内存时,应该解除映射。删除共享内存段:当不再需要共享内存时,进程通过
shmctl
来删除共享内存段。
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存示意图
实现进程间通信的准备工作
1.
shmget
函数 -- 创建|获取共享内存段shm --- shared memory
用于创建一个新的共享内存段或获取一个已存在的共享内存段。
函数原型:
int shmget(key_t key, size_t size, int shmflg);
#include <sys/ipc.h>#include <sys/shm.h>
参数:
key
:由ftok,
由用户自主生成的key值。想让两进程在不进行通信的前提下,就能得到同一个key,通过一个进程来创建,一个进程来获取来实现进程间的通信。进程如何得知共享内存是否在OS中存在?共享内存的数据结构Struct Shm当中一定会有一个来区分共享内存唯一性的标识符struct Shm *next(类似于进程的pid)
size
:共享内存段的大小(字节)。
shmflg
:标志位(可以采用位图的方式传参)具体是怎么设计的可以看这篇博客【Linux】 基础IO之操作与文件描述符fd全解析:从C语言到系统调用底层实现_io导出0kb文件-CSDN博客
标志位通常设置为:
IPC_CREAT
(创建新段):如果不存在就创建,存在,获取该共享内存并返回标识符。
IPC_EXCL
(不单独使用,和IPC_CREAT组合才有意义
)
IPC_CREAT | IPC_EXCL
(创建新段并确保唯一性):如果不存在就创建,存在就出错返回-1,并设置errno。如果成功返回就意味着这shm是全新的。
还可以通过0666
等权限位设置访问权限。因为,在未来总是有人在创建,有人在获取。命名管道中所讲到的Creater,User。
返回值:
成功:返回共享内存段的标识符(
shm_id
)。失败:返回-1,并设置
errno
。示例:
int shm_id = shmget(key, 4096, IPC_CREAT | 0666); if (shm_id == -1) {perror("shmget error");exit(1); }
2. ftok
函数--生成一个唯一的key值
该key值将用于创建或访问共享内存段。
函数原型:
key_t ftok(const char *pathname, int proj_id);
#include <sys/types.h>#include <sys/ipc.h>
参数:
pathname
:一个存在的文件路径名,用于生成key。
proj_id
:项目标识符,通常是一个字符,用于区分不同的项目。返回值:
成功:返回生成的key值。
失败:返回-1,并设置
errno
。示例:
key_t key = ftok("/tmp/shmfile", 'A'); if (key == -1) {perror("ftok error");exit(1); }
具体实现:
需要准备四个文件:
Shm.hpp --->用于管理共享内存空间 server.cc client.cc
server
和client
是两个常见的角色名称,这种命名方式是为了模拟一种典型的客户端-服务器(Client-Server)架构Makefile:
.PHONY: all all: client serverclient: client.ccg++ -o $@ $^ -std=c++11server: server.ccg++ -o $@ $^ -std=c++11.PHONY: clean clean:rm -rf client server
server
和 client
公共值
1.文件路径了,项目id
const std::string gpathname = "/home/pupu/bitclass/class_29/4.shm";const int gproj_id = 0x66;
2.创建key并转为16进制:
std::string ToHex(key_t k)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}key_t GetCommKey(const std::string &pathname, int proj_id)
{key_t k = ftok(pathname.c_str(), proj_id);if(k < 0){perror("ftok");}return k;
}
3.检测server
和 client获取到的key:
#include "Shm.hpp"int main()
{key_t key = GetCommKey(gpathname, gproj_id);std::cout << "key: " << ToHex(key) << std::endl;return 0;
}
获取到同样的key:
3.创建共享内存
1.创建共享内存
int GetShmHelper(key_t key, int size) {int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL);if (shmid < 0){perror("shmget");}return shmid; }
2.测试:
int shmid = GetShmHelper(key, 4096); //创建一个4096B的共享内存
第一次创建:
多次创建(包括make clean):
这说明了共享内存不随着进程的结束而自动释放
因为共享内存是OS创建的,需要通过系统调用来手动释放,不然这个共享内存会一直存在,除非重启系统。
共享内存的生命周期随内核,文件的生命周期随进程
4.删除共享内存(指令)
IPC指令以及key和shmid的区别
ipcs -m ---- 查到共享内存
ipcs -m
ipcrm -m shmid --- 删除共享内存:
ipcrm -m 0
1.无法用key直接删除
2.使用shmid可删除
key:是我们自己创建的,属于用户形成,内核使用的一个字段,用户不能使用key来进行shm的管理,内核进行区分shm的唯一性
shmid:内核给用户返回的一个标识符,用来进行用户级对共享内存管理的id值(类似于文件系统中的fd ---> struct FILE*(对应每个文件的地址))
为什么要分开:这是为了让内核与用户级隔离 ---> 解耦。
5.封装以上代码
1.添加身份标识:
const int gCreater = 1;
const int gUser = 2;
2.成员参数:
private:key_t _key;int _shmid;std::string _pathname;int _proj_id;int _who;
3.各函数修改:
public: std::string ToHex(){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", _key);return buffer;}private:key_t GetCommKey(){key_t k = ftok(_pathname.c_str(), _proj_id);if (k < 0){perror("ftok");}return k;}int GetShmHelper(key_t key, int size, int flag){int shmid = shmget(key, size, flag);if (shmid < 0){perror("shmget");}return shmid;}// std::string
6.User和Creater对于共享内存的访问与创建
共享内存的大小:
const int gShmSize = 4097; // 4096*n
bool GetShmUseCreate(){if (_who == gCreater){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);if (_shmid >= 0)return true;std::cout << "shm create done..." << std::endl;}return false;}bool GetShmForUse(){if (_who == gUser){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);if (_shmid >= 0)return true;std::cout << "shm get done..." << std::endl;}return false;}
构造函数
Shm(const std::string &pathname, int proj_id, int who): _pathname(pathname), _proj_id(proj_id), _who(who){_key = GetCommKey();if (_who == gCreater)GetShmUseCreate();else if (_who == gUser)GetShmForUse();std::cout << "shmid: " << _shmid << std::endl;std::cout << "_key: " << ToHex() << std::endl;}
7. shmctl
函数--对共享内存段进行控制操作
用于,如获取状态、设置选项或删除共享内存段。
函数原型:
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
参数:
shm_id
:共享内存段标识符。
cmd
:命令,常见的有:
IPC_STAT
:获取共享内存段的状态。
IPC_SET
:设置共享内存段的选项。
IPC_RMID
:删除共享内存段。
buf
:指向shmid_ds
结构的指针,用于存储或修改共享内存段的信息。返回值:
成功:返回0。
失败:返回-1,并设置
errno
。示例(删除共享内存段):
if (shmctl(shm_id, IPC_RMID, NULL) == -1) {perror("shmctl error");exit(1); }
8.析构函数
~Shm(){if (_who == gCreater){int res = shmctl(_shmid, IPC_RMID, nullptr);}std::cout << "shm remove done..." << std::endl;}
以上代码能使服务端和客户端看到共享内存,如何使用在下面的代码讲解:
9. shmat
函数-将共享内存段映射到调用进程的地址空间。
shm --- shared memory,at --- attach(关联)
函数原型:
void *shmat(int shm_id, const void *shmaddr, int shmflg);
参数:
shm_id
:由shmget
返回的共享内存段标识符。
shmaddr
:指定共享内存段映射到进程地址空间的位置。通常设置为NULL
,让系统自动选择地址。
shmflg
:标志位,通常设置为0
。对共享内存的访问权限。可以设置为
SHM_RND
(对齐到页边界)或SHM_RDONLY
(只读映射)。返回值:
成功:返回映射到进程地址空间的指针(地址空间中共享内存的起始地址 ---> 类似于malloc返回的我们在堆上申请的空间的起始地址)。
失败:返回
(void *)-1
,并设置errno
。示例:
char *shm_ptr = (char *)shmat(shm_id, NULL, 0); if (shm_ptr == (void *)-1) {perror("shmat error");exit(1); }
10.
shmdt
函数--解除共享内存段与进程地址空间的映射函数原型:
int shmdt(const void *shmaddr);
参数:
shmaddr
:由shmat
返回的映射地址。返回值:
成功:返回0。
失败:返回-1,并设置
errno
。示例:
if (shmdt(shm_ptr) == -1) {perror("shmdt error");exit(1); }
11.挂接共享内存
void *AttachShm(){void *shmaddr = shmat(_shmid, nullptr, 0);if(shmaddr == nullptr){perror("shmat");}std::cout << "who: "<< RoleToString(_who) <<", attach shm..." << std::endl;return shmaddr;}std::string RoleToString(int who){if(who == gCreater) return "Creater";else if(who == gUser) return "User";else return "null";}
//服务端int main() {// 1. 服务端创建共享内存Shm shm(gpathname, gproj_id, gCreater);// 2. 服务端挂接共享内存char *addr = (char*)shm.AttachShm();sleep(5);return 0; }//客户端 int main() {// 1. 客户端获取共享内存Shm shm(gpathname, gproj_id, gUser);// 2. 服务端挂接共享内存char *addr = (char*)shm.AttachShm();sleep(5);return 0; }
测试并查看效果:在系统当中每隔一秒查看一次共享内存
while :; do ipcs -m; sleep 1; done
perms: 权限 --- 666
nattch:当前共享内存的挂接数(有几个进程)
0---1(server挂接上)---2(server、client同时挂接上)---1 (server删除共享内存)---- null
status:状态
以上所看到的数字变化是基于在创建共享内存时,写了共享内存的权限 0666 or 0664
server:
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
client:
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);
当没有写权限的时候,看不到挂接数量的变化
12.解除共享内存段与进程地址空间的映射
void DetachShm(void *shmaddr){if(shmaddr == nullptr) return;shmdt(shmaddr);std::cout << "who" << RoleToString(_who) << ", detach shm... " << std::endl;}
测试代码(client同理):
//解除共享内存段与进程地址空间的映射shm.DetachShm(addr);sleep(5);
优化以上部分代码:
1.添加成员变量地址空间中共享内存的起始地址的字段
void *_addrshm;
2.在构造函数的时候直接将地址空间中共享内存的起始地址的字段一并初始化:
Shm(const std::string &pathname, int proj_id, int who): _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr){_key = GetCommKey();if (_who == gCreater)GetShmUseCreate();else if (_who == gUser)GetShmForUse();_addrshm = AttachShm();std::cout << "shmid: " << _shmid << std::endl;std::cout << "_key: " << ToHex() << std::endl;}
2.确保重新附加共享内存前,之前的映射已被正确清理
void *AttachShm(){if (_addrshm != nullptr)DetachShm(_addrshm);void *shmaddr = shmat(_shmid, nullptr, 0);if (shmaddr == nullptr){perror("shmat");}std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;return shmaddr;}
为何在
AttachShm()
中提前解除映射?
防止重复映射
如果AttachShm()
被多次调用(例如在对象生命周期内需要重新附加共享内存),新的shmat()
调用会返回一个新的地址,而旧的地址映射仍然存在。这会导致:
内存泄漏:旧的映射未被释放,占用进程地址空间。
逻辑错误:
_addrshm
可能指向无效或过时的地址,导致后续操作异常。确保状态一致性
_addrshm
是类成员变量,代表当前附加的共享内存地址。在重新附加前解除旧的映射,可以确保_addrshm
始终指向最新的有效地址,避免悬空指针。共享内存的灵活管理
共享内存的附加(shmat
)和解除(shmdt
)是进程级的操作,允许一个进程多次附加同一块共享内存到不同地址。若在重新附加前不解除旧映射,进程的地址空间中会积累多个无效映射,影响性能和稳定性。类比与场景举例
类比文件操作
想象打开一个文件时,如果之前已打开过,需要先关闭旧的文件描述符,再打开新的。否则,文件描述符会泄漏。
最后在服务端和客户端去使用的时候:
只需要创建好共享内存段,以及获取共享内存地址。其余的事情都在对象的生命周期,类的内部中完成。
创建公有函数获得映射在虚拟地址空间的共享内存首地址
void *Addr(){return _addrshm;}
Shm shm(gpathname, gproj_id, gUser);char *shmaddr = (char*)shm.Addr();
将Shm类内部再进行调整,确保类的封装完美:
private:
key_t GetCommKey()int GetShmHelper(key_t key, int size, int flag)std::string RoleToString(int who)void *AttachShm()void DetachShm(void *shmaddr)
public:
Shm(const std::string &pathname, int proj_id, int who): _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr)~Shm()std::string ToHex()bool GetShmUseCreate()bool GetShmForUse()void *Addr(){return _addrshm;}
以上所做的都是使用共享内存段来实现进程间通信的准备工作
进行通信:
共享内存不提供对共享内存的任何保护机制,会导致数据不一致问题。
1.让客户端进行写入:2s写一次
char ch = 'A';while (ch <= 'Z'){shmaddr[ch - 'A'] = ch;ch++;sleep(2);}
2.让服务端进行读取,打印客户端写入的内容:1s读取一次
while(true){std::cout << "shm memory content: " << shmaddr << std::endl;sleep(1);}
3.在创建获取共享内存后,将共享内存清空:
void Zero(){if(_addrshm){memset(_addrshm, 0, gShmSize);}}
运行后:
这里的问题:明显在之前的代码当中,已经让客户端sleep(3)后再开始写内容,但是服务端却已经在客户端挂接好后就直接开始读取了。
这次能看见更明显的bug:客户端已经关闭,并且一开始我们就清除了共享内存中的内容,以确保共享内存没有任何内容,但是服务端依旧在读取内容,这里可以回顾管道中所讲的当管道文件中并没有内容写入,那么读取进程就会被阻塞【Linux】深度解析Linux进程间通信:匿名管道原理、实战进程池与高频问题排查。-CSDN博客。
而在这里:客户端2s才写一次,服务端读取一次后按理来说就没有内容了,需要等待写入,却还是将上一个内容读取到了两次。
实际上以上出现的问题是因为:
共享内存不提供对共享内存的任何保护机制,会导致数据不一致问题。
共享内存是所有进程IPC速度最快的,这是因为,共享内存大大减少了数据的拷贝次数(被映射到虚拟地址空间,由用户直接使用)。
如何保护共享内存
利用管道的保护机制进行通信
服务端server:
1.创建共享内存
2.创建管道(使用之前已经封装好的命名管道,这个的讲解在上一篇文章)如图:
运行结果:
在设定共享内存的大小时,我设定的是4097,但是建议设置为4096*n
虽然我设置的是4097,实际上OS分配的共享内存是4096*2,会以4096为一个单位向上取整,但是虚拟地址空间映射过来的大小还是4097,其余的浪费在共享内存当中
共享内存等于共享内存的属性
IPC_STAT
:获取共享内存段的状态。
获取共享内存的属性:
void DebugShm(){struct shmid_ds ds;int n = shmctl(_shmid, IPC_STAT, &ds);if (n < 0)return;std::cout << "ds.shm_perm.__key : " << ToHex() << std::endl;std::cout << "ds.shm_nattch: " << ds.shm_nattch << std::endl;}
在服务端进行测试:
运行结果:
结语:
随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。