多任务编程中,除了同步互斥的问题外,还存在另外一种最常见的问题—进程间的信息传递。如果采用了多线程的方式实现了多任务,则由于多个线程之间共享宿主进程的资源(比如文件描述符,堆地址空间),因而这些线程之间可以很方便的实现信息的共享,唯一需要做的就是通过同步、互斥机制保证不出现竞态。但是如果是采用多进程的方式来实现多任务,则由于每个进程都拥有自己独立的资源,因而如果要在他们之间交互信息,就必须借助其它手段,这种手段通常被称为进程间通信(也即IPC---InterProcess Communication)。
IPC主要包括:管道,消息队列,信号量,共享内存, 套接字(SOCKET)。
一、IPC对象的持久性
每种IPC机制都会借助一种数据结构,这种数据结构的实例称为该IPC机制的对象(相应的,用于同步互斥的数据结构的实体也可以称为该机制的对象)。理清IPC对象的持久性,有助于理解相应的IPC的工作机制。
1.对象持久性
大致上IPC对象的持久性可以分为三种:
进程持久性:具有这种持久性的对象在持有它的最后一个进程关闭了该对象时就会消失。
内核持久性:具有这种持久性的对象在两种情形下会消失,一是系统重启了,而是它被显式的删除了
文件系统持久性:具有这种持久性的对象只有在它被显式删除时才会消失。
下表是常见的IPC对象以及用于同步互斥的对象的持久性
类型 | 持久性 |
管道 FIFO | 进程持久性 进程持久性 |
Posix互斥锁 Posix条件变量 Posix读写锁 Fcntl记录锁 | 进程持久性 进程持久性 进程持久性 进程持久性 |
Posix消息队列 Posix命名信号量 Posix信号量 Posix共享内存 | 内核持久性 内核持久性 进程持久性 内核持久性 |
System V消息队列 System V信号量 System V共享内存 | 内核持久性 内核持久性 内核持久性 |
TCP socket UDP socket Unix域socket | 进程持久性 进程持久性 进程持久性 |
从表中看没有一种对象的持久性是文件系统的。这也是合理的,因为很少有进程能不受系统重启的影响;而且使用文件系统持久性也可能会降低该IPC机制的性能。
2.调用fork,exec,_exit对IPC对象及同步互斥对象的影响
类型 | fork | exec | _exit |
管道和FIFO | 子进程获得父进程的所有打开的描述符的拷贝 | 除非描述符的FD_CLOSEXEC比特被置位了,否则描述符保持打开状态 | 所有描述符都被关闭,在描述符最后一次被关闭时,管道或FIFO中的数据会被删除 |
Posix消息队列 | 子进程获得父进程的所有打开的消息队列描述符的拷贝 | 所有打开的消息队列的描述符都被关闭 | 所有打开的消息队列的描述符都被关闭 |
System V消息队列 | 没影响 | 没影响 | 没影响 |
Posix互斥锁和条件变量 | 如果在共享内存中并且设置了进程共享属性则就被共享 | 除非位于仍被打开的共享内存中并且具有进程共享属性否则就将消失 | 除非位于仍被打开的共享内存中并且具有进程共享属性否则就将消失 |
Posix读写锁 | 如果在共享内存中并且设置了进程共享属性则就被共享 | 除非位于仍被打开的共享内存中并且具有进程共享属性否则就将消失 | 除非位于仍被打开的共享内存中并且具有进程共享属性否则就将消失 |
Posix(基于内存的)信号量 | 如果在共享内存中并且设置了进程共享属性则就被共享 | 除非位于仍被打开的共享内存中并且具有进程共享属性否则就将消失 | 除非位于仍被打开的共享内存中并且具有进程共享属性否则就将消失 |
Posix命名信号量 | 在父进程被打开的在子进程中仍保持打开 | 所有被打开的都将被关闭 | 所有被打开的都将被关闭 |
System V信号量 | 所有的semadj的值在子进程中被设置为0 | 所有的semadj的值被传递给新的程序 | 所有的semadj的值被加到相应的信号量上 |
记录锁 | 父进程持有的锁不会被子进程所继承 | 只要描述保持打开,锁就不会因为exec的动作而变化 | 所有被进程持有的锁都会被释放 |
mmap共享内存 | 父进程的共享内存被子进程保留 | 共享内存被unmapped | 共享内存被unmapped |
Posix共享内存 | 父进程的共享内存被子进程保留 | 共享内存被unmapped | 共享内存被unmapped |
System V共享内存 | 父进程中已连接上的共享内存被子进程保留 | 共享内存被deattached | 共享内存被deattached |
二、POSIX IPC
Posix IPC包含:
- Posix 消息队列
- Posix 信号量
- Posix共享内存区
它们具有一些共同属性:用作标识的路径名、打开或创建时指定的标志以及访问权限;
1.Posix IPC函数汇总
消息队列 | 信号量 | 共享内存 | |
头文件 | <mqueue.h> | <semaphore.h> | <sys/mman.h> |
创建、打开或删除IPC函数 | mq_open mq_close mq_unlink | sem_open sem_close sem_unlink ---------------------- sem_init sem_destory | shm_open shm_unlink |
控制IPC操作的函数 | mq_getattr mq_setattr | ftruncate fstat | |
IPC操作函数 | mq_send mq_receive mq_notify | sem_wait sem_trywait sem_post sem_getvalue | mmap munmap |
2.IPC名字
- 它必须符合已有的路径名规则(必须最多由PATH_MAX个字节构成,包括结尾的空字节)。
- 如果它以斜杠符开头,那么对这些函数的不同调用将访问同一个队列。如果它不以斜杠符开头,那么调用的结果取决于实现。
- 名字中额外的斜杠符的解释由实现定义。为了可移植性,IPC名字必须以一个斜杠开头,并且不能包含其他的斜杠符。
Posix.1定义了三个宏:
S_TYPEISMQ(buf)
S_TYPEISSEM(buf)
S_TYPEISSHM(buf)
它们需要一个指向某个stat结构的指针作为参数,其内容由fstat、lstat或stat这三个函数填入。如果所指定的IPC对象(消息队列、信号量或共享内存区对象)是作为一种确切的文件类型实现的,而且参数所指向的stat结构引用了这样的文件类型,那么这三个宏计算出一个非零值。否则,计算出的值为0。不过由于无法保证这三种类型的IPC使用一种独特的文件类型实现,因此它们不具备可移植性,也就没有多大的实际用途。因此,为了可移植性,最好自己实现一个可移植的获得IPC名字的函数。
3.创建与打开IPC通道
mq_open,sem_open,shm_open用来创建或打开一个IPC对象,第2个参数oflag指定打开IPC对象的方式。消息队列可以以只读,只写或读写任何一种模式打开,信号量的打开不需要指定任何模式,共享内存区不能以只写模式打开。大体上说,IPC的模式及标识的含义类似于文件系统的相应的模式及标记。
oflag常值
说明 | mq_open | sem_open | shm_open |
只读 | O_RDONLY | O_RDONLY | |
只写 | O_WRONLY | ||
读写 | O_RDWR | O_RDWR | |
若不存在则创建 | O_CREAT | O_CREAT | O_CREAT |
排它性创建 | O_EXCL | O_EXCL | O_EXCL |
非阻塞创建 | O_NONBLOCK | ||
若存在则截断 | O_TRUNC |
- O_CREAT:若不存在则创建指定名字的消息队列,信号量或共享内存对象(同时检查O_EXCL标志)。创建一个新的消息队列、信号量或共享内存区对象时,至少需要另外一个称为mode的参数。该参数指定权限位。
Mode取值及其含义
常值 | 说明 |
S_IRUSR S_IWUSR | 用户读 用户写 |
S_IRGRP S_IWGRP | 组成员读 组成员写 |
S_IROTH S_IWOTH | 其他用户读 其他用户写 |
这些常值定义在<sys/stat.h>头文件中。所指定的权限受当前进程的文件模式创建掩码影响,当前进程的文件模式创建掩码可以通过调用umask函数或使用shell的umask命令来设置。创建一个新的消息队列、信号量或共享内存区对象时,其用户ID被设置为当前进程的有效组ID;信号量或共享内存区对象组的ID被设置为当前进程的有效组ID或某个系统默认组ID;新消息队列对象的组ID被置为当前进程的有效组ID。
- O_EXCL:如果该标识和O_CREAT一起指定,那么IPC函数只在所指定的名字的消息队列,信号量或共享内存区对象不存在时才创建新的对象。如果该对象已经存在,而且指定了O_CREAT|O_EXCL,那么返回一个EEXIST错误。
- O_NONBLOCK:该标志使得一个消息队列在队列为空时的读或队列填满时的写不被阻塞。
- O_TRUNC:如果以读写模式打开了一个已存在的共享内存区对象,那么该标识将使得该对象的长度被截成0;
4.IPC权限
当用mq_open,sem_open,shm_open打开已经存在的消息队列,信号量或者共享内存对象时,基于如下信息执行权限测试
- 创建时赋予该IPC对象的权限位;
- 所请求的访问类型(O_RDONLY、O_WRONLY或O_RDWR);
- 调用进程的有效用户ID、有效组ID以及各个辅助组ID;
执行权限测试的步骤
- 如果当前进程的有效用户ID为0,那就允许访问;
- 如果当前的进程的有效用户ID等于该IPC对象属主ID:检查用户访问权限位,如果相应的用户访问权限位已设置,那就允许访问,否则就拒绝访问;
- 如果当前进程的有效组ID或它的某个辅助组ID等于该IPC对象的组ID:则检查组访问权限位,如果相应的组访问权限已设置,那就允许访问,否则拒绝访问;
- 如果相应的其他用户访问权限已设置,那就允许访问,否则拒绝访问。
三、System V IPC
System V IPC包含:
- System V消息队列
- System V信号量
- System V共享内存区
1.System V IPC函数汇总
消息队列 | 信号量 | 共享内存 | |
头文件 | <sys/msg.h> | <sys/sem.h> | <sys/shm.h> |
创建、打开或删除IPC | msgget | semget | shmget |
控制IPC操作 | msgctl | semctl | shmctl |
IPC操作 | msgsend msgrcv | semop | shmat shmdt |
2..key_t键和ftok函数
三种类型的System VIPC使用key_t值作为他们的名字。头文件<sys/types.h>把key_t这个数据类型定义为一个整数,它通常是一个至少32位的整数。这些整数值通常是ftok函数赋赋予的。
函数ftok把一个已经存在的路径和一个整数标识符转换为一个key_t值,叫IPC键(IPC key).
ftok典型的实现调用stat函数,然后组合以下三个值:
- pathname所在文件系统的信息;
- 该文件在本文件系统内的索引节点号;
- id的低序8位;
如果pathname不存在或者当前进程不能访问该文件,则ftok返回-1,另外需要注意的是其pathname用于产生key的文件在使用ftok产生的key的进程运行期间不能被创建和删除,因为每次文件被创建,它会需要一个新索引结点编号,这就会导致下次使用ftok时获得了一个不同的key。
3.ipc_perm结构
内核给每个IPC对象维护一个信息结构,其内容跟内核给文件维护的信息类似。
struct ipc_perm
{
uid_t uid;//owner的用户ID
gid_t gid;//owner的组ID
uid_t cuid;//creater的用户ID
gid_t cgid;//creater的组ID
mode_t mode;//读写模式
ulong_t seq;//序列号
key_t key;//IPC key
};
4.创建与打开IPC通道
创建或打开一个IPC对象的三个getXXX函数的都需要一个类型为key_t的IPC键的key值,并且返回一个整数标识符。该标识符不同于ftok函数的id参数。对于key值,应用程序有两种选择:
- 调用ftok,给它传递pathname和id;
- 指定key为IPC_PRIVATE,这将保证会创建一个新的、唯一的IPC对象;
三个getXXX函数,都有一个oflag参数,他指定IPC对象的读写权位,并选择是创建一个新的IPC对象还是访问一个已存在的IPC对象,规则如下:
- key指定为IPC_PRIVATE能保证创建一个唯一的IPC对象。没有一对id和pathname的组合会导致ftok产生IPC_PRIVATE这个键值;
- 设置oflag参数的IPC_CREAT位但不设置它的IPC_EXCL位时,如果指定键的IPC对象不存在,那就创建一个新对象,否则返回该对象;
- 设置oflag参数的IPC_CREAT位和它的IPC_EXCL位时,如果所指定键的IPC对象不存在,那就创建一个新的对象,否则返回一个EEXIST错误,因为该对象已经存在;
- 如果要访问一个已经存在的IPC,就不能指定IPC_PRIVATE标记,因为这是一个特殊的用于创建IPC对象的键值
5.IPC权限
创建一个新的IPC对象时,以下信息就保存该对象的ipc_perm结构中:
- oflag参数中某些比特会初始化ipc_perm结构的mode成员。
- cuid和cgid成员分别设置为调用进程的有效用户ID和有效组ID。这两个成员合称为创建者ID。
- ipc_perm结构的uid和gid成员也分别设置为调用进程的有效用户ID和有效组ID。这两个成员合称为所有者ID。
在创建IPC结构时,除seq以外的所有字段都赋初值。以后,可以调用msgctl、semctl或shmctl修改uid、gid和mode字段。为了改变这些值,调用进程必须是IPC结构的创建者或超级用户。
系统V的IPC许可权
许可权 | 消息队列 | 信号量 | 共享内存 |
用户读 用户写(更改) | MSG_R MSG_W | SEM_R SEM_A | SHM_R SHM_W |
组读 组写(更改) | MSG_R>>3 MSG_W>>3 | SEM_R>>3 SEM_A>>3 | SHM_R>>3 SHM_W>>3 |
其他读 其他写(更改) | MSG_R>>6 MSG_W>>6 | SEM_R>>6 SEM_A>>6 | SHM_R>>6 SHM_W>>6 |
需要注意的是:
- 创建者ID永远不会改变,但是进程可以通过IPC机制中的IPC_SET命令修改所有者ID。三个getXXX没有使用UNIX的文件创建模式掩码,IPC对象的权限被设置为指定的值。Posxi IPC非常类似文件,但是System V IPC在权限的存储上是与文件系统的不同的,它的权限不受文件创建模式掩码的影响。
- 任意进程要访问一个IPC对象都需要经历两个层级的检查:一个在IPC对象被打开时(检查是否指定了未包含在ipc_perm结构中的模式,因为创建的IPC对象的权限是存在于ipc_perm结构中的),一个在IPC对象被使用时(过程类似于PosixIPC的权限检查)。
6.标识符重用
ipc_perm结构还含有一个名为seq的变量,它是一个槽位使用情况的序列号。该变量是一个由内核为系统中每个潜在的IPC对象维护的计数器,每当删除一个IPC对象时,内核就递增相应的槽位号,若溢出则循环回0。
递增槽位使用情况序列号的另一个原因是为了避免短时间内重用System V IPC标识符。这有助于确保过早终止的服务器重启后不会重用标识符。
7.ipcs和ipcrm程序
由于System V IPC的三种类型不是以文件系统中路径名标识的,因此使用标准的ls和rm程序无法看到它们,也无法删除他们。不过实现了这些类型IPC的任何系统都提供两个特殊的程序:ipcs和ipcrm。ipcs输出有关System V IPC特性的各种信息,ipcrm则删除一个System V消息队列、信号量集或共享储存区。前者支持约十来个命令行选项,他们决定报告那种类型的IPC以及输出哪些信息,后者支持6个命令行选项。
8.内核限制
System V IPC的多数实现有内在的内核限制,例如消息队列的最大数目、每个信号量集的最大信号量数。
另外还存在一些缺点:
- IPC结构是在系统范围内起作用的,没有访问计数。例如,如果创建了一个消息队列,在该队列中放入了几则消息,然后终止,但是该消息队列及其内容并不被删除。它们余留在系统中直至:由某个进程调用msgrcv或msgctl读消息或删除消息队列,或某个进程执行ipcrm命令删除消息队列;或由正在再起动的系统删除消息队列。
- 这些I P C结构并不按名字为文件系统所知。因此不能用常规的文件系统函数来存取它们或修改它们的特性。为了支持它们不得不增加了十多个全新的系统调用(msgget、semop、shmat等)。不能用ls命令见到它们,不能用rm命令删除它们,不能用chmod命令更改它们的存取权。于是,也不得不增加了全新的命令ipcs和ipcrm。
- 这些IPC不使用文件描述符,所以不能对它们使用多路转接I / O函数:select和poll。