一、System V —— 共享内存(详解)
共享内存区是最快的 IPC 形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说,就是进程不再通过执行进入内核的系统调用来传递彼此的数据。
下面我们还需要了解进程间通信之 System V 标准下的共享内存,前面所讲的管道其实不属于 System V 标准,但是它依旧是操作系统下最原生的通信方式。
System V 标准下有最典型的三种通信方式:共享内存、消息队列、信号量。下面会重点谈谈共享内存,然后简单提一下消息队列,而信号量将放在后面的多线程部分再进行了解。
1、共享内存的原理
正如上面两种管道,进程间通信的第一步一定是先让不同的进程看到同一份资源,然后才是通信的过程。
进程间通信中的大部分内容都是第一步,在这之前要让不同进程看到同一份资源:匿名管道是通过父子共享文件的特征;命名管道是通过文件路径具有唯一性。
其中,这两种管道归根到底所看到的资源都是文件资源。
对于上图中的内容,我们之前已经接触过了。操作系统为了满足通信的需求,需要:
- 在物理内存上申请一块物理内存空间。
- 再把这块空间通过页表映射到共享区(也就是堆栈之间)。
- 最后,将建立映射之后的虚拟地址返回给用户。
操作系统当然可以做到这些操作,因为操作系统是软硬件资源的管理者,这并不难理解,在 C/C++ 中使用 malloc / new 时,实际上就是在堆上申请空间,最后也还是在物理内存中申请,然后再返回地址。以前 malloc / new 是为了让你这个进程私有使用,而现在是为了能够让多个进程看到同一份资源。
此时又有一个进程 B,它和进程 A 没有任何关系,虽然它们是类似的数据结构来表示进程,但是它们的代码和数据是被加载到内存中不同的位置,所以实际上它们是具有很强的独立性。举一个例子:一个全局变量,然后父进程直接打印出它的地址和值,子进程修改内容后再打印出地址和值,最后结果显示二者地址一样,值却不一样。
同样对于进程 B,操作系统也可以向物理内存申请空间,然后再映射到共享区,接着返回给进程。不过,我们要做到的是让不同的进程能够看到同一份资源,所以原理就是操作系统向物理内存申请一块空间,这块空间就叫做共享内存,再把这块空间分别映射到两个进程中 mm_struct 的共享区(这个共享区在基础 IO 中已经说过动态库是被映射到这个区域的,现在就知道了物理内存中申请的共享内存也会被映射到这块区域),然后再返回给进程,那么这两个进程就可以使用各自的虚拟地址、页表访问同一块物理内存,这就是共享内存。上述步骤一定是有对应的系统调用接口帮助我们实现。共享内存的提供者是操作系统。
操作系统内部是提供通信机制的(IPC),也就是其中有一个 ipc 模块。前面讲到操作系统申请一块内存空间,但也得是有人来告诉操作系统自己需要申请,所以本质还是进程申请的。从宏观上来看,操作系统内一定存在大量的共享内存,所有的共享内存都是进程向操作系统申请的,其中操作系统当然要管理这么多的共享内存,那应该怎么管理呢?—— 先描述,再组织(共享内存是给进程使用的,而操作系统为了管理这些共享内存,它也需要申请大量对应的数据结构来维护),共享内存 = 共享内存块 + 对应的共享内存的内核数据结构,所以操作系统对共享内存的管理就变成了对共享内存所对应的数据结构的管理。
综上所述,流程对应如下:
- 申请共享内存。
- 进程 A、B 分别挂接对应的共享内存到自己的地址空间(共享区)。
- 进程双方就能够看到同一份资源,也就可以互相通信了。
- 释放共享内存。
2、 shmget
(1)认识接口
shmget 是系统提供来申请共享内存的一个系统接口。
key 是这个共享内存段的名字。
size 是我们想申请共享内存的大小,理论上来说是可以任意,但还是建议选择 4kb 的倍数(后面会解释原因)。
shmflg 有 IPC_CREAT 和 IPC_EXCL 两个选项。前者是创建共享内存,后者单独使用并没有意义。其次,这里还可以 | 上一个八进制方案,表示这个共享内存的权限。shmflg 由九个权限标志构成,它们的用法和创建文件时使用的 mode 模式标志是一样的
- 若同时设置 IPC_CREAT 和 IPC_EXCL,如果目标共享内存不存在,则创建;否则,出错返回。这样做的意义是如果调用 shmget 成功,那么得到的一定是全新的共享内存,因为如果它失败就出错了。所以一般这两个选项会组合使用,就可以从 0 到 1 的创建一个共享内存。
- 若只设置 IPC_CREAT(同 0)(没有意义),如果目标共享内存不存在,则创建;否则,则获取共享内存。
一定是一个进程设置 IPC_CREAT | IPC_EXCL,另一个进程设置 IPC_CREAT(什么叫做同时设置呢?之前我们就说过标志位用 int 太浪费了,所以这里用的是一个 bit 位来表示一种状态。如果有多个状态需要同时设置就使用 |,这里可以验证一下,结果可以看到这里 define 的是一种 8 进程数据,这里的 1 2 4 就说明用的是一串 01 序列,但只有一个 1,而且 1 的位置不一样,所以 | 就可以获取到多个标志位)。
如果共享内存已经存在了,此时就不应该再进行创建了,而是选择获取。因为如果一个创建好共享内存的进程要与另一个进程通信的话,另一个进程就只能是获得要通信进程对应的共享内存的返回值:如果成功,会返回一个合法的共享内存的标识符,类似之前学的 fd;否则,返回 -1。
它可以通过这个返回值来唯一标识这个共享内存。这个概念有点类似文件描述符,共享内存 ipc 机制也确实与文件系统有关,但 ipc 机制是操作系统另外一个独立的模块,这样的小模块还有很多,之前了解的都是一些宏观上的模块,就比如:进程管理、文件管理、内存管理、驱动管理。
如何保证两个进程看到的是同一块共享内存呢?
每个共享内存都有自己对应的数据结构 struct shm_ipc,此时通过 key 就可以进行唯一区分(像是我们的身份证号码,更多的是强调唯一性)。其中 A 进程创建了共享内存,key 的值是 123,如果 B 进程想要和 A 进程通信,就需要遍历共享内存数据结构中的 key 值。
那么现在的问题就变成了如何保证两进程获得的是同一个 key 值呢?—— ftok。
它和 fork 很像,但是没有任何关系。它的内部不进行任何的系统调用,而是一套算法,ftok 没有任何的系统调用。它只是把第一参数的字符串和第二个参数的整数合起来形成一个唯一的 key 值。它可以按照自己的情况任意填写,但必须要保证通信的两个进程填的 key 值是一样的,这样就能够保证两个进程使用的是同一个规则来形成的 key 值。
(2)代码
A. makefile
makefile 中是可以定义变量的, makefile 中取变量要用 $()。
B. common.h
c. 必须要先保证 server.c 和 client.c 中的 ftok 获取的 key 值是一样的
ftok 本身没有任何的系统调用,key 值就是 ftok 将 PATH_NAME 和 PROJ_ID 组合形成唯一的 key 值。
D. 申请共享内存
a. ipcs
ipcs 命令默认它会查看 Message Queues(消息队列)、Shared Memory Segments(共享内存段)、Semaphore Arrays(信号量数组)相关信息。如果只想查看共享内存,则 ipcs -m,这里我们可以看到好像并没有什么共享内存,sudo 之后也没有,这时我们再打开一个共享内存。
此时 ./server,输出结果之后 server 进程当然退出了,所以它的退出码是 0。
(说明当进程运行结束,共享内存依旧存在)
此时再 ipcs -m 就可以看到 server 进程所申请的共享内存信息了。
正如上面所看到的,server 进程已经结束了,但是它所申请的 ipc 共享内存资源仍然存在。这里表达的是,所有的 Systrem V IPC 资源的生命周期都是随内核,而不随进程。这里有两种方法可以释放共享内存:
- 手动删除:操作系统进行重启或者命令行指令(
ipcrm -m shmid
释放共享内存,规范应该是由所对应的进程来调用系统接口来释放的)。- 代码删除:当进程退出时,用调用释放(有申请共享内存,自然也有释放)。
2、shmctl
(1)认识接口
既然有 shmget 来申请共享内存,那么也必须要有 shmctl 来释放共享内存,shmctl 用于控制共享内存。
- shmid 是 shmget 创建共享内存成功后返回的共享内存标识码 id。
- cmd 是将要采取的动作(有三个可取值),如果想释放共享内存,那么就用 IPC_RMID 选项。
- buf 类似于上面说的共享内存的属性 struct shm_ipc,这很少使用,它指向一个保存着共享内存的模式状态和访问权限的数据结构,设置为 NULL。
(2)代码
这里的现实的 perms 就是共享内存的权限。
3、shmat
至此,我们完成了让进程在物理内存中创建好共享内存,然后释放共享内存,那么接下来还要将进程与共享内存关联。所以刚刚在查看共享内存时,nattch 就是与当前共享内存关联的进程的个数,我们可以看到,这里只是创建了共享内存,还没有任何一个进程与之关联。
(1)认识接口
shmat 是系统提供于共享内存和进程关联的一个系统接口,也就是将共享内存段连接到进程地址空间。
- shmid 是让这个进程和哪个共享内存关联,是一个共享内存标志。
- shmaddr 是一个指定连接地址,要把共享内存挂接到进程的哪个虚拟上,这里要挂接到共享区,我们直接设置为 NULL,操作系统会帮我们选择。
- shmflg 的选项是挂接的方式,我们默认填 0,它的两个可能取值是 SHM_RND 和 SHM_RDONLY。
- shmat 的返回值是 void*,我们之前学的 malloc 的返回值也是 void*,虽然它们的区域不一样,但是原理类似,malloc 成功后返回值就是堆上的一块空间的起始地址,而 shmat 成功就返回关联共享内存段的起始地址。
- shmaddr 为 NULL,核心自动选择一个地址。
- shmaddr 不为 NULL 且 shmflg 无 SHM_RND 标记,则以 shmaddr 为连接地址。
- shmaddr 不为 NULL 且 shmflg 设置了 SHM_RND 标记,则连接的地址会自动向下调整为 SHMLBA 的整数倍。公式:shmaddr - (shmaddr % SHMLBA)。
- shmflg=SHM_RDONLY,表示连接操作用来只读共享内存。
【Linux 系统】进程间通信(共享内存、消息队列、信号量)(下)https://developer.aliyun.com/article/1515667?spm=a2c6h.13148508.setting.19.11104f0e63xoTy