操作系统管理System V标准中三种资源的方式¶
约 1609 个字 244 行代码 预计阅读时间 8 分钟
前面介绍了四种进程间通信的方式,其中共享内存、消息队列和信号量属于System V标准的通信方式,在使用这三种进程间通信方式时可以发现其中的接口都比较类似,如下表所示:
| 操作\通信方式 | 共享内存 | 消息队列 | 信号量 |
|---|---|---|---|
| 申请资源 | shmget | msgget | semget |
| 操作资源 | 常规读写操作 | msgsnd和msgrcv | semop |
| 释放资源 | shmctl | msgctl | semctl |
从应用层了解三种通信方式的属性¶
因为遵循着同一个标准,所以三者的操作都大差不差,所以操作系统底层为了更方便管理这三种资源,就考虑对这三种资源进行统一管理,分析如下:
首先查看共享内存的相关定义:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 | |
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 | |
接着查看消息队列的相关定义:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 | |
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
最后查看信号量的相关定义:
| C | |
|---|---|
1 2 3 4 5 6 7 | |
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
从上面的三部分代码可以看出,不论是共享内存、消息队列,还是信号量,三者的第一个成员都是struct ipc_perm结构,这个结构用于存放当前资源的相关权限已经标识符,例如其中的__key表示资源在系统中的编号,另外还有对应的uid表示持有当前资源的用户,三者除了有struct ipc_perm结构外,还有一些其他成员也基本类似,共同组成了指定的资源
其他属性暂时不考虑,主要看每一种资源的第一个成员为什么都是struct ipc_perm,实际上这就是操作系统将三种资源都看成一种资源来管理的原因
通过代码获取对应的属性如下:
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 | |
从内核层了解三者通信方式管理的本质¶
操作系统为了管理三种通信方式的资源,会通过一个数据结构进行管理,在Linux 2.6版本的内核中结构如下:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
通过上面的结构,操作系统创建出三个对象分别为:
| C | |
|---|---|
1 | |
| C | |
|---|---|
1 | |
| C | |
|---|---|
1 | |
上面三个结构对象中都存在着一个成员:
| C | |
|---|---|
1 | |
该成员具体实现如下:
| C | |
|---|---|
1 2 3 | |
对应的struct kern_ipc_perm结构如下:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
这个结构中的struct kern_ipc_perm实际上就是应用层的struct ipc_perm,从每一个资源的结构也可以得出这个结论:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 | |
因为每一个struct ipc_ids对象都有一个struct kern_ipc_perm成员,这个指针指向着一个动态开辟的空间,现在假设有三个资源的对象如下:
| C | |
|---|---|
1 2 3 4 5 6 | |
操作系统获取到这三个对象就可以访问该对象对应的资源,即结构体中相关的成员。在C语言中,结构体的地址有一个特性:第一个成员的地址就是结构体的地址,此时操作系统就可以通过对上面三个对象进行强制类型转换使其成为struct kern_ipc_perm *p中的元素,例如:
| C | |
|---|---|
1 2 3 4 5 6 | |
此时操作系统就可以通过管理struct kern_ipc_perm *p统一对三个资源进行管理,而因为struct kern_ipc_perm中含有相关的成员,例如key,所以这个key实际上是一个三个资源共有的成员,也就是说三个资源都需要同一种key
如果此时需要通过struct kern_ipc_perm *p访问指定成员就可以再次通过强制转换为具体的资源访问对应资源中的其他成员:
| C | |
|---|---|
1 2 3 4 5 6 | |
实际上,上面的过程就是多态的基本原理,其中struct kern_ipc_perm就是基类(父类),struct shmid_kernel、struct msg_queue和struct sem_array就是派生类(子类)
再谈共享内存¶
在共享内存的结构中,存在一个特殊的成员:
| C | |
|---|---|
1 2 3 4 5 6 | |
这个结构在文件部分也提到过,但是为什么作为内存也会存在这个结构,本质就是以为共享内存本质也是一个打开的文件,但是它并没有使用到文件的相关接口,这也是为什么他比管道的速度快,既然如此,其就需要将对应的地址映射到进程的虚拟地址空间,在进程地址空间结构的vm_area_struct中也存在一个结构如下:
| C | |
|---|---|
1 2 3 4 5 | |
通过这个指针,就可以将共享内存这个文件映射到进程地址空间,而进程要访问就需要对应的起始地址和终止地址,即:
| C | |
|---|---|
1 2 3 4 5 6 | |
这就是为什么共享内存可以直接使用常规的读写操作而不需要使用文件接口的原因
同样,如果需要将用户打开的文件映射到进程地址空间,可以使用mmap接口:
| C | |
|---|---|
1 | |
接口中的第一个参数表示映射的地址,如果传递NULL表示让系统自动选择映射地址,第二个参数表示文件的大小,第三个参数表示文件的权限,第四个参数表示映射文件的类型,第五个参数为文件描述符,第六个参数为文件内容的偏移量。该接口返回映射的地址,在取消映射时需要使用
对于第二个参数来说,其获取的方式有下面三种:
- 通过
struct stat结构中的st_size属性确定 - 通过
lseek获取偏移量确定 - 通过
fseek及ftell确定
struct stat结构定义如下:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
后两种方式获取文件大小的代码如下:
| C | |
|---|---|
1 | |
| C | |
|---|---|
1 2 | |
需要注意,如果文件已经映射到了进程的地址空间,但是文件在取消映射之前已经关闭,此时不会自动取消映射,所以关闭文件后还需要手动解除映射,可以使用munmap接口取消映射:
| C | |
|---|---|
1 | |
接口第一个参数传递映射的地址,第二个参数传递文件的大小
例如下面使用mmap进行文件映射的示例代码:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |