unix网络编程2-IPC-2-Posix IPC

继续学习《unix网络编程卷2: 进程间通信》,本篇主要记录第2章 Posix IPC 的相关内容。

2.1 概述

以下三种类型的IPC合称为Posix IPC.

  • Posix 消息队列
  • Posix 信号量
  • Posix 共享内存区

Posix IPC 在访问它们的函数和描述它们的信息上是有一些相似的。

本章主要讲述这些IPC的共同属性:用于标识的路径名打开或创建时指定的标志访问权限

下图汇总了所有IPC Posix函数。

2.2 IPC名字

第一章的图1.4中指出,三种类型的Poix IPC都使用Posix IPC名字进行标识。mq_open、sem_open和shm_open这三个函数的第一个参数就是这样的名字,它可能是某个文件系统中的一个真正的路径名,也可能不是。Posix.1中是这么描述Posix IPC名字的:

  • 它必须符合已有的路径名规则(必须最多由PATH_MAX个字节构成,包括结尾的空字节)

  • 如果它以斜杠符开头,那么对这些函数的不同调用将访问同一个队列,如果它不以斜杠符开头,那么效果取决于实现

  • 名字中额外的斜杠符的解释由实现定义。

因此,为了便于移植起见,Posix IPC名字必须以一个斜杠符开头,并且不能再包含其他斜杠符。

遗憾的是,这些规则还不够,仍会出现移植性问题。

Solaris 2.6要求有打头的斜杠符,但是不允许有另外的斜杠符。假设要创建的是一个消息队列,创建函数将在/tmp中创建三个以.MQ开头的文件。例如,如果给mg_open的参数为/queue.1234,那么这三个文件分别为 /tmp/.MQDqueue.1234、/tmp/.MQLqueue.1234和/tmp/.MQPqueue.1234。Digital Unix 4.0B则在文件系统中创建所指定的文件名。

当我们指定三个斜杠符(作为首字母)的名字时,移植性问题就发生了:我们必须在根目录中具有写权限。例如,/tmp.1234符合Posix规则,在Solaris下也可行,但是Digital Unix却会试图创建这个文件,这是除非我们有在根目录中的写权限,否则这样的尝试会失败。如果我们指定一个/tmp/test.1234的名字,那么在以该名字创建一个真正文件的所有系统上都将成功(前提是/tmp目录存在,而且具有写权限,对于多数unix系统来说,这是正常情况),在soloris下则失败

为避免这些移植性问题,我们应把Posix IPC名字的#define行放在一个便于修改的头文件中,这样应用程序转移到另一个系统上时,只需修改这个头文件。

px_ipc_name函数

解决上述移植性问题的另一种方案是自己定义一个名为px_ipx_name的函数,它为定位Posix IPC名字而添加上正确的前缀目录。

1
2
#include "unpipc.h"
char *px_ipc_name(const char *name);

参数name不能包含任何斜杠符。例如调用:

1
px_ipc_name("test1")

在solaris 2.6下该函数将返回一个执行字符串 /test1 的指针,在Digital Uninx 4.0B下返回一个指向字符串/tmp/test1的指针。存放结果的字符串的内存空间是动态分配的,并可通过调用free释放。另外,环境变量PX_IPC_NAME能够覆盖默认目录。

下面给出了px_ipc_name函数的实现。

lib/px_ipc_name.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
/* include px_ipc_name */
#include "unpipc.h"

char *
px_ipc_name(const char *name)
{
char *dir, *dst, *slash;

if ( (dst = malloc(PATH_MAX)) == NULL)
return(NULL);

/* 4can override default directory with environment variable */
if ( (dir = getenv("PX_IPC_NAME")) == NULL) {
#ifdef POSIX_IPC_PREFIX
dir = POSIX_IPC_PREFIX; /* from "config.h" */
#else
dir = "/tmp/"; /* default */
#endif
}
/* 4dir must end in a slash */
slash = (dir[strlen(dir) - 1] == '/') ? "" : "/";
snprintf(dst, PATH_MAX, "%s%s%s", dir, slash, name);

return(dst); /* caller can free() this pointer */
}
/* end px_ipc_name */

char *
Px_ipc_name(const char *name)
{
char *ptr;

if ( (ptr = px_ipc_name(name)) == NULL)
err_sys("px_ipc_name error for %s", name);
return(ptr);
}

snprintf函数与sprintf函数的区别在于前者能检查目标缓存区是否溢出,snprintf函数第二个参数是目标缓冲区的大小。

2.3 创建或打开IPC对象

mq_open、sem_open和shm_open这三个创建或打开一个IPC对象的函数,它们的名为oflag的第二个参数指定应该怎么打开所请求的对象。这个标准open函数的第二个参数类似。下图给出了可组合构成该参数的各种常值。

前三行指定应该怎样打开对象:只读、只写或读写。消息队列能以其中任何一种模式打开,信号量的打开不指定任何模式(任意信号量操作,都需读写要访问权限),共享内存区对象则不能以只写模式打开。

后四行的标志是可选的。

若不存在则创建由函数第一个参数所指定名字的消息队列、信号量或共享内存区对象,同时检查O_EXCL标志。

创建一个新的消息队列、信号量或共享内存区对象时,至少需要另外一个称为mode的参数。该参数指定权限位,它是由下图中所示常值按位或组成的。

同新创建的文件一样,当创建一个新的消息队列、信号量或共享内存区对象时,其用户ID被置为当前进程的有效用户ID。信号量和共享内存区对象的组ID被置为当前进程的而有效组ID或某个系统默认组ID。新消息队列对象的组ID被置为当前进程的有效组ID。

这三种IPC对象在设置组ID上存在的差异多少有点奇怪。由open新创建的文件组ID或者是当前进程的有效组ID或是该文件所在目录的组ID,但是IPC函数不能假定系统为IPC对象创建了一个在文件系统中的路径。

如果该参数和O_CREAT参数一起指定,那么IPC函数只在所指定名字的消息队列、信号量或共享内存区对象不存在时创建新的对象。如果该对象已经存在,而且指定了O_CREAT | O_EXCL,那么返回一个EEXIST错误。

考虑到其他进程的存在,检查所指定名字的消息队列、信号量或共享内存区对象的存在与否和创建它(如果它不存在)这两步必须是原子的。

该标志使一个消息队列在队列为空时的读或队列填满时的写不被阻塞。

如果以写模式打开了一个已存在的共享内存区对象,那么该标志将使得该对象的长度被截成0.

图2.5给出了打开一个IPC对象的真正逻辑流程。

:如图2.6所示,如果oflag标志只指定O_CREAT,就无法区分是创建了一个新对象,还是引用了一个已经存在的对象。

2.4 IPC权限

新的消息队列、有名信号量或共享内存区对象是由其oflag参数中含有O_CREAT标志的mq_open、sem_open或shm_open函数创建的。如图2-4所注,权限位与这些类型的每个对象相关联,就像它们与每个Unix文件一样。