🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
文章目录
除了上次说的管道之外,还有system V的通信方案
system V有三种方式
① 消息队列(太陈旧,不考虑)
② 共享内存
③ 信号量
今天之说共享内存
一、共享内存原理
先回顾一下动态库,动态库是运行时库,在运行时才进行加载,而在使用动态库的地方保存的是偏移量,动态库加载后,各个使用动态库的程序都会根据加载的起始地址计算出使用到的函数和变量的地址。这些都在地址空间的共享区建立了映射,一个动态库可以被多个程序使用,所以也叫共享库。
那操作系统自己申请了一部分空间,将这一部分空间,通过页表,映射到申请的空间,然后页表返回这一部分空间的起始地址,那上层就拿到了这样一个虚拟地址,同样的,可以映射给另一个进程,这样就可以同时访问到这部分空间(共享,所以一般,共享内存,共享库,等等都是建立在堆栈之间的【共享区】)
1.申请空间
2.建立映射
就能够指向同一块内存了,达成了IPC的前提条件
二、共享内存的建立原理
共享内存是属于OS的,因为是OS创建的共享内存
共享内存是操作系统专门设立的一个内存模块,专用于IPC,所以这个模块就称为system V版本的进程IPC
管道是基于文件的读取方式上,恰巧能完成IPC,因为文件有读写的功能,而共享内存是专门为了IPC设计的,一定就决定了操作系统要提供一些功能或者接口,而且它属于操作系统
其次,如果很多进程都要通信,那就需要大量的共享内存,操作系统就要将他们管理起来
共享内存 = 共享内存块 + 对于的共享内存的内核数据结构
所以用链表等等数据结构管理起来,那么对于共享内存的管理,就变成了对特定数据结构的增删查改
三、共享内存的创建
但是共享内存有点不符合Linux下一切皆文件的概念,因为是一套独立的接口,所以后续用的并不多,但是OS也支持
shmget(shared memory segment)共享内存段
int shmget(key_t key,size_t size,int shmflg);
参数的解释:
①key
共享内存创建好了,对于要通信的双方能够看到,并且看到的是属于它的共享内存
这时候我们需要用key开保证,key是多大不重要,只要能够保证在系统唯一即可,比如服务端server和客户端client使用一个key,就可以看到同一个共享内存,就可以IPC
第一个参数就只会一个路径,但是这个路径一定要有权限
第二个参数项目id随便写(要求0-255,但是超过了会截断)
ftok不进行任何的系统调用,内部仅仅是一套算法,算出基本上的唯一值(极小概率重复,所以有了ftok也不一定就会创建成功,其实哈希算法也挺好的)
②size,顾名思义,共享内存段大小
最好是页的整数倍,一PAGE的大小是4K -> 4096
但是开其他大小也不影响,但是操作系统管理内存的时候,采用的是分段式和分页式的技术,就算申请4097,分配的也是8K,而且多出来的你不用,别人也用不了,纯纯浪费空间
③shmflg
常见选项有两种
IPC_CREAT
IPC_EXCL
①单独使用IPC_CREAT的话,如果底层已经存在该共享内存,会直接获取它,并返回,如果不存在,则创建它,并返回
②单独使用IPC_EXCL没有意义
③IPC_CREAT和IPC_EXCL一起使用,如果底层存在,出错返回,如果不存在,创建
④如果shmflg是0,那么默认是IPC_CRETE
shmget的返回值是共享内存的用户层标识符(类似于文件描述符)
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t k = ftok(PATH_NAME,PROJ_ID);
//path和项目id,自己设定
assert(k!=-1);//k==-1就是唯一的key生成失败
//创建共享内存
int shmid = shmget(k,SHM_SIZE/*宏*/,IPC_CREAT|IPC_EXCL);
//建议生成一个全新的共享内存,因为他是通信的发起者
if(shmid ==-1)
{
perror("shmget");
exit(1);
}
}
四、共享内存的删除
我们发现,创建成功,当进程运行结束时,我们的共享内存还存在
ipcs -m //查看IPC ipcs就是查看总舵的ipc资源,-m就是memory共享内存
//手动删除
ipcrm -m shmid //删除ipc资源,是通过shmid来删除的
//代码删除
shmctl //shared memory control;
perms是permissions权限
我们可以看到,刚刚创建出来的共享内存是没有权限的,所以int shmid = shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
在后面加上权限,这样来创建
int shmctl(int shmid,int cmd,struct shmid_ds*buf);
//cmd有很多选项,删除共享内存用的是 IPC_RMID
//buf我们设为nullptr就可以了
想要使用共享内存
①申请共享内存
②在系统当中进行映射,ipcs -m中nattch表示有n个和共享内存关联
五、共享内存挂接到自己的地址空间
shmat (shared memory attach关联,贴上)
①第一个参数就是shmid(共享内存的用户层标识)
②第二个参数是挂接的地址,我们设为nullptr,让操作系统自动挂接,本来是需要传挂机在进程地址空间的哪个位置,但是不推荐自己挂,我们不容易分清楚情况,OS非常清楚,除非有特殊人的用途才需要我们自己挂接
③第三个参数是挂接方式,设为0,默认就挂机好了以读写的方式
但其实shmat这个接口最重要的是它的返回值,返回的是共享内存段的address(虚拟地址),失败返回-1
六、从进程地址空间去掉与共享内存的关联
attach是挂接
shmdt是shared memory detach,取消挂接
int shmdt(const void*shmaddr);
//从这个参数的形式,我们很容易想到shmat的返回值
//也就是说,我传共享空间的地址过去就可以取消挂接了
七、共享内存完整过程代码
//头文件....
string TransToHex(key_t k)//转十六机制
{
char buffer[32];
sprintf(buffer,sizeof buffer,"0x%x",k);
return buffer;
}
//全大写的都是宏
int main()
{
//创建公共的相同key
//相同的算法,相同的数据源,算出相同的key
key_t k = ftok(PATH_NAME,PROJ_ID);
assert(k!=-1);
//日志,这个可以不用管
Log("Create key done",Debug)<<"server key : "<<TransToHex(k)<<endl;
//创建共享内存
int shmid = shmget(k,SHM_SIZE,TCP_CREAT|TCP_EXCL|0666/*权限是八进制*/);
if(shmid == -1)
{
perror("shmget");
exit(1);
}
Log("Create shm done",Debug)<<" shmid : "shmid<<endl;
//挂接
char*shmaddr = (char*)shmat(shmid,nullptr,0);//挂接地址操作系统绝对,方式默认是读写(传0);
Log("Create shm done",Debug)<<" shmid : "shmid<<endl;
//通信
...
//去挂接
int n = shmdt(shmaddr);
assert(n!=-1);
(void)n;//避免告警
Log("Detach shm done",Debug)<<" shmid : "shmid<<endl;
sleep(10);
//删除共享内存 shmctl
int m = shmctl(shmid,IPC_RMID,ullptr);//删除shmid的共享内存
//shmid是共享内存的用户级标识
assert(m!=-1);
(void)m;
Log("Delete shm done",Debug)<<" shmid : "shmid<<endl;
return 0;
}
八、对于建立好共享内存之后的通信
①首先,共享内存是被映射在共享区的,共享区是属于用户空间的,也就是说,这部分空间用户拿到了,可以不通过系统调用,直接访问 (双方要进行通信,直接进程内存级的读和写,不用经过任何函数)
②在之前调用管道的时候,我们建立好管道任然需要去read,write通过系统调用去IPC,因为用的是管道,而文件是属于内核当中的一种特定数据结构(操作系统:进程管理,内存管理,文件管理,驱动管理),所以文件是OS去维护的,所以他是在3G~4G空间范围内的,属于内核空间,用户无法直接进行访问。
而共享内存是创建在堆栈之间的,它属于用户空间,所以不需要调用系统调用,直接访问
访问共享内存方法
我们可以将共享内存看成是一个大的字符串
某一端读取:
printf("%s\n",shmaddr);
//直接就可以打印出共享内存中的内容
另一端写:
snprintf(shmaddr,SHM_SIZE-1,"hello server,我是其他进程,我的pid是:%d",getpid());
结论
①主要是通信双方都是用shm,一方向共享内存写入数据,另一方就可以立马看到对方写入的数据,
所以说共享内存的IPC是所有IPC中最快的。因为不需要过多的拷贝(表现就是不需要将数据交给OS)
②在管道中,管道是提供了访问控制的,而共享内存缺乏访问控制!共享内存天然就是为了让我们快速进行通信的一种机制,所以内部没有提供任何的访问控制,比如shm都没有数据,但是也能读取,不过读到的是空串,可能双方进程都不知道对方的存在。
正因为共享内存缺乏访问控制,那么就会带来并发问题,比如我想写完了你再去,而不是拿一半数据就跑,这样的出来的结果是未定义的,会导致数据不一致问题
进行共享内存的访问控制
那此时如果我想进行一定的访问控制呢》
我们可以通过复用管道来实现共享内存同步的过程
具体做法就是向共享内存写,但是让对方先等待,就是使用管道,让对方去等待管道的数据(访问控制,控制读取的长度),等到了再往后执行,我们也可以用到RAII的思想来控制管道的析构
还是那个问题,共享内存的IPC用的并不多,是独立的一套接口,不符合Linux的一切皆文件观点,无法兼容后序的网络服务,后面有更强大的通信方式,而且通信手段也特别多
最后一个问题就是共享内存不做同步和互斥的处理是很容易出现问题的,非常容易数据不一致