在前面的章节中介绍了两种允许无关进程共享内存区域以便执行 IPC 的技术: System V共享内存(第 48 章)和共享文件映射(49.4.2 节)。这两种技术都存在一些不足。
1.System V 共享内存模型使用的是键和标识符,这与标准的 UNIX I/O 模型使用文件名和描述符的做法是不一致的。这种差异意味着使用 System V 共享内存段需要一整套全新的系统调用和命令。
2.使用一个共享文件映射来进行 IPC 要求创建一个磁盘文件,即使无需对共享区域进行持久存储也需要这样做。除了因需要创建文件所带来的不便之外,这种技术还会带来一些文件 I/O 开销。
由于存在这些不足,所以 POSIX.1b 定义了一组新的共享内存 API: POSIX 共享内存,这也是本章的主题。
54.1 概述
POSIX 共享内存能够让无关进程共享一个映射区域而无需创建一个相应的映射文件。
Linux 从内核 2.4 起开始支持 POSIX 共享内存。
SUSv3 并没有对 POSIX 共享内存的实现细节进行规定,特别是没有要求使用一个(真实或虚拟)文件系统来标识共享内存对象,但很多 UNIX 实现都采用了文件系统来标识共享内存对象。一些 UNIX 实现将共享对象名创建为标准文件系统上一个特殊位置处的文件。
Linux 使用挂载于/dev/shm 目录下的专用 tmpfs 文件系统(14.10 节)。这个文件系统具有内核持久性——它所包含的共享内存对象会一直持久,即使当前不存在任何进程打开它,但这些对象会在系统关闭之后丢失。
要使用 POSIX 共享内存对象需要完成下列任务。
1.使用 shm_open()函数打开一个与指定的名字对应的对象。 shm_open()函数与 open()系统调用类似,它会创建一个新共享对象或打开一个既有对象。作为函数结果, shm_open()会返回一个引用该对象的文件描述符。
2.将上一步中获得的文件描述符传入 mmap()调用并在其 flags 参数中指定 MAP_SHARED。 这会将共享内存对象映射进进程的虚拟地址空间。与 mmap()的其他用法一样,一旦映射了对象之后就能够关闭该文件描述符而不会影响到这个映射。然而,有可能需要将这个文件描述符保持在打开状态以便后续的fstat()和ftruncate()调用使用这个文件描述符(参见54.2节)。
由于共享内存对象的引用是通过文件描述符来完成的,因此可以直接使用 UNIX 系统中已经定义好的各种文件描述符系统调用(如 ftruncate())而无需增加新的用途特殊的系统调用(System V 共享内存就需要这样做)。
54.2 创建共享内存对象
shm_open()函数创建和打开一个新的共享内存对象或打开一个既有对象。 传入 shm_open()的参数与传入 open()的参数类似。
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/mman.h>
int shm_open(const char *name,int oflag,mode_t mode);/*返回文件描述符*/
name 参数标识出了待创建或待打开的共享内存对象。 oflag 参数是一个改变调用行为的位掩码,表 54-1 对这个参数的取值进行了总结。 表 54-1: shm_open() oflag 参数的位值
标 记 | 描 述 |
---|---|
O_CREAT O_EXCL | 对象不存在时创建对象 与 O_CREAT 互斥地创建对象 |
O_RDONLY O_RDWR | 打开只读访问 打开读写访问 |
O_TRUNC | 将对象长度截断为零 |
oflag 参数的用途之一是确定是打开一个既有的共享内存对象还是创建并打开一个新对象。如果 oflag 中不包含 O_CREAT,那么就打开一个既有对象。如果指定了 O_CREAT,那么在对象不存在时就创建对象。同时指定 O_EXCL 和 O_CREAT 能够确保调用者是对象的创建者,如果对象已经存在,那么就返回一个错误(EEXIST)。 oflag 参数还表明了调用进程在共享内存对象上的访问模式,其取值为 O_RDONLY 或O_RDWR。剩下的标记值 O_TRUNC 会导致在成功打开一个既有共享内存对象之后将对象的长度截断为零。
在一个新共享内存对象被创建时,其所有权和组所有权将根据调用 shm_open()的进程的有效用户和组 ID 来设定,对象权限将会根据 mode 参数中设置的掩码值来设定。 mode 参数能取的位值与文件上的权限位值是一样的(表 15-4)。与 open()系统调用一样, mode 中的权限掩码将会根据进程的 umask(15.4.6 节)来取值。与 open()不同的是,在调用 shm_open()时总是需要 mode 参数,在不创建新对象时需要将这个参数值指定为 0。
shm_open()返回的文件描述符会设置 close-on-exec 标记(FD_CLOEXEC, 27.4 节),因此当程序执行了一个 exec()时文件描述符会被自动关闭。(这与在执行 exec()时映射会被解除的事实是一致的。)
一个新共享内存对象被创建时其初始长度会被设置为 0。 这意味着在创建完一个新共享内存对象之后通常在调用 mmap()之前需要调用 ftruncate()(5.8 节)来设置对象的大小。在调用完 mmap()之后可能还需要使用 ftruncate()来根据需求扩大或收缩共享内存对象, 但需要记住在49.4.3 节讨论过的各个要点。
在扩展一个共享内存对象时,新增加的字节会自动被初始化为 0。在任何时候都可以在 shm_open()返回的文件描述符上使用 fstat() (15.1 节)以获取一个 stat结构,该结构的字段会包含与这个共享内存对象相关的信息,包括其大小( st_size)、权限(st_mode)、所有者(st_uid)以及组(st_gid)。(这些字段是 SUSv3 唯一要求 fstat()在 stat 结构中设置的字段,但 Linux 还会在时间字段中返回有意义的信息, 并且会在剩下的字段中返回 各种用处稍小一点的信息。)
使用 fchmod()和 fchown()能够分别修改共享内存对象的权限和所有权
54.4 删除共享内存对象
SUSv3 要求 POSIX 共享内存对象至少具备内核持久性,即它们会持续存在直到被显式删除或系统重启。当不再需要一个共享内存对象时就应该使用 shm_unlink()删除它。
#include<sys/stat.h>
int shm_unlink(const char *name);
shm_unlink()函数会删除通过 name 指定的共享内存对象。删除一个共享内存对象不会影响对象的既有映射(它会保持有效直到相应的进程调用 munmap()或终止),但会阻止后续的shm_open()调用打开这个对象。一旦所有进程都解除映射这个对象,对象就会被删除,其中的内容会丢失。 程序清单 54-4 中的程序使用 shm_unlink()来删除通过程序的命令行参数指定的共享内存对象
54.5 共享内存 API 比较
到现在为止已经考虑了几种不同的在无关进程间共享内存区域的技术。
1.System V 共享内存(第 48 章)。
2.共享文件映射(49.4.2 节)。
3.POSIX 共享内存对象(本章的主题)。
下列要点适用于所有这些技术。
共同点
1.它们提供了快速 IPC,应用程序通常必须要使用一个信号量(或其他同步原语)来同步对共享区域的访问。
2.一旦共享内存对象区域被映射进进程的虚拟地址空间之后,它就与进程的内存空间中的其他部分无异了。
3.系统会以类似的方式将共享内存区域放置进进程的虚拟地址空间中。 在 48.5 节中介绍System V 共享内存的时候对这种放置进行了概括。 Linux 特有的/proc/PID/maps 文件会列出与所有种类的共享内存区域相关的信息。
4.假设不会将一个共享内存区域映射到一个固定的地址处,那么就需要确保所有对区域中的位置的引用会使用偏移量来表示,而不是使用指针来表示,这是因为这个区域在不同进程中所处的虚拟地址可能是不同的(48.6 节)。
5.在第 50 章中介绍的操作虚拟内存区域的函数可被应用于使用这些技术中任意一项技术创建的共享内存区域。
不同点
在这些共享内存技术之间还存在一些显著的差异。
1.一个共享文件映射的内容会与底层映射文件同步意味着存储在共享内存区域中的数据能够在系统重启之间得到持久保存。
2.System V 和 POSIX 共享内存使用了不同的机制来标识和引用共享内存对象。System V使用了其自己的键和标识符模型,它们与标准的 UNIX I/O 模型是不匹配的并且需要单独的系统调用(如 shmctl())和命令(ipcs 和 ipcrm)。与之形成对比的是, POSIX共享内存使用了名字和文件描述符,其结果是使用各种既有的 UNIX 系统调用(如fstat()和 fchmod())就能够查看和操作共享内存对象了。
3.System V 共享内存段的大小在创建时( shmget())就确定了。与之形成对比的是,在基于文件的映射和 POSIX 共享内存对象上可以使用 ftruncate()来调整底层对象的大小,然后使用 munmap()和 mmap()(或 Linux 特有的 mremap())重建映射。
4.因为历史原因, System V 共享内存受支持程度比 mmap()和 POSIX 共享内存对象广得多,尽管现在大多数 UNIX 实现都已经提供所有这些技术。
除了最后有关可移植性的一点之外,上面列出的差异都是共享文件映射和 POSIX 共享内存对象的优势。因此在新应用程序中应该优先从这些接口中挑选一个使用,而不是 System V共享内存。至于选择哪个接口则取决于是否需要一个持久性存储。共享文件映射提供了持久性存储,而 POSIX 共享内存对象则避免了在无需持久存储时使用磁盘文件所产生的开销
54.6 总结
POSIX 共享内存对象用来在无关进程间共享一块内存区域而无需创建一个底层的磁盘文件。 为创建 POSIX 共享内存对象需要使用 shm_open()调用来替换通常在 mmap()调用之前调用的 open()。 shm_open()调用会在基于内存的文件系统中创建一个文件,并且可以使用传统的文件描述符系统调用在这个虚拟文件上执行各种操作。特别地,必须要使用 ftruncate()来设置共享内存对象的大小,因为其初始长度为零
现在已经介绍了无关进程间的三种共享内存区域技术: System V 共享内存、共享文件映射以及 POSIX 共享内存对象。这三种技术之间存在很多相似之处,但也存在一些重要的差别,除了可移植性问题外,这些差异都对共享文件映射和 POSIX 共享内存对象有利。