在之前的文章中介绍了允许无关进程共享内存区域以便执行 IPC 的技术:共享文件映射。但他存在一些不足。
- 使用一个共享文件映射来进行 IPC 要求创建一个磁盘文件,即使无需对共享区域进行持久存储也需要这样做。
- 除此之外,这种技术还会带来一些文件 I/O 开销。
由于存在这些不足,所以 POSIX.1b 定义了一组新的共享内存 API:POSIX 共享内存。
1 概述
POSIX 共享内存能够让无关进程共享一个映射区域而无需创建一个相应的映射文件。Linux 从内核 2.4 起开始支持 POSIX 共享内存。
Linux 使用挂载于/dev/shm 目录下的专用 tmpfs 文件系统1。这个文件系统具有内核持久性——它所包含的共享内存对象会一直持久,即使当前不存在任何进程打开它,但这些对象会在系统关闭之后丢失。
要使用 POSIX 共享内存对象需要完成下列任务。
- 使用 shm_open()函数打开一个与指定的名字对应的对象。shm_open()函数与 open()系统调用类似,它会创建一个新共享对象或打开一个既有对象。作为函数结果,shm_open()会返回一个引用该对象的文件描述符。
- 将上一步中获得的文件描述符传入 mmap()调用2并在其 flags 参数中指定 MAP_SHARED。这会将共享内存对象映射进进程的虚拟地址空间。与 mmap()的其他用法一样,一旦映射了对象之后就能够关闭该文件描述符而不会影响到这个映射。然而,有可能需要将这个文件描述符保持在打开状态以便后续的fstat()和ftruncate()调用使用这个文件描述符。
2 创建共享内存对象
shm_open()函数创建和打开一个新的共享内存对象或打开一个既有对象。传入 shm_open()的参数与传入 open()的参数类似。
#include <fcntl.h> /*Defines 0*constants */
#include <sys/stat.h> /* Defines mode constants */
#include<sys/mman.h>int shm_open(const char *name, int oflag, mode_t mode);/*Returns file descriptor on success, or -l on error*/
name 参数标识出了待创建或待打开的共享内存对象。oflag 参数是一个改变调用行为的位掩码,表中对这个参数的取值进行了总结。
oflag 参数的用途之一是确定是打开一个既有的共享内存对象还是创建并打开一个新对象。
- 如果 oflag 中不包含 O_CREAT,那么就打开一个既有对象。
- 如果指定了 O_CREAT,那么在对象不存在时就创建对象。
- 同时指定 O_EXCL 和 O_CREAT 能够确保调用者是对象的创建者,如果对象已经存在,那么就返回一个错误(EEXIST)。
oflag 参数还表明了调用进程在共享内存对象上的访问模式,其取值为 O_RDONLY 或O_RDWR。剩下的标记值 O_TRUNC 会导致在成功打开一个既有共享内存对象之后将对象的长度截断为零。
在一个新共享内存对象被创建时,其所有权和组所有权将根据调用 shm_open()的进程的有效用户和组 ID 来设定,对象权限将会根据 mode 参数中设置的掩码值来设定。mode 参数能取的位值与文件上的权限位值是一样的。
- 与 open()系统调用一样,mode 中的权限掩码将会根据进程的 umask来取值。
- 与 open()不同的是,在调用 shm_open()时总是需要 mode 参数,在不创建新对象时需要将这个参数值指定为 0。
shm_open()返回的文件描述符会设置 close-on-exec 标记(FD_CLOEXEC),因此当程序执行了一个 exec()时文件描述符会被自动关闭。(这与在执行 exec()时映射会被解除的事实是一致的。)
一个新共享内存对象被创建时其初始长度会被设置为 0。这意味着在创建完一个新共享内存对象之后通常在调用 mmap()之前需要调用 ftruncate()来设置对象的大小。在调用完 mmap()之后可能还需要使用 ftruncate()来根据需求扩大或收缩共享内存对象。
在任何时候都可以在 shm_open()返回的文件描述符上使用 fstat()以获取一个 stat结构,该结构的字段会包含与这个共享内存对象相关的信息,包括其大小(st_size)、权限(st_mode)、所有者(st_uid)以及组(st_gid)。使用 fchmod()和 fchown()能够分别修改共享内存对象的权限和所有权。
示例程序
程序提供了一个简单的使用 shm_open()、ftruncate()以及 mmap()的例子。这个程序创建了一个大小通过命令行参数指定的共享内存对象并将该对象映射进进程的虚拟地址空间。这个程序允许使用命令行选项来选择 shm_open()调用使用的标记(O_CREAT 和 O_EXCL)。
下面的例子使用这个程序创建了一个 10000 字节的共享内存对象,然后在/dev/shm 中使用 ls 命令显示出了这个对象。
$ ./pshm_create -c /demo_shm 10000
$ ls -l /dev/shm/
total 0
-rw------- 1 dockdroid dockdroid 10000 May 7 11:03 demo_shm
/* pshm/pshm_create.cCreate a POSIX shared memory object with specified size and permissions.Usage as shown in usageError().创建一个 POSIX 共享内存对象
*/
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include "tlpi_hdr.h"static void
usageError(const char *progName)
{fprintf(stderr, "Usage: %s [-cx] shm-name size [octal-perms]\n", progName);fprintf(stderr, " -c Create shared memory (O_CREAT)\n");fprintf(stderr, " -x Create exclusively (O_EXCL)\n");exit(EXIT_FAILURE);
}int
main(int argc, char *argv[])
{int flags, opt, fd;mode_t perms;size_t size;void *addr;flags = O_RDWR;while ((opt = getopt(argc, argv, "cx")) != -1) {switch (opt) {case 'c': flags |= O_CREAT; break;case 'x': flags |= O_EXCL; break;default: usageError(argv[0]);}}if (optind + 1 >= argc)usageError(argv[0]);size = getLong(argv[optind + 1], GN_ANY_BASE, "size");perms = (argc <= optind + 2) ? (S_IRUSR | S_IWUSR) :getLong(argv[optind + 2], GN_BASE_8, "octal-perms");/* Create shared memory object and set its size */fd = shm_open(argv[optind], flags, perms);if (fd == -1)errExit("shm_open");if (ftruncate(fd, size) == -1)errExit("ftruncate");/* Map shared memory object */if (size > 0) {addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (addr == MAP_FAILED)errExit("mmap");}exit(EXIT_SUCCESS);
}
3 使用共享内存对象
示例程序演示了如何使用一个共享内存对象将数据从一个进程传输到另一个进程中。程序将其第二个命令行参数中包含的字符串复制到了一个名字通过其第一个命令行参数指定的既有共享内存对象中。在映射这个对象和执行复制之前,这个程序使用了 ftruncate()来将共享内存对象的长度设置为与待复制的字符串的长度一样。
/* pshm/pshm_write.cUsage: pshm_write shm-name stringCopy 'string' into the POSIX shared memory object named in 'shm-name'.将数据复制进一个 POSIX 共享内存对象See also pshm_read.c.
*/
#include <fcntl.h>
#include <sys/mman.h>
#include "tlpi_hdr.h"int
main(int argc, char *argv[])
{int fd;size_t len; /* Size of shared memory object */char *addr;if (argc != 3 || strcmp(argv[1], "--help") == 0)usageErr("%s shm-name string\n", argv[0]);fd = shm_open(argv[1], O_RDWR, 0); /* Open existing object */if (fd == -1)errExit("shm_open");len = strlen(argv[2]);if (ftruncate(fd, len) == -1) /* Resize object to hold string */errExit("ftruncate");printf("Resized to %ld bytes\n", (long) len);addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (addr == MAP_FAILED)errExit("mmap");if (close(fd) == -1) /* 'fd' is no longer needed */errExit("close");printf("copying %ld bytes\n", (long) len);memcpy(addr, argv[2], len); /* Copy string to shared memory */exit(EXIT_SUCCESS);
}
下面示例程序中在标准输出上显示了名字通过其命令行参数指定的既有共享内存对象中的字符串。在调用 shm_open()之后,这个程序使用了 fstat()来确定共享内存的大小并在映射该对象的 mmap()调用中和打印这个字符串的 write()调用中使用这个值。
/* pshm_read.cUsage: pshm_read shm-nameCopy the contents of the POSIX shared memory object named in'name' to stdout.See also pshm_write.c.
*/
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include "tlpi_hdr.h"int
main(int argc, char *argv[])
{int fd;char *addr;struct stat sb;if (argc != 2 || strcmp(argv[1], "--help") == 0)usageErr("%s shm-name\n", argv[0]);fd = shm_open(argv[1], O_RDONLY, 0); /* Open existing object */if (fd == -1)errExit("shm_open");/* Use shared memory object size as length argument for mmap()and as number of bytes to write() */if (fstat(fd, &sb) == -1)errExit("fstat");addr = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);if (addr == MAP_FAILED)errExit("mmap");if (close(fd) == -1) /* 'fd' is no longer needed */errExit("close");write(STDOUT_FILENO, addr, sb.st_size);write(STDOUT_FILENO, "\n", 1);exit(EXIT_SUCCESS);
}
下面的 shell 会话演示了如何使用示例程序。首先创建一个长度为零的共享内存对象。
$ ./pshm_create -c /demo_shm 0
$ ls -l /dev/shm/
total 0
-rw------- 1 dockdroid dockdroid 0 May 7 11:27 demo_shm
然后将一个字符串复制进共享内存对象。
$ ./pshm_write /demo_shm "hello"
Resized to 5 bytes
copying 5 bytes
$ ls -l /dev/shm/
total 4
-rw------- 1 dockdroid dockdroid 5 May 7 11:28 demo_shm
从上面的输出中可以看出这个程序重新设定了共享内存对象的大小使之具备足够的空间来存储指定的字符串。
最后显示共享内存对象中的字符串。
$ ./pshm_read /demo_shm
hello
应用程序通常需要使用一些同步技术来让进程协调它们对共享内存的访问。在这里给出的示例 shell 会话中,这种协调是通过用户一个一个运行这些程序来完成的。通常,应用程序会使用一种同步原语(如信号量)来协调对共享内存对象的访问。
4 删除共享内存对象
POSIX 共享内存对象具备内核持久性,即它们会持续存在直到被显式删除或系统重启。当不再需要一个共享内存对象时就应该使用 shm_unlink()删除它。
#include <sys/mman.h>
int shm unlink(const char *name);
shm_unlink()函数会删除通过 name 指定的共享内存对象。删除一个共享内存对象不会影响对象的既有映射(它会保持有效直到相应的进程调用 munmap()或终止),但会阻止后续的shm_open()调用打开这个对象。一旦所有进程都解除映射这个对象,对象就会被删除,其中的内容会丢失。
示例程序使用 shm_unlink()来删除通过程序的命令行参数指定的共享内存对象。
/* pshm_unlink.cUsage: pshm_unlink shm-nameRemove the POSIX shared memory object identified by 'name'使用 shm_unlink()来断开链接一个 POSIX 共享内存对象
*/
#include <fcntl.h>
#include <sys/mman.h>
#include "tlpi_hdr.h"int
main(int argc, char *argv[])
{if (argc != 2 || strcmp(argv[1], "--help") == 0)usageErr("%s shm-name\n", argv[0]);if (shm_unlink(argv[1]) == -1)errExit("shm_unlink");exit(EXIT_SUCCESS);
}
5 共享内存 API 比较
到现在为止已经了解了两种不同的在无关进程间共享内存区域的技术。
- 共享文件映射。
- POSIX 共享内存对象。
下列要点适用于所有这些技术。
- 它们提供了快速 IPC,应用程序通常必须要使用一个信号量(或其他同步原语)来同步对共享区域的访问。
- 一旦共享内存对象区域被映射进进程的虚拟地址空间之后,它就与进程的内存空间中的其他部分无异了。
- 系统会以类似的方式将共享内存区域放置进进程的虚拟地址空间中。Linux 特有的/proc/PID/maps 文件会列出与所有种类的共享内存区域相关的信息。
- 假设不会将一个共享内存区域映射到一个固定的地址处,那么就需要确保所有对区域中的位置的引用会使用偏移量来表示,而不是使用指针来表示,这是因为这个区域在不同进程中所处的虚拟地址可能是不同的。
- 操作虚拟内存区域的函数可被应用于使用这些技术中任意一项技术创建的共享内存区域。
系统上 POSIX 共享内存区域占据的内存总量受限于底层的 tmpfs 文件系统的大小。这个文件系统通常会在启动时使用默认大小(如 256MB)进行挂载。如果有必要的话,超级用户能够通过使用命令 mount –o remount,size=重新挂载这个文件系统来修改它的大小。 ↩︎
使用 POSIX 共享内存对象需要两步式过程(shm_open()加上 mmap())而没有使用单个函数来执行两项任务是因为历史原因。在 POSIX 委员会增加这个特性时,mmap()调用已经存在了。实际上,这里所需要做的事情是使用shm_open()调用替换open()调用,其中的差别是使用 shm_open()无需在一个基于磁盘的文件系统上创建一个文件。 ↩︎