神奇的共享内存

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

前言

共享内存(shared memory)是最常见的ipc进程之间通讯的方式之一了,很多linux书籍上,都将共享内存评价为“最有用的ipc机制”,就连Binder机制盛行的android体系,同样也离不开共享内存的应用!在所以ipc方式中,共享内存以“快”赢得了很多开发者的掌声,我们下面深入看看!

共享内存相关函数

image.png 首先讲到共享内存,那么肯定离不开要介绍几个函数

shmget

int shmget(key_t key, size_t size, int shmflg);
复制代码

shmget函数用来获取一个内存区的ipc标识,这个标识在内核中,属于一个身份标识符号(ipc标识符,正常情况下是不会重复的,但是标识符也有限制的,比如linux2.4最大为32768,用完了就会重新计算),通过shmget调用,会返回给我们当前的ipc标识,如果这个共享内存区本来就不存在,就直接创建,否则就把当前标识直接返回给我们!说了一大堆,其实很简单,就相当于给我们返回了一个代表该共享内存的标识罢了!

shmat

void *shmat(int shmid, const void *shmaddr, int shmflg);
复制代码

shmat把一个共享内存区域添加到进程上,我们之前在mmap这一章节有提到过线性区的概念,就是进程可用的一组地址(可以用,但是用的时候才真正分配),而shmat就把共享内存的这块地址,通过(shmid shmget可以获取到的)放到了进程中的可用地址范围内,用范围内的合适地址(shmaddr这里指进程想要发生映射的可用地址)指向了共享内存实际的地址,可以见上图!

shmdt

int shmdt(const void *shmaddr);
复制代码

用于从当前进程把指定的共享内存shmaddr地址分离出去,这里只是分离,只是从当前进程中不可见了,但是对于其他进程来说,还是依旧存在的,再拿上面的图举例子,如果进程1中调用了shmadt,那么当前状态就如下图所示

image.png 同时这里有个非常需要注意的点,就是就算共享内存没有被其他任何进程使用,它所占有的页也是不能直接被删除的,只能用“页的换出”操作代替不用的页(留个疑问,后文解析)

image.png 当然,为了避免用户态过程中共享内存的过分创建,一般的限制大小为4096个

共享内存本质

看到这里的朋友,包括我,一定会想问,共享内存最本质是个什么东西呀?为什么linux会创建处理这么一个神奇的东西?在这里我可以告诉大家,共享内存其实就是一个“文件”!不光如此,我们所熟知的ipc方式,比如管道,消息队列,共享内存,其实就是对文件的操作!我的天,我们嗤之以鼻的“文件”,最不起眼不被用的ipc方式,只是换了个名称,就让大家高攀不起了!是的,共享内存的本质,其实就是shm特殊文件系统的一个文件罢了!因为shm文件系统在linux系统中没有安装点,即没有可视化的文件路径,普通用户无法“看到”或者“摸到”,就给我们产生了一个错觉,以为是一个很高深的东西,其实并没有啦!一个共享内存,其实就是一个文件,只不过这个文件我们看不到罢了,但是linux内核能看到,就这么简单!(以后面试官问到ipc有哪些,回答“文件”即可哈哈哈,手动狗头)

那么接下来又有一个问题了,为什么一个文件能有这么大的奇效,我们常说的共享内存只需要一次拷贝(假如进程a写入到进程b可见算一次)呀,面试官还经常问我们呢!一个小小文件怎么做到的?没错,没错!就是mmap搞得鬼呀!属于共享内存的这个文件,在进程中其实就是使用了mmap操作,把进程的地址映射到了这个文件,所以写入一次就对其他同样进行mmap的进程可见罢了!这个mmap,是通过shm_mmap函数实现的(细节可看官网,这里就不贴出来了)最后我们再看一下共享内存的核心数据结构,shmid_kernel

struct shmid_kernel /* private to the kernel */
{   
    struct kern_ipc_perm shm_perm;  //描述进程间通信许可的结构
    struct file * shm_file;        //指向共享内存文件的指针 
    unsigned long shm_nattch;     //挂接到本段共享内存的进程数
    unsigned long shm_segsz;     //段大小
    time_t        shm_atim;     //最后挂接时间
    time_t        shm_dtim;    //最后解除挂接时间
    time_t        shm_ctim;   //最后变化时间
    pid_t         shm_cprid; //创建进程的PID 
    pid_t         shm_lprid;//最后使用进程的PID
    
    ....
};
复制代码

共享内存页回收问题

我们刚刚留下了一个疑问点,就是共享内存的页就算没有进程引用,也不能被直接删除,而是采用换出的方式!为什么不能被删除呢?因为在正常情况下,linux内核中对于页删除有比较严格的判断,页被删除的前提需要页被标记被脏,触发磁盘写回的操作,然后才会从删除这个页!但是共享内存的页其实在磁盘上是没有存在映射的索引节点的,因此写回磁盘这个操作前提就不成立,所以正常的处理是这个页会被保留,但是页的内容会被其他有需要的页的“伙伴”被复用,做到只是数据的删除页不删除!这是需要注意的点!当然,在紧急内存不足的情况下,系统也会调用try_to_swap_out方法,回收一般页,但是共享内存的页会有定制的shmem_write_page,会进行页的copy操作,防止了属于共享内存的页被“直接删除”。

Android中的共享内存

Android中也有很多地方用到了共享内存,比如ContentProvider中数据的交换,比如CursorWindow的数据交换,里面其实就是利用了共享内存。还有就是传递给SurfaceFlinger的渲染数据,也就是通过共享内存完成的。之所以使用共享内存,还是得益于共享内存的设计,效率较高且没有像管道这种多拷贝的情况,不使用Binder是也是因为Binder依赖的Parcel数据传输,在大数据上并没有很大的优势!当然,相比于Binder,共享内存算是作为最底层api,并没有提供同步机制!当然,Binder同时也用了mmap(binder_mmap),在这基础上通过mutex_lock进行了同步机制,算是比共享内存有了更加契合Android的设计

image.png

总结

看完这里,应该都会用共享内存进行我们所需的开发了,无论是Binder还是共享内存,只有在合适自己的场合使用,才能获得最大收益!最后!

image.png

猜你喜欢

转载自juejin.im/post/7131333107696795678