41 | IPC(中):不同项目组之间抢资源,如何协调?
下载APP
关闭
渠道合作
推荐作者
41 | IPC(中):不同项目组之间抢资源,如何协调?
2019-07-01 刘超 来自北京
《趣谈Linux操作系统》
课程介绍
讲述:刘超
时长17:32大小16.06M
了解了如何使用共享内存和信号量集合之后,今天我们来解析一下,内核里面都做了什么。
不知道你有没有注意到,咱们讲消息队列、共享内存、信号量的机制的时候,我们其实能够从中看到一些统一的规律:它们在使用之前都要生成 key,然后通过 key 得到唯一的 id,并且都是通过 xxxget 函数。
在内核里面,这三种进程间通信机制是使用统一的机制管理起来的,都叫 ipcxxx。
为了维护这三种进程间通信进制,在内核里面,我们声明了一个有三项的数组。
我们通过这段代码,来具体看一看。
根据代码中的定义,第 0 项用于信号量,第 1 项用于消息队列,第 2 项用于共享内存,分别可以通过 sem_ids、msg_ids、shm_ids 来访问。
这段代码里面有 ns,全称叫 namespace。可能不容易理解,你现在可以将它认为是将一台 Linux 服务器逻辑的隔离为多台 Linux 服务器的机制,它背后的原理是一个相当大的话题,我们需要在容器那一章详细讲述。现在,你就可以简单的认为没有 namespace,整个 Linux 在一个 namespace 下面,那这些 ids 也是整个 Linux 只有一份。
接下来,我们再来看 struct ipc_ids 里面保存了什么。
首先,in_use 表示当前有多少个 ipc;其次,seq 和 next_id 用于一起生成 ipc 唯一的 id,因为信号量,共享内存,消息队列,它们三个的 id 也不能重复;ipcs_idr 是一棵基数树,我们又碰到它了,一旦涉及从一个整数查找一个对象,它都是最好的选择。
也就是说,对于 sem_ids、msg_ids、shm_ids 各有一棵基数树。那这棵树里面究竟存放了什么,能够统一管理这三类 ipc 对象呢?
通过下面这个函数 ipc_obtain_object_idr,我们可以看出端倪。这个函数根据 id,在基数树里面找出来的是 struct kern_ipc_perm。
如果我们看用于表示信号量、消息队列、共享内存的结构,就会发现,这三个结构的第一项都是 struct kern_ipc_perm。
也就是说,我们完全可以通过 struct kern_ipc_perm 的指针,通过进行强制类型转换后,得到整个结构。做这件事情的函数如下:
通过这种机制,我们就可以将信号量、消息队列、共享内存抽象为 ipc 类型进行统一处理。你有没有觉得,这有点儿面向对象编程中抽象类和实现类的意思?没错,如果你试图去了解 C++ 中类的实现机制,其实也是这么干的。
有了抽象类,接下来我们来看共享内存和信号量的具体实现。
如何创建共享内存?
首先,我们来看创建共享内存的的系统调用。
这里面调用了抽象的 ipcget、参数分别为共享内存对应的 shm_ids、对应的操作 shm_ops 以及对应的参数 shm_params。
如果 key 设置为 IPC_PRIVATE 则永远创建新的,如果不是的话,就会调用 ipcget_public。ipcget 的具体代码如下:
在 ipcget_public 中,我们会按照 key,去查找 struct kern_ipc_perm。如果没有找到,那就看是否设置了 IPC_CREAT;如果设置了,就创建一个新的。如果找到了,就将对应的 id 返回。
我们这里重点看,如何按照参数 shm_ops,创建新的共享内存,会调用 newseg。
newseg 函数的第一步,通过 kvmalloc 在直接映射区分配一个 struct shmid_kernel 结构。这个结构就是用来描述共享内存的。这个结构最开始就是上面说的 struct kern_ipc_perm 结构。接下来就是填充这个 struct shmid_kernel 结构,例如 key、权限等。
newseg 函数的第二步,共享内存需要和文件进行关联。** 为什么要做这个呢?我们在讲内存映射的时候讲过,虚拟地址空间可以和物理内存关联,但是物理内存是某个进程独享的。虚拟地址空间也可以映射到一个文件,文件是可以跨进程共享的。
咱们这里的共享内存需要跨进程共享,也应该借鉴文件映射的思路。只不过不应该映射一个硬盘上的文件,而是映射到一个内存文件系统上的文件。mm/shmem.c 里面就定义了这样一个基于内存的文件系统。这里你一定要注意区分 shmem 和 shm 的区别,前者是一个文件系统,后者是进程通信机制。
在系统初始化的时候,shmem_init 注册了 shmem 文件系统 shmem_fs_type,并且挂在到了 shm_mnt 下面。
接下来,newseg 函数会调用 shmem_kernel_file_setup,其实就是在 shmem 文件系统里面创建一个文件。
__shmem_file_setup 会创建新的 shmem 文件对应的 dentry 和 inode,并将它们两个关联起来,然后分配一个 struct file 结构,来表示新的 shmem 文件,并且指向独特的 shmem_file_operations。
newseg 函数的第三步,通过 ipc_addid 将新创建的 struct shmid_kernel 结构挂到 shm_ids 里面的基数树上,并返回相应的 id,并且将 struct shmid_kernel 挂到当前进程的 sysvshm 队列中。
至此,共享内存的创建就完成了。
如何将共享内存映射到虚拟地址空间?
从上面的代码解析中,我们知道,共享内存的数据结构 struct shmid_kernel,是通过它的成员 struct file *shm_file,来管理内存文件系统 shmem 上的内存文件的。无论这个共享内存是否被映射,shm_file 都是存在的。
接下来,我们要将共享内存映射到虚拟地址空间中。调用的是 shmat,对应的系统调用如下:
在这个函数里面,shm_obtain_object_check 会通过共享内存的 id,在基数树中找到对应的 struct shmid_kernel 结构,通过它找到 shmem 上的内存文件。
接下来,我们要分配一个 struct shm_file_data,来表示这个内存文件。将 shmem 中指向内存文件的 shm_file 赋值给 struct shm_file_data 中的 file 成员。
然后,我们创建了一个 struct file,指向的也是 shmem 中的内存文件。
为什么要再创建一个呢?这两个的功能不同,shmem 中 shm_file 用于管理内存文件,是一个中立的,独立于任何一个进程的角色。而新创建的 struct file 是专门用于做内存映射的,就像咱们在讲内存映射那一节讲过的,一个硬盘上的文件要映射到虚拟地址空间中的时候,需要在 vm_area_struct 里面有一个 struct file *vm_file 指向硬盘上的文件,现在变成内存文件了,但是这个结构还是不能少。
新创建的 struct file 的 private_data,指向 struct shm_file_data,这样内存映射那部分的数据结构,就能够通过它来访问内存文件了。
新创建的 struct file 的 file_operations 也发生了变化,变成了 shm_file_operations。
接下来,do_mmap_pgoff 函数我们遇到过,原来映射硬盘上的文件的时候,也是调用它。这里我们不再详细解析了。它会分配一个 vm_area_struct 指向虚拟地址空间中没有分配的区域,它的 vm_file 指向这个内存文件,然后它会调用 shm_file_operations 的 mmap 函数,也即 shm_mmap 进行映射。
shm_mmap 中调用了 shm_file_data 中的 file 的 mmap 函数,这次调用的是 shmem_file_operations 的 mmap,也即 shmem_mmap。
这里面,vm_area_struct 的 vm_ops 指向 shmem_vm_ops。等从 call_mmap 中返回之后,shm_file_data 的 vm_ops 指向了 shmem_vm_ops,而 vm_area_struct 的 vm_ops 改为指向 shm_vm_ops。
我们来看一下,shm_vm_ops 和 shmem_vm_ops 的定义。
它们里面最关键的就是 fault 函数,也即访问虚拟内存的时候,访问不到应该怎么办。
当访问不到的时候,先调用 vm_area_struct 的 vm_ops,也即 shm_vm_ops 的 fault 函数 shm_fault。然后它会转而调用 shm_file_data 的 vm_ops,也即 shmem_vm_ops 的 fault 函数 shmem_fault。
虽然基于内存的文件系统,已经为这个内存文件分配了 inode,但是内存也却是一点儿都没分配,只有在发生缺页异常的时候才进行分配。
shmem_fault 会调用 shmem_getpage_gfp 在 page cache 和 swap 中找一个空闲页,如果找不到就通过 shmem_alloc_and_acct_page 分配一个新的页,他最终会调用内存管理系统的 alloc_page_vma 在物理内存中分配一个页。
至此,共享内存才真的映射到了虚拟地址空间中,进程可以像访问本地内存一样访问共享内存。
总结时刻
我们来总结一下共享内存的创建和映射过程。
调用 shmget 创建共享内存。
先通过 ipc_findkey 在基数树中查找 key 对应的共享内存对象 shmid_kernel 是否已经被创建过,如果已经被创建,就会被查询出来,例如 producer 创建过,在 consumer 中就会查询出来。
如果共享内存没有被创建过,则调用 shm_ops 的 newseg 方法,创建一个共享内存对象 shmid_kernel。例如,在 producer 中就会新建。
在 shmem 文件系统里面创建一个文件,共享内存对象 shmid_kernel 指向这个文件,这个文件用 struct file 表示,我们姑且称它为 file1。
调用 shmat,将共享内存映射到虚拟地址空间。
shm_obtain_object_check 先从基数树里面找到 shmid_kernel 对象。
创建用于内存映射到文件的 file 和 shm_file_data,这里的 struct file 我们姑且称为 file2。
关联内存区域 vm_area_struct 和用于内存映射到文件的 file,也即 file2,调用 file2 的 mmap 函数。
file2 的 mmap 函数 shm_mmap,会调用 file1 的 mmap 函数 shmem_mmap,设置 shm_file_data 和 vm_area_struct 的 vm_ops。
内存映射完毕之后,其实并没有真的分配物理内存,当访问内存的时候,会触发缺页异常 do_page_fault。
vm_area_struct 的 vm_ops 的 shm_fault 会调用 shm_file_data 的 vm_ops 的 shmem_fault。
在 page cache 中找一个空闲页,或者创建一个空闲页。
课堂练习
在这里,我们只分析了 shm_ids 的结构,消息队列的程序我们写过了,但是 msg_ids 的结构没有解析,你可以试着解析一下。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 6
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
40 | IPC(上):不同项目组之间抢资源,如何协调?
下一篇
42 | IPC(下):不同项目组之间抢资源,如何协调?
精选留言(14)
- Spring2019-09-27文章一遍看不懂但底下总结的图很好,终于明白了为什么需要两个file。file1是shmem内存文件系统里的文件,file2是进程虚拟内存里映射的文件,所以file1是属于共享内存的,file2是属于某个进程的。共 1 条评论17
- 小橙子2019-11-06工作了几年 ,业务代码写多了,框架与API调来调去的,遇到很多疑难杂症,还是不明所以。 回过头再看下 操作系统真是核心,很多人说操作系统就是功夫里面的易筋经,内功章法。学习了操作系统,再看很多其他的技术,感觉更自然,理解的更深刻了。一直想读内核代码,但是啃起来很费劲,这个专栏一直再看,越看越喜欢,很多篇章都会反复的看。相信看完专栏后,再去看一些深入理解linux内核,会清晰很多。共 1 条评论11
- book尾汁2020-04-27共享内存: 创建共享内存,通过shmget系统调用来创建一个共享内存,主要是通过key来创建一个struct kern_ipc_perm,信号量 队列和共享内存的结构都是一样的,可以通过强制类型转换来转化成任意一个类型.然后接下来去填充这个结构,然后将一个文件与共享内存关联,内存映射有两种方式一种是匿名映射 一种是映射到一个文件,物理内存是不能共享的,文件可以跨进程共享,所以要映射到文件.但这里的文件是一个特殊的文件系统内存文件系统上的文件,这个内存文件系统也会在系统初始化的时候注册 挂载, 该文件也有目录项和inode以及其fs_operation,然后会将新创建的共享内存对象加入到shm_ids基数上,并将其加到当前进程的sysvshm队列中. 现在共享内存其实还只是内核中的一个结构体,我们只有共享内存的id,要想使用共享内存还要通过id来将其映射到使用者的用户态的进程空间中,通过shmat系统调用可以做到这一点,该系统调用的主要工作如下: 1 通过共享内存的ip在 shm_ids基数树上找到该共享内存的结构体,然后取出内存文件系统里file并将其赋值给新创建的struct shm_file_data->file,这里我们已经有了可以共享的文件"file",然后在用户进程虚拟空间的mmap映射区分配一个vm_area struct来做文件映射就可以了,将vm_area_struct里的vm_file的private_data指向shm_file_data,为什么不能直接用file呢?private_data貌似只有共享内存才有用,不太理解,可能因为vm_file有其独特的file_operation的问题吧,两个file'虽然可以是同一类结构体,但差异还是很大.在创建vm_file的过程中应该可以找到答案,略过,映射内存时还会将vm_area的vm_ops先指向shmem_vm_ops,然后在指向shm_vm_ops, 将shm_file_data的vm_ops指向shmem_vm_ops即内存文件系统的文件的vm_ops,到这里就完成了.后面进程读或写数据时,如果对应的页表项没有建立会触发缺页异常,跟之前的缺页异常流程差不多,最终会调用内存文件系统的缺页异常函数来分配对应的物理页,并建立页表项.展开
作者回复: 赞
共 3 条评论5 - 珠闪闪2020-03-08文章两遍读下来蒙蒙的,最后对着总结图把共享内存的创建和在用户态映射的流程理顺了。关键就是因为共享内存刚开始申请的物理内存无法在进程中共享,所以先要把物理内存的shmid_kernel对应到shmem文件系统的一个文件,这样shmem中的文件可以再进程中共享;然后在shmat函数时,相当于将shm映射到shmem的file2,先映射到shmem文件系统的文件file1,然后再通过file1的mmap函数完成shm_file_data和vm_area_struct的ops设置。这样内存映射完毕后,并没有真的分配物理内存,当访问内存会触发缺页异常。然后vm_area_struct的vm_ops的shm_fault会调用shm_file_data的vm_ops的shmem_fault。最后在page cache中找一个空闲页,或者创建一个空闲页。展开共 1 条评论6
- book尾汁2020-04-28补充下该查了下file是VFS框架的一个基本概念,一切皆文件指的就是这个,f,然后在这个file下面会有各种各样的实现,比如设备是文件 sock是文件 pipe是文件 共享内存也是文件,file结构体里面都是一些通用的属性,而private_data里面是一些各个披着文件外衣的各种结构体的一个独特的东西,因此这里会有两个file,vm_file就是这个外壳,其private_data里就是共享内存的相关数据
作者回复: 对的,
5 - Amark2019-07-02老师有没有什么通俗易懂的资料,您将的太专业了
作者回复: 的确比较硬核,我实在是没办法再比喻了
2 - NeverSeeYouAgainBUG2022-06-12哎呀超哥,深入浅出啊深入浅出啊。关键在 浅出,要好好总结,
- 小鳄鱼2022-06-02为了统一操作入口:一切皆文件。构建了各种各样的“文件系统”。共享内存文件系统,硬盘文件系统(ext4等),设备文件系统,相信还有各种各样的文件系统!虽然感觉复杂了,但是实际的场景本来就不简单。反而统一入口之后,“上层建筑”的开发人员不再需要关系底层的具体实现,从而实现并行开发,独立维护。
- Geek_2b44d42022-05-09这个page cache 跟swap的选择时机是怎样的?
- 艾瑞克小霸王2019-12-06对于 sem_ids、msg_ids、shm_ids 各有一棵基数树 --------------------------------------------------- 应该是共享一个树吧?
- Leosocy2019-11-14seq 和 next_id 用于一起生成 ipc 唯一的 id,因为信号量,共享内存,消息队列,它们三个的 id 也不能重复 这句话不太明白,不同的ipc_ids不是有不同的idr吗?为什么要保证他们三个id不重复?
- 奔跑的码仔2019-09-30将本节所讲的共享内存实现流程与文件内存映那节所讲的流程对比着梳理一下,感觉明朗了好多
- 嘉木2019-08-13C的面向对象居然这么巧妙
- 不一样的烟火2019-07-01听完了 快点更新😁
作者回复: 牛