引入
System V 共享内存和共享文件映射允许无关进程共享内存区域以便执行IPC技术,这两种技术都存在一些不足
- System V共享内存模型使用的是key和标识符,这与标准的Unix IO模型使用文件名和描述符的做法是不一致的。这种差异意味着使用System V共享内存段需要一整套系统调用和命名
- 使用一个共享文件映射来进行IPC要求创建一个磁盘文件,即使无需对共享区域进行持久存储也需要这样做。除了因为需要创建文件所带来的的不便之外,这种技术还会带来一些文件 I/O 开销
由于存在这些不足,所以 POSIX.1b 定义了一组新的共享内存 API:POSIX 共享内存
概述
POSIX共享内存能够让无关进程共享一个映射区域而无需创建一个相应的映射文件。Linux 从内核2.4开始支持POSIX共享内存。
要使用 POSIX 共享内存对象需要完成下列任务。
- 使用shm_open()函数打开一个与指定的名字对应的对象。shm_open()函数与 open()系统调用类似,它会创建一个新共享对象或打开一个既有对象。作为函数结果,shm_open()会返回一个引用该对象的文件描述符
- 讲上一步中获得的文件描述符传入mmap()调用并在其flags参数中指定MAP_SHARED。这将共享内存对象映射进进程的虚拟地址空间。与mmap()一样,一旦映射了对象之后就能够关闭该文件描述符而不会影响到这个映射。然而,有可能需要将这个文件描述符保持在打开状态以便后续的fstat()和ftruncate()调用使用这个文件描述符
POSIX 共享内存上 shm_open()和 mmap()的关系类似于 System V 共享内存上 shmget()和shmat()的关系。
由于共享内存对象的引用是通过文件描述符来完成的,因此可以直接使用Unix系统中已经定义好的各种文件描述符系统调用(如 ftruncate())而无需增加新的用途特殊的系统调用(System V 共享内存就需要这样做)
创建共享内存对象
shm_open()创建和打开一个新共享内存对象或者打开一个既有对象。传入shm_open()的参数与传入open()的参数类似
NAME
shm_open, - create/open POSIX shared memory objects
SYNOPSIS
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
int shm_open(const char *name, int oflag, mode_t mode);
Link with -lrt.
name 参数标识出了待创建或待打开的共享内存对象。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 会导致在成功打开一个既有共享内存对象之后将对象的长度截断为零。
在 Linux 上,截断在只读打开时也会发生。但 SUSv3 声称使用 O_TRUNC 进行一个只读打开操作的结果是未定义的,因此在这种情况下无法可移植地依赖于某个特定的行为。
对象权限将会根据 mode 参数中设置的掩码值来设定。在调用 shm_open()时总是需要 mode 参数,在不创建新对象时需要将这个参数值指定为 0。
shm_open()返回的文件描述符会设置chose-on-exec标记,因此当程序执行了一个exec()时文件描述符会被自动关闭。即在执行 exec()时映射会被解除
一个新共享内存对象被创建时其初始长度会被设置为0。这意味着在创建完一个新共享内存对象之后通常在调用mmap()之前需要调用ftruncate()来设置对象的大小。在调用mmap()之后可能还需要使用ftruncate()来根据需求扩大或者缩小共享内存对象。
在扩展一个共享内存对象时,新增加的字节会被初始化为0
在任何时候都可以在shm_open()返回的文件描述符上使用fstat()获取一个stat结构,这个结构中包含这个共享对象相关的信息,包括其大小(st_size)、权限(st_mode)、所有者(st_uid)以及组(st_gid)。
使用 fchmod()和 fchown()能够分别修改共享内存对象的权限和所有权。
下面程序创建了一个大小通过命令行参数指定的共享内存对象并将该对象映射进进程的虚拟地址空间。(映射这一步是多余的,因为实际上不会对共享内存做任何操作,这里仅仅是为了演示
如何使用 mmap()。)
// pshm_create
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.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){
printf("%s",argv[0]);
exit(EXIT_FAILURE);
}
size = atoi(argv[optind + 1]);
perms = (argc <= optind + 2) ? (S_IRUSR | S_IWUSR) :
atoi(argv[optind + 2]);
/* Create shared memory object and set its size */
fd = shm_open(argv[optind], flags, perms);
if (fd == -1){
printf("shm_open");
exit(EXIT_FAILURE);
}
if (ftruncate(fd, size) == -1){
printf("ftruncate");
exit(EXIT_FAILURE);
}
/* Map shared memory object */
addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED){
printf("mmap");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
使用:先创建一个10000字节的共享内存对象,然后在/dev/shm中使用ls命令显示出这个对象
使用共享内存对象
下面两个程序演示了如何使用一个共享内存对象将数据从一个进程传输到另一个进程中
- 下面程序将数据复制进一个POSIX共享内存对象中,在映射这个对象和执行复制之前,这个程序使用了ftruncate()来将共享内存对象的长度设置为待复制字符串的长度
// pshm_write
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)
{
printf("%s shm-name string\n", argv[0]);
exit(EXIT_FAILURE);
}
fd = shm_open(argv[1], O_RDWR, 0); /* Open existing object */
if (fd == -1)
{
printf("shm_open");
exit(EXIT_FAILURE);
}
len = strlen(argv[2]);
if (ftruncate(fd, len) == -1) /* Resize object to hold string */
{
printf("ftruncate");
exit(EXIT_FAILURE);
}
printf("Resized to %ld bytes\n", (long) len);
/*FIXME: above: should use %zu here, and remove (long) cast */
addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED){
printf("mmap");
exit(EXIT_FAILURE);
}
if (close(fd) == -1) /* 'fd' is no longer needed */
{
printf("close");
exit(EXIT_FAILURE);
}
printf("copying %ld bytes\n", (long) len);
/*FIXME: above: should use %zu here, and remove (long) cast */
memcpy(addr, argv[2], len); /* Copy string to shared memory */
exit(EXIT_SUCCESS);
}
- 下面程序从一个 POSIX 共享内存对象中复制数据 :在调用 shm_open()之后,这个程序使用了 fstat()来确定共享内存的大小并在映射该对象的 mmap()调用中和打印这个字符串的 write()调用中使用这个值。
// pshm_read
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
int fd;
char *addr;
struct stat sb;
if (argc != 2 || strcmp(argv[1], "--help") == 0)
{
printf("%s shm-name\n", argv[0]);
exit(EXIT_FAILURE);
}
fd = shm_open(argv[1], O_RDONLY, 0); /* Open existing object */
if (fd == -1)
{
printf("shm_open");
exit(EXIT_FAILURE);
}
/* Use shared memory object size as length argument for mmap()
and as number of bytes to write() */
if (fstat(fd, &sb) == -1)
{
printf("fstat");
exit(EXIT_FAILURE);
}
addr = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED)
{
printf("mmap");
exit(EXIT_FAILURE);
}
if (close(fd) == -1) /* 'fd' is no longer needed */
{
printf("close");
exit(EXIT_FAILURE);
}
write(STDOUT_FILENO, addr, sb.st_size);
write(STDOUT_FILENO, "\n", 1);
exit(EXIT_SUCCESS);
}
使用: 首先创建了一个长度为零的共享内存对象
将一个字符串复制进共享内存对象
从上面的输出可以看到这个程序重新设定了共享内存对象的大小并使之具备足够的空间来存储指定的字符串
删除共享内存对象
SUSv3要求POSIX共享内存对象至少具备内核持久性,即它们会持续存在直到被显示删除或者系统重新。当不再需要一个共享内存对象就应该使用shm_unlink()删除它
NAME
shm_unlink -unlink POSIX shared memory objects
SYNOPSIS
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
int shm_unlink(const char *name);
Link with -lrt.
shm_unlink()函数会删除通过 name 指定的共享内存对象。删除一个共享内存对象不会影响对象的既有映射(它会保持有效直到相应的进程调用 munmap()或终止),但会阻止后续的shm_open()调用打开这个对象。一旦所有进程都解除映射这个对象,对象就会被删除,其中的内容会丢失。
共享内存 API 比较
对于这些无关进程间(System V 共享内存、共享文件映射、POSIX共享内存映射)共享内存区域技术,其共同点:
- 它们提供了快速IPC,应用程序通常必须要使用一个信号量(或者其他同步原语)来同步对共享区域的访问
- 一旦共享内存对象被映射进进程的虚拟地址空间后,它就与进程的内存空间中的其他部分无异了
- 系统会以类似的方式将共享内存区域防止进进程的虚拟地址去空间。Linux 特有的/proc/PID/maps 文件会列出与所有种类的共享内存区域相关的信息
- 假设不会讲一个共享内存区域映射到一个固定地址处,那么就需要确保所有对区域中的位置的引用使用偏移量来表示,而不是使用指针来标识。因为这个区域在不同进程中所处的虚拟地址可能不同
在这些共享内存技术之间还存在一些显著的差异
- 一个共享文件映射的内容会与底层映射文件同步意味着存储在共享内存区域中的数据能够在系统重启之间得到持久保存
- System V 和 POSIX 共享内存使用了不同的机制来标识和引用共享内存对象。System V使用了其自己的键和标识符模型,它们与标准的 UNIX I/O 模型是不匹配的并且需要单独的系统调用(如 shmctl())和命令(ipcs 和 ipcrm)。与之形成对比的是,POSIX共享内存使用了名字和文件描述符,其结果是使用各种既有的 UNIX 系统调用(如fstat()和 fchmod())就能够查看和操作共享内存对象了。
- System V 共享内存段的大小在创建时(shmget())就确定了。与之形成对比的是,在基于文件的映射和 POSIX 共享内存对象上可以使用 ftruncate()来调整底层对象的大小,然后使用 munmap()和 mmap()(或 Linux 特有的 mremap())重建映射。
- 因为历史原因,System V 共享内存受支持程度比 mmap()和 POSIX 共享内存对象广得多,尽管现在大多数 UNIX 实现都已经提供所有这些技术。
因此,优先使用POSIX而非System V共享内存。至于选择哪个接口则取决于是否需要一个持久性存储。共享文件映射提供了持久性存储,而 POSIX 共享内存对象则避免了在无需持久存储时使用磁盘文件所产生的开销。
总结
POSIX 共享内存对象用来在无关进程间共享一块内存区域而无需创建一个底层的磁盘文件。为创建 POSIX 共享内存对象需要使用 shm_open()调用来替换通常在 mmap()调用之前调用的 open()。shm_open()调用会在基于内存的文件系统中创建一个文件,并且可以使用传统的文件描述符系统调用在这个虚拟文件上执行各种操作。特别地,必须要使用 ftruncate()来设置共享内存对象的大小,因为其初始长度为零。