【Linux】systemV共享内存_共享内存申请接口
个人主页~
systemV共享内存
- 一、工作原理
- 二、系统调用接口
-
- 1、申请共享内存
-
- (一)key的获取
- (二)共享内存的申请
- 2、将共享内存段连接到进程地址空间
- 3、将内存共享段与当前进程脱离
- 4、控制共享内存
- 三、开始通信
-
- 1、comm.hpp
- 2、processa.cpp
- 3、processb.cpp
- 四、其他问题
一、工作原理
操作系统在物理内存上申请一块空间,然后将申请到的空间通过页表映射到进程地址空间mm_struct
的共享区中,然后返回虚拟地址供程序使用,如果多个进程申请的是同一块物理空间,那么它们就可以进行通信
由于同一时间可能有多组进程进行通信,所以系统当中可能存在多个共享内存块,所以操作系统要把这些内存管理起来,所以内核中会有一个结构体来描述共享内存
二、系统调用接口
1、申请共享内存
(一)key的获取
#include #include key_t ftok(const char *pathname, int proj_id);
返回值:成功ftok 会将proj_id
与pathname
对应的文件信息结合起来生成最终的key值,失败返回-1
pathname
:已经存在的文件或目录的路径名
proj_id
:由用户指定的非零整数,通常是一个单字符(因为只有低 8 位会被使用),
只要这两个才参数一致,那么生成的key值就一定一致
key
值是我们少见的由我们自己传参生成的固定的值,Linux在涉及内存方面的时候,通常是操作系统代为处理,这里其实是因为假设我们有两个进程AB,操作系统生成一个key
给到A,但是B也需要这个key
,但是操作系统是不知道哪两个进程要建立信道进行通信的,只有程序员知道我们哪两个进程要建立信道进行通信,因为在建立信道之前进程之间是相互独立的,所以就不能由操作系统来分配key
值
(二)共享内存的申请
shmget
函数的主要作用是在内核中创建或获取共享内存段的标识符
#include #include int shmget(key_t key, size_t size, int shmflg);
返回值:成功返回一个非负整数,即该共享内存段的标识码(shmid
),失败返回-1
key
:key
是一个数字,它是不同共享内存让自己具备唯一性的标识
size
:共享内存大小,单位为字节,一般最好是4096的整数倍,它按照页申请,即需要4097字节就申请4096*2个字节
shmflg
是一个标识符
IPC_CREAT
IPC_EXCL
IPC_CREAT
一起使用,若共享内存已存在,shmget
调用失败,errno
设为 EEXIST
,可确保新创建共享内存IPC_NOWAIT
SHM_HUGETLB
SHM_NORESERVE
0600
、0666
等)0600
表示只有所有者有读写权限,0666
表示所有用户都有读写权限2、将共享内存段连接到进程地址空间
shmat
函数的核心作用是在调用进程的虚拟地址空间和共享内存段的物理内存之间建立映射关系,在调用shmget
函数时,虽然创建或获取了共享内存段的标识符,但进程还不能直接访问该共享内存,只有通过shmat
函数将共享内存段附加到进程的地址空间后,进程才能像访问普通内存一样访问共享内存段中的数据
#include #include void *shmat(int shmid, const void *shmaddr, int shmflg);
返回值:成功返回一个指针,指向共享内存的起始地址,失败返回 -1
shmid
:共享内存段的标识符,用于指定要附加的共享内存段,即shmget
的返回值
shmaddr
:指定共享内存段要附加到的进程地址空间的地址,如果为 NULL,则由系统自动选择合适的地址
shmflg
:标识符
SHM_RDONLY
SHM_RND
shmaddr
参数不为 NULL
时,将 shmaddr
向下舍入到一个合适的内存边界(通常是系统页面大小的整数倍),保证共享内存的正确附加SHM_REMAP
SHM_EXEC
SHM_COPY
SHM_ANON
shmid
参数会被忽略,可结合 shmaddr
使用,常用于父子进程间的内存共享3、将内存共享段与当前进程脱离
当进程调用shmat
函数将共享内存段附加到自己的地址空间后,系统会在进程的虚拟地址空间和共享内存段的物理内存之间建立映射关系,使得进程可以像访问普通内存一样访问共享内存,而shmdt
函数的核心作用就是解除这种映射关系
#include #include int shmdt(const void *shmaddr);
返回值:成功返回0,失败返回-1
shmaddr
:shmat
返回的指针
4、控制共享内存
通过cmd控制共享内存
#include #include int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:成功返回0,失败返回-1
shmid
:同上
cmd
:将要采取的动作
IPC_STAT
shmid
关联的共享内存段的当前状态信息复制到 buf
指向的 struct shmid_ds
结构体中,这个结构体包含了如共享内存段的大小、所有者、权限、创建时间、最后访问时间等信息IPC_SET
buf
指向的 struct shmid_ds
结构体中的值来更新与 shmid
关联的共享内存段的部分状态信息,可以更新的信息包括共享内存段的所有者、权限等IPC_RMID
shmid
关联的共享内存段为删除状态,当最后一个使用该共享内存段的进程分离它之后,系统会真正释放该共享内存段所占用的资源,此时 buf
参数会被忽略,通常传递 NULL
IPC_INFO
struct shmid_ds
),buf
应指向该结构体,用于接收信息SHM_INFO
buf
要指向合适的结构体来接收数据SHM_STAT
IPC_STAT
,但不是通过 shmid
来指定共享内存段,而是通过一个索引值,可以通过这种方式遍历系统中的共享内存段buf
:指向一个保存着共享内存的模式状态和访问权限的结构体struct shmid_ds
三、开始通信
由于共享内存通信没办法做到同步和互斥,我们通过加入命名管道的方式来形成同步与互斥的效果,普通的共享内存通信就调用完接口直接写直接读就行,比较简单,因为共享内存是可以通过指针直接读的,所以我们升级一下
1、comm.hpp
#ifndef __COMM_HPP__#define __COMM_HPP__#include #include #include #include #include #include #include #include #include #define FIFO_FILE \"./myfifo\"#define MODE 0664enum{ FIFO_CREATE_ERR = 1, FIFO_DELETE_ERR, FIFO_OPEN_ERR, FIFO_READ_ERR};using namespace std;Log log;const int size = 4096; const string pathname=\"/home/slm\";const int proj_id = 0x6667;//封装一个获取Key值的函数key_t GetKey(){//使用ftok函数获取key值 key_t k = ftok(pathname.c_str(), proj_id); if(k < 0) { exit(1); } //返回生成的key值 return k;}//封装一个建立共享内存区的函数int GetShareMemHelper(int flag){//调用获取key值,然后在物理内存上申请共享内存 key_t k = GetKey(); int shmid = shmget(k, size, flag); if(shmid < 0) { exit(2); }//返回shmid return shmid;}//用于创建一个新的共享内存段,如果指定的共享内存段已经存在,函数会失败int CreateShm(){ return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);}//用于获取一个共享内存段,如果指定的共享内存段不存在,函数会创建一个新的共享内存段;如果已经存在,则直接返回该共享内存段的标识符int GetShm(){ return GetShareMemHelper(IPC_CREAT); }class Init{public: Init() { // 创建管道 int n = mkfifo(FIFO_FILE, MODE); if (n == -1) { perror(\"mkfifo\"); exit(FIFO_CREATE_ERR); } } ~Init() {// 销毁管道 int m = unlink(FIFO_FILE); if (m == -1) { perror(\"unlink\"); exit(FIFO_DELETE_ERR); } }};#endif
2、processa.cpp
#include \"comm.hpp\"extern Log log;int main(){//创建管道 Init init; //调用 CreateShm 函数创建一个共享内存段,并返回该共享内存段的标识符shmid int shmid = CreateShm(); //调用 shmat 函数将共享内存段附加到当前进程的地址空间,shmid 是共享内存段的标识符 //nullptr 表示让系统自动选择附加地址,0 表示默认附加标志 //返回值是共享内存段在进程地址空间中的起始地址,将其强制转换为char*类型并赋值给shmaddr char *shmaddr = (char*)shmat(shmid, nullptr, 0); //以只读模式打开命名管道文件FIFO_FILE,由于是以只读模式打开,该操作会阻塞 //直到有其他进程以写模式打开同一个命名管道 int fd = open(FIFO_FILE, O_RDONLY); if (fd < 0) { exit(FIFO_OPEN_ERR); } //定义shmds用于存储共享内存段的状态信息 struct shmid_ds shmds; while(true) {//定义一个字符变量c,调用read函数从命名管道文件描述符fd中读取1个字节的数据到c中//并将实际读取的字节数存储在s中,如果能读取到说明通信进程给我们发信号了,有内容//那我们就读取共享内存部分 char c; ssize_t s = read(fd, &c, 1); if(s == 0) break; else if(s < 0) { exit(FIFO_READ_ERR); }//可以直接通过指针访问共享内存 cout << \"client say@ \" << shmaddr << endl; sleep(1);//调用shmctl函数,使用IPC_STAT命令获取共享内存段的状态信息,并将其存储在shmds结构体中//然后将结构体shmds部分信息打印出来 shmctl(shmid, IPC_STAT, &shmds); cout << \"shm size: \" << shmds.shm_segsz << endl; cout << \"shm nattch: \" << shmds.shm_nattch << endl; printf(\"shm key: 0x%x\\n\", shmds.shm_perm.__key); cout << \"shm mode: \" << shmds.shm_perm.mode << endl; }//取消链接 shmdt(shmaddr); //调用shmctl函数,使用IPC_RMID命令将共享内存段标记为待删除状态 //当所有附加进程都分离后,系统会自动删除该共享内存段 shmctl(shmid, IPC_RMID, nullptr); close(fd); return 0;}
3、processb.cpp
#include \"comm.hpp\"int main(){//调用GetShm函数获取一个共享内存段的标识符shmid//在参数相同、无特殊情况的情况下,shmid也相同 int shmid = GetShm(); char *shmaddr = (char*)shmat(shmid, nullptr, 0); int fd = open(FIFO_FILE, O_WRONLY); if (fd < 0) { exit(FIFO_OPEN_ERR); } // 一旦有了共享内存,挂接到自己的地址空间中,直接把他当成你的内存空间来用即可 // 不需要调用系统调用接口 while(true) { cout << \"Please Enter@ \"; fgets(shmaddr, 4096, stdin); //在管道写入一个字符来通知服务器共享内存有新数据 write(fd, \"c\", 1); }//调用shmdt函数将共享内存段从当前进程的地址空间分离,释放相关的资源 shmdt(shmaddr); close(fd); return 0;}
四、其他问题
key
是在操作系统内标定共享内存唯一性的,而shmid
是在进程内标定资源唯一性的,二者虽然都是标定唯一性,但是使用范围不同,并且虽然共享内存属于文件系统,但是shmid
和文件描述符兼容性不好,共享内存这方面单独搞了一套类似于文件描述符表的规则
共享内存的生命周期随内核,除非用户释放或者内核重启,共享内存才会释放
ipcs -m
命令可以查看操作系统中所有的共享内存,其中perms
是权限位,nattch
是和当前共享内存关联的进程个数
共享内存是没有存在同步互斥这样的保护机制的,它是所有进程通信方式中最快的,因为相较其他方式,它的拷贝更少
共享内存内部的数据由用户自己维护
今日分享就到这里了~