POSIX.1b 实时扩展定义了一组 IPC 机制,它们与在第 45 章到第 48 章章中介绍的System V IPC 机制类似。这些 IPC 机制被统称为 POSIX IPC。这三种 POSIX IPC机制具体如下。
1.消息队列可以用来在进程间传递消息。与 System V 消息队列一样,消息边界被保留了下来, 这样读者和写者就以消息为单位进行通信了。POSIX 消息队列允许给每个消息赋一个优先级,这样在队列中优先级较高的消息会排在优先级较低的消息前面。这种功能从某种程度上来讲与 System V 消息中的类型字段提供的功能是一样的。
2.信号量允许多个进程同步各自的动作。与 System V 信号量一样, POSIX 信号量也是一个由内核维护的整数, 其值永远都不会小于 0。与 System V 信号量相比, POSIX信号量在用法上要简单一些:它们是逐个分配的(与 System V 信号量集相比),并且在单个信号量上只能使用两个操作来将信号量的值加 1 或减 1(与 semop()系统 调用能原子地在一个 System V 信号量集中的多个信号量上加上或减去一个任意值相比)。
3.共享内存使得多个进程能够共享同一块内存区域。与 System V 共享内存一样, POSIX共享内存提供了一种快速 IPC。一个进程一旦更新了共享内存之后,所发生的变更立即对共享同一区域的其他进程可见。
本章将对各个 POSIX IPC 工具进行概述,并着重介绍它们的共有特性。
51.1 API 概述
三种 POSIX IPC 机制拥有很多共有特性。表 51-1 对它们的 API 进行了总结,在后面几节中将深入介绍它们的共有特性的细节信息
接 口 | 消 息 队 列 | 信 号 量 | 共 享 内 存 |
---|---|---|---|
头文件 | <mqueue.h> | <semaphore.h> | <sys/mman.h> |
对象句柄 | mqd_t | sem_t * | int 文件描述符 |
创建/打开 | mq_open() | sem_open() | shm_open() + mmap() |
关闭 | mq_close() | sem_close() | munmap() |
断开链接 | mq_unlink() | sem_unlink() | shm_unlink() |
执行 IPC | mq_send(), mq_receive() | sem_post(), sem_wait(), sem_getvalue() | 在共享区域中的位置上操作 |
其他操作 | mq_setattr()设置特性 mq_getattr()获取特性 mq_notify()请求通知 | br>sem_init()初始化未命名信号量 sem_destroy()销毁未命名信号量 | 无 |
IPC 对象名字
要访问一个 POSIX IPC 对象就必须要通过某种方式来识别出它。在 SUSv3 中规定的唯一一种用来标识 POSIX IPC 对象的可移植的方式是使用以斜线打头后面跟着一个或多个非斜线字符的名字,如/myobject。 Linux 和其他一些实现(如 Solaris)允许采用这种可移植的命名方式来给 IPC 对象命名。
在 Linux 上, POSIX 共享内存和消息队列对象的名字的最大长度为 NAME_MAX(255)个字符, 而信号量的名字的最大长度要少 4 个字符, 这是因为实现会在信号量名字前面加上字符串 sem.。
SUSv3 并没有禁止使用形式不为/myobject 的名字, 但表示这种名字的语义是由实现定义的。在一些系统上,创建 IPC 对象名字的规则是不同的。如在 Tru64 5.1 上, IPC 对象名字会被创建成标准文件系统中的名字,并且名字会被解释成为一个绝对或相对路径名。如果调用者没有权限在该目录中创建文件,那么 IPC open 调用就会失败。
这意味着在 Tru64 上非特权程序无法创建形如/myobject 之类的名字,因为非特权用户通常无法在根目录(/)中创建文件。其他一些实现在传递给 IPC open 调用的名字的构建上也存在特定的规则。因此在可移植的应用程序中应该将 IPC 对象名的生成工作放在一个根据目标实现裁剪过的单独的函数或头文件中。
创建或打开 IPC 对象
每种 IPC 机制都有一个关联的 open 调用(mq_open()、 sem_open()以及 shm_open()),它与用于打开文件的传统的 UNIX open()系统调用类似。给定一个 IPC 对象名, IPC open 调用会完成下列两个任务中的一个。
1.使用给定的名字创建一个新对象,打开该对象并返回该对象的一个句柄。
2.打开一个既有对象并返回该对象的一个句柄。
IPC open 调用返回的句柄与传统的 open()系统调用返回的文件描述符类似——它在后续的调用中被用来引用该对象。 IPC open 调用返回的句柄的类型依赖于对象的类型。对于消息队列来讲返回的是一个消息队列描述符,其类型为 mqd_t。对于信号量来讲,返回的是一个类型为 sem_t *的指针。对于共享内存来讲返回的是一个文件描述符。 所有 IPC open 调用都至少接收三个参数——name、 oflag 以及 mode——如下面的shm_open()调用所示:
这些参数与传统的 UNIX open()系统调用接收的参数类似。 name 参数标识出了待创建或待打开的对象。 oflag 参数是一个位掩码,在这个参数中至少可以包含下列几种标记。 O_CREAT 如果对象不存在,那么就创建一个对象。如果没有指定这个标记并且对象不存在,那么就返回一个错误ENOENT。
O_EXCL 如果同时也指定了 O_CREAT 并且对象已经存在,那么就返回一个错误(EEXIST)。这两步——检查是否存在和创建——是原子操作(5.1 节)。 这个标记在不指定 O_CREAT 时是不起作用的。 根据对象的类型, oflag 还可能会包含 O_RDONLY、 O_WRONLY 以及 O_RDWR 这三个值中的一个,其含义与它们在 open()中含义相同。一些 IPC 机制还支持额外的标记。 剩下的参数 mode 是一个位掩码,它指定了在对象被创建时(即指定了 O_CREAT 并且对象不存在) 施加于新对象之上的权限。 mode 参数能取的值与其在文件上的取值一样(表 15-4)。 与 open()系统调用一样, mode 中的权限掩码会根据进程的 umask(15.4.6 节)取掩码。新 IPC对象的所有权和组所有权将根据发起这个 IPC open 调用的进程的有效用户 ID 和组 ID 来确定。 在那些 IPC 对象位于标准文件系统中的系统上, SUSv3 允许实现将新 IPC 对象的组 ID设置为父目录的组 ID。
关闭 IPC 对象
对于 POSIX 消息队列和信号量来讲, 存在一个 IPC close 调用来表明调用进程已经使用完该对象,系统可以释放之前与该对象关联的所有资源了。
POSIX 共享内存对象的关闭则是通过使用 munmap()解除映射来完成的。
IPC 对象在进程终止或执行 exec()时会自动被关闭。
IPC 对象权限
IPC 对象上的权限掩码与文件上的权限掩码是一样的。 访问一个 IPC 对象的权限与访问文件的权限(15.4.3 节)是类似的,但对于 POSIX IPC 对象来讲,执行权限是没有意义的。
从内核 2.6.19 起, Linux 支持使用访问控制列表(ACL)来设置 POSIX 共享内存对象和命名信号量上的权限。目前,在 POSIX 消息队列上不支持 ACL。
IPC 对象删除和对象持久性
与打开文件一样, POSIX IPC 对象也有引用计数——内核会维护对象上的打开引用计数。与System V IPC 对象相比, 这种方式使得应用程序能够更加容易地确定何时可以安全地删除一个对象。
每个 IPC 对象都有一个对应的 unlink 调用,其操作类似于应用于文件的传统的 unlink()系统调用。 unlink 调用会立即删除对象的名字,然后在所有进程使用完对象(即当引用计数等于 0 时)之后销毁该对象。对于消息队列和信号量来讲,这意味着当所有进程都关闭对象之后对象会被销毁;对于共享内存来讲,当所有进程都使用 munmap()解除与对象之间的映射关系之后就会销毁该对象。
当一个对象被断开链接之后, 指定同一个对象名的 IPC open 调用将会引用一个新对象(在不指定 O_CREAT 时会失败)。 与 System V IPC 一样, POSIX IPC 对象也拥有内核持久性。对象一旦被创建,就会一直存在直到被断开链接或系统被关闭。这样一个进程就能够创建一个对象、修改其状态,然后退出并将对象留给在后面某个时刻启动的一些进程访问。
通过命令行列出和删除 POSIX IPC 对象
System V IPC 提供了两个命令 ipcs 和 ipcrm 来列出和删除 IPC 对象。对于 POSIX IPC 对象来讲,不存在标准的命令来执行类似的任务。然而在很多系统上,包括 Linux, IPC 对象是在一个挂载在根目录(/)下某处的真实或虚拟文件系统中实现的,因此可以使用标准的 ls 和rm 命令来列出和删除 IPC 对象。(SUSv3 并没有规定使用 ls 和 rm 来完成这些任务。)
使用这些命令存在的主要问题是 POSIX IPC 对象名以及它们在文件系统中所处的位置是不标准的。在 Linux 上, POSIX IPC 对象位于挂载在设置了粘滞位的目录下的虚拟文件系统中。这个位是一个受限的删除标记( 15.4.5 节),设置该位表示非特权进程只能够断开它自己拥有的POSIX IPC 对象的链接。
在 Linux 上编译使用 POSIX IPC 的程序
在 Linux 上,使用 POSIX IPC 机制的程序必须要与实时库 librt 链接起来,这可以通过在 cc 命令中指定–lrt 选项来完成。
51.2 System V IPC 与 POSIX IPC 比较
下面几个章节将分别对各种 POSIX IPC 机制进行介绍,同时还会将它们与其在 System V中的对应机制进行对比。下面考虑这两种 IPC 之间的一些常规比较。 与 System V IPC 相比, POSIX IPC 拥有下列常规优势:
1.POSIX IPC 的接口比 System V IPC 接口简单。
2.POSIX IPC 模型——使用名字替代键、使用 open、 close 以及 unlink 函数——与传统的 UNIX 文件模型更加一致。
3.POSIX IPC 对象是引用计数的。这就简化了对象删除,因为可以断开一个 POSIX IPC对象的链接,并且知道当所有进程都关闭该对象之后对象就会被销毁。
然而 System V IPC 具备一个显著的优势:可移植性。 POSIX IPC 在下列方面的移植性不如 System V IPC。
1.System V IPC 在 SUSv3 中进行了规定,并且几乎所有的 UNIX 实现都支持 System V IPC。而与之相反的是, POSIX IPC 机制在 SUSv3 中则是一个可选的组件。一些 UNIX实现并不支持(所有) POSIX IPC 机制。这种情况可以通过 Linux 上的微观世界反映出来: POSIX 共享内存从内核 2.4 开始得到支持;完整的 POSIX 信号量实现从内核2.6 开始得到支持; POSIX 消息队列从内核 2.6.6 开始得到支持。
2.尽管 SUSv3 对 POSIX IPC 对象名字进行了规定,但各种实现仍然采用不同的规则来命名 IPC 对象。这些差异使得程序员在编写可移植应用程序时需要做一些(很少)额外的工作。
3.POSIX IPC 的各种细节并没有在 SUSv3 中进行规定。 特别是没有规定使用哪些命令来显示和删除系统上的 IPC 对象。在很多实现上使用的是标准的文件系统命令,但用来标识 IPC 对象的路径名的细节信息则因实现而异。
51.3 总结
POSIX IPC 是一个一般名称, 它指由 POSIX.1b 设计来取代与之类似的 System V IPC 机制的三种 IPC 机制——消息队列、信号量以及共享内存。
POSIX IPC 接口与传统的 UNIX 文件模型更加一致。 IPC 对象是通过名字来标识的,并使用 open、 close 以及 unlink 等操作方式与相应的文件相关的系统调用类似的调用来管理。
POSIX IPC 提供的接口在很多方面都优于 System V IPC 接口,但 POSIX IPC 可移植性要比 System V IPC 稍差。