IPC(Inter-Process Communication,进程间通讯)可以有三种信息共享方式(随文件系统,随内核,随共享内存)。(当然这里虽然说是进程间通讯,其实也是可以和线程相通的)。
相对的IPC的持续性(Persistence of IPC Object)也有三种:
随进程持续的(Process-Persistent IPC)
IPC对象一直存在,直到最后拥有他的进程被关闭为止,典型的IPC有pipes(管道)和FIFOs(先进先出对象)
随内核持续的(Kernel-persistent IPC)
IPC对象一直存在直到内核被重启或者对象被显式关闭为止,在Unix中这种对象有System v 消息队列,信号量,共享内存。(注意Posix消息队列,信号量和共享内存被要求为至少是内核持续的,但是也有可能是文件持续的,这样看系统的具体实现)。
随文件系统持续的(FileSystem-persistent IPC)
除非IPC对象被显式删除,否则IPC对象会一直保持(即使内核才重启了也是会留着的)。如果Posix消息队列,信号量,和共享内存都是用内存映射文件的方法,那么这些IPC都有着这样的属性。
不同的Unix IPC的持续性:
随进程:
Pipe, FIFO, Posix的mutex(互斥锁), condition variable(条件变量), read-write lock(读写锁),memory-based semaphore(基于内存的信号量) 以及 fcntl record lock,TCP和UDP套接字,Unix domain socket
随内核:
Posix的message queue(消息队列), named semaphore(命名信号量), System V Message queue, semaphore, shared memory。
要注意的是,虽然上面所列的IPC并没有随文件系统的,但是我们就像我们刚才所说的那样,Posix IPC可能会跟着系统具体实现而不同(具有不同的持续性),举个例子,写入文件肯定是一个文件系统持续性的操作,但是通常来说IPC不会这样实现。很少有IPC会实现文件系统持续,因为这会降低性能,不符合IPC的设计初衷。
Unix的IPC有一些是有名的,有一些是无名的,到具体使用的时候就知道了,如果是无名IPC(典型是Pipe),必须是依赖于进程的,但是有名IPC(典型是FIFOs),就可以使用在两个没有依赖性的进程上(依赖性可以表现在,一个进程是另一个进程的子进程)。
Posix IPC
Posix的全称是 "Portable Operating System Interface",Posix不仅仅是一个单一标准,而且是IEEE(Institute for Electrical and Electronics Engineers, Inc. IEEE)指定的一个标准族。
Posix IPC一共有三个,就是Message Queue(消息队列),semaphores(信号量),Shared Memory(共享内存),在Posix.1中,这三个IPC的命名规则为:
- 必须符合已有的路径名规则(必须最多由PATH_MAX个字节构成,包括结尾的空字符)。
- 如果它以斜杠开头,那么对这些函数的不同调用将访问同一个队列,如果它不以斜杠符开头,那么效果取决于实现
- 名字中的额外斜杠符的解释由实现来定义。
由于Unix系统不同发行版的命名系统的规则都很不一样,所以使用Posix的时候需要注意命名规则。为了预防这种移植性问题,Unix系统给了定义了三个宏:
S_TPYEISMQ(buf)
S_TYPEISSEM(buf)
S_TYPEISSHM(buf)
这三个宏的作用就是为了检测当前系统是否有着对Posix IPC(message queue, semaphores, shared memory)的不同实现方式,buf是一个指向stat结构体的一个结构体(就是那个可以给fstat,lstat或者stat函数填充的那个缓冲结构)。如果当前系统对于特定的IPC有着不同的实现方式,那么对应的宏将会返回非0值,否则就会返回0。
然而这三个宏很少使用,因为没有一个系统保证这三个宏对应的Posix IPC(message queue, semaphores, shared memory)会在不同的系统有不同的实现方式。比如在Solaris2.6系统,这三个宏都是返回0的。
我们可以使用下面的函数来新建一个Posix IPC的名字:
char *px_ipc_name(const char *name) { char *dir, *dst, *slash; if((dst = malloc(PATH_MAX)) == NULL) { if((dir = getenv("PX_IPC_NAME")) == NULL) { #ifdef POSIX_IPC_PREFIX dir = POSIX_IPC_PREFIX #else dir = "/tmp/"; } } slash = (dir[strlen(dir) - 1] == '/') ? "" : "/"; snprintf(dst, PATH_MAX, "%s%s%s", dir, slash, name); return dst; }
Posix IPC的通道的打开与创建
打开Posix的三种IPC通道其实用的是三个不同的函数mq_open(打开消息队列),sem_open(打开信号量),shm_open(打开共享内存),这三种个函数都可以用不同的打开方式来创建IPC通道,除了最平常的O_RDONLY(只读), O_WRONLY(只写), O_RDWR(可读可写)外,还有四个方式
- O_CREAT:
创建一个不存在的消息通道,信号量或者共享内存IPC,当创建一个新的消息队列,信号量,或者共享内存时,它们的userID会被设置为进程有效的userID。信号量,共享内存的groupID会被设置为进程有效的groupID或者是系统默认groupID;消息队列的groupID会被设置为进程的groupID。- O_EXCL: 当这个标志和O_CREAT一起用的时候,只能创建不存在的消息通道,信号量或者共享内存IPC,如果所创建的IPC已经存在,创建函数(就是那三个函数)将会返回EEXIST错误。(注意单独的O_EXCL是没有意义的)
- O_NONBLOCK:
这个标志可以让消息队列读一个空的队列或者写一个满的队列。- O_TRUNC
只作用于共享内存,如果共享内存以读写方式打开,那么创建的IPC将会以0的长度创建。
关于Posix IPC权限
新的消息队列,有名信号量或者共享内存区的IPC对象是由oflags参数中含有O/_CREAT标志的mq_open,sem_open或者shm_open函数创建的,这些权限位与IPC类型的每个对象相关联,就像它们与每个Unix文件相关联一样(这个是很容易想象的,因为Unix就是把内存的操作对象映射到文件当中方便操作的。)
当同样由这三个函数打开一个已经存在的消息队列,信号量或者共享内存对象的时候(或者未指定O_CREAT,或者制定了O_CREAT但没有指定O_EXCL,同时对象已经存在),将基于如下信息执行权限测试:
- 创建时赋予该IPC对象的权限位;
- 所请求的访问类型(O/RDONLY,O/WRONLY或者O_RDWR);
- 调用进程的有效用户ID,有效组ID以及各个辅助组ID(如果支持辅助组的话)
大多数Unix内核按照如下步骤执行权限测试(如果如下步骤有哪一步不满足,那么其下面的步骤都不执行,操作视为失败)
- 如果当前进程的有效用户ID为0(superuser),那就允许访问
- 在当前进程的有效用户ID就等于该IPC对象的属主ID的前提下,如果相应的用户访问权限位已经设置,那就允许访问,否则拒绝访问。
这里的相应的访问权限位的意思是:如果当前进程为读访问而打开IPC对象,那么用户读权限位必须设置,如果当前进程为写访问而打开该IPC对象,那么用户写权限位必须设置- 在当前进程的有效组ID或它的某个辅助组ID等于该IPC对象的组ID的前提下,如果相应的组访问权限位已经设置,那么就允许访问,否则拒绝访问。
- 如果相应的其他用户权限位已经设置,那么就允许访问,否则拒绝访问。
System V IPC
System V IPC和Posix IPC其实是本质上是差不多的,不过需要注意的是,System V IPC不是随进程持续的,是随内核持续的。 Posix的IPC的名字可以像文件系统找文件一样找到它们的名字,但是System V IPC不可以找到它们(这些IPC)的名字。
key_t Keys和ftok Function
System V IPC系统创建新的IPC的三个函数分别是msgget(),senget(),shmget()这三个函数的参数都是(key_t key, int mode),其实key是由ftok函数创建的一个键值,ftok函数的声明为:
#include <sys/ipc.h> key_t ftok(const char *pathname,int id);
这个函数假定了这个程序使用了System V IPC进行通讯,客户端和服务器端同意使用具有一定意义的pathname(是已存在的路径名),如果客户端和服务器只用单一通道,那么可以将id为1;如果客户端和服务器需要双向通道(而且是两条),那么可以令一个通道的id为1,另一个为2,只要pathname是一样的,那可以认为客户端和服务器使用是同一种通道进行通讯的。
使用ftok函数内部实现是调用了stat函数,然后进行如下行为(典型实现,不是强制要求,一定要注意pathname是已存在路径):
- pathname所在文件系统信息(stat结构的st_dev)(12位)
- pathname对应的文件的在本文件系统的索引节点号(stat结构的st_ino)(12位)
- 低8位是id值(不能为0(8位)
System V IPC不保证当不同路径时Keys是不一样的,id值绝对不能0(所以很多实现都把id值为0的键值定义为IPC_PRIVATE)。
ipc_perm Structure
System V IPC中,由kernel维持一个IPC的信息结构,就像文件信息结构一样:(注意这个结构和书上的有点出入)
struct ipc_perm { __key_t __key; /* Key. */ __uid_t uid; /* Owner's user ID. */ __gid_t gid; /* Owner's group ID. */ __uid_t cuid; /* Creator's user ID. */ __gid_t cgid; /* Creator's group ID. */ unsigned short int mode; /* Read/write permission. */ unsigned short int __pad1; unsigned short int __seq; /* Sequence number. */ unsigned short int __pad2; __syscall_ulong_t __glibc_reserved1; __syscall_ulong_t __glibc_reserved2; };
System IPC V的两个标志位(IPC_CREAT, IPC_EXCL)的用法基本上和Posix的是一样的,注意System IPC V还多了个IPC_PRIVATE的标志位,这个标志位就是专门用来创建独立的IPC通道的(没有一种pathname和id的组合能创建IPC_PRIVATE)。
IPC权限
事实上System V IPC的权限是由上面的所说的IPC_CREAT, IPC_EXCL和IPC_PRIVATE加上读写权限组成的,读写权限由系统定义的6个宏决定MSG_R, MSG_W, SEM_R, SEM_A, SHM_R, SHM_W共同组成的,具体怎么组成看下表:
注意ipc_perm Structure的cuid、 uid创建IPC时会被设置为调用者的user id,cgid、 gid会被设置为调用者的组group id,唯一区别就是creator的ids是不允许被改变的,但是owner的ids是可以通过ctlXXX指令来改变的(ctLXXX指令对应于三种不同的IPC有着三个不同的函数,这三个函数不使用文件模式的掩码修改权限,而是设置为对应函数指定的准确的值)
每当一个进程访问某个IPC对象时,IPC就执行两级检查,该IPC对象被打开(调用getXXX函数)执行一次,以后每次使用该对象时执行一次:
- 当每一个进程以某个getXXX函数建立访问某个已经存在的IPC对象的通道时,IPC就执行一次初始检查。验证调用者的oflag参数有没有指定不在该对象ipc_perm结构mode成员中的任何访问位。任何调用进程创建一个IPC时,如果其所指定的oflag对应权限位被禁止,该函数将会返回一个错误。但是其实这种测试是没用的,因为它假定调用者知道自己的权限范畴(用户,组成员或者其他用户),调用进程只用把oflag指定为0就可以绕过这个检查。
- 每次调用IPC的权限测试和Posix的权限检查方式一样。
identifier Reuse标识符重用
ipc_perm结构还有一个名为seq的变量,它是一个槽位使用情况序列号,该该变量是一个由内核为系统中每个潜在的IPC对象维护的计数器,每当删除一个IPC对象时,内核就递增相应的槽位号,如果溢出则循环为0(注意这只是普通的SVR4实现,Unix98没有强制使用这个技巧,比如我在Ubuntu 16里面就没有这样的实现)。
为了防止恶意进程乱读取System V IPC来截获信息,IPC标识符的可能值被设计的非常大,当一个IPC表项被访问时,获取到的IPC值将增加一个IPC表项数,下面以一个进程为例:
int i, msqid; for (i = 0; i < 10;i++) { msqid = msgget((key_t)IPC_PRIVATE, SVMSG_MODE | IPC_CREAT); printf("msgid = %d ",msqid); msgctl(msqid, IPC_RMID, NULL); } return 0;
输出:
ipcs and ipcrm Programs
由于System V IPC的三种类型不是以文件系统中的路径名标识的,所以标准的ls和rm程序无法看到他们,也没办法删除,不过任何实现了System V IPC的系统都提供了两个特殊的程序,ipcs和ipcrm,ipcs输出有关System V IPC特性的各种信息,ipcrm删除一个System V 消息队列,信号量或者共享内存区。
Kernel Limits
System V IPC的多数实现有内在的内核限制,比如最大数目等,这些限制往往很小,但是还是可以修改的。