多任务编程中,除了同步互斥的问题外,还存在另外一种最常见的问题—进程间的信息传递。如果采用了多线程的方式实现了多任务,则由于多个线程之间共享宿主进程的资源(比如文件描述符,堆地址空间),因而这些线程之间可以很方便的实现信息的共享,唯一需要做的就是通过同步、互斥机制保证不出现竞态。但是如果是采用多进程的方式来实现多任务,则由于每个进程都拥有自己独立的资源,因而如果要在他们之间交互信息,就必须借助其它手段,这种手段通常被称为进程间通信(也即IPC---InterProcess Communication)。
IPC主要包括:管道,消息队列,信号量,共享内存, 套接字(SOCKET)。
每种IPC机制都会借助一种数据结构,这种数据结构的实例称为该IPC机制的对象(相应的,用于同步互斥的数据结构的实体也可以称为该机制的对象)。理清IPC对象的持久性,有助于理解相应的IPC的工作机制。
大致上IPC对象的持久性可以分为三种:
进程持久性:具有这种持久性的对象在持有它的最后一个进程关闭了该对象时就会消失。
内核持久性:具有这种持久性的对象在两种情形下会消失,一是系统重启了,而是它被显式的删除了
文件系统持久性:具有这种持久性的对象只有在它被显式删除时才会消失。
下表是常见的IPC对象以及用于同步互斥的对象的持久性
类型 | 持久性 |
管道 FIFO |
进程持久性 进程持久性 |
Posix互斥锁 Posix条件变量 Posix读写锁 Fcntl记录锁 |
进程持久性 进程持久性 进程持久性 进程持久性 |
Posix消息队列 Posix命名信号量 Posix信号量 Posix共享内存 |
内核持久性 内核持久性 进程持久性 内核持久性 |
System V消息队列 System V信号量 System V共享内存 |
内核持久性 内核持久性 内核持久性 |
TCP socket UDP socket Unix域socket |
进程持久性 进程持久性 进程持久性 |
从表中看没有一种对象的持久性是文件系统的。这也是合理的,因为很少有进程能不受系统重启的影响;而且使用文件系统持久性也可能会降低该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包含:
它们具有一些共同属性:用作标识的路径名、打开或创建时指定的标志以及访问权限;
消息队列 | 信号量 | 共享内存 | |
头文件 | <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 |
Posix.1定义了三个宏:
S_TYPEISMQ(buf)
S_TYPEISSEM(buf)
S_TYPEISSHM(buf)
它们需要一个指向某个stat结构的指针作为参数,其内容由fstat、lstat或stat这三个函数填入。如果所指定的IPC对象(消息队列、信号量或共享内存区对象)是作为一种确切的文件类型实现的,而且参数所指向的stat结构引用了这样的文件类型,那么这三个宏计算出一个非零值。否则,计算出的值为0。不过由于无法保证这三种类型的IPC使用一种独特的文件类型实现,因此它们不具备可移植性,也就没有多大的实际用途。因此,为了可移植性,最好自己实现一个可移植的获得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 |
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。
当用mq_open,sem_open,shm_open打开已经存在的消息队列,信号量或者共享内存对象时,基于如下信息执行权限测试
执行权限测试的步骤
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 |
三种类型的System VIPC使用key_t值作为他们的名字。头文件<sys/types.h>把key_t这个数据类型定义为一个整数,它通常是一个至少32位的整数。这些整数值通常是ftok函数赋赋予的。
函数ftok把一个已经存在的路径和一个整数标识符转换为一个key_t值,叫IPC键(IPC key).
ftok典型的实现调用stat函数,然后组合以下三个值:
如果pathname不存在或者当前进程不能访问该文件,则ftok返回-1,另外需要注意的是其pathname用于产生key的文件在使用ftok产生的key的进程运行期间不能被创建和删除,因为每次文件被创建,它会需要一个新索引结点编号,这就会导致下次使用ftok时获得了一个不同的key。
内核给每个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
};
创建或打开一个IPC对象的三个getXXX函数的都需要一个类型为key_t的IPC键的key值,并且返回一个整数标识符。该标识符不同于ftok函数的id参数。对于key值,应用程序有两种选择:
三个getXXX函数,都有一个oflag参数,他指定IPC对象的读写权位,并选择是创建一个新的IPC对象还是访问一个已存在的IPC对象,规则如下:
创建一个新的IPC对象时,以下信息就保存该对象的ipc_perm结构中:
在创建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 |
需要注意的是:
ipc_perm结构还含有一个名为seq的变量,它是一个槽位使用情况的序列号。该变量是一个由内核为系统中每个潜在的IPC对象维护的计数器,每当删除一个IPC对象时,内核就递增相应的槽位号,若溢出则循环回0。
递增槽位使用情况序列号的另一个原因是为了避免短时间内重用System V IPC标识符。这有助于确保过早终止的服务器重启后不会重用标识符。
由于System V IPC的三种类型不是以文件系统中路径名标识的,因此使用标准的ls和rm程序无法看到它们,也无法删除他们。不过实现了这些类型IPC的任何系统都提供两个特殊的程序:ipcs和ipcrm。ipcs输出有关System V IPC特性的各种信息,ipcrm则删除一个System V消息队列、信号量集或共享储存区。前者支持约十来个命令行选项,他们决定报告那种类型的IPC以及输出哪些信息,后者支持6个命令行选项。
System V IPC的多数实现有内在的内核限制,例如消息队列的最大数目、每个信号量集的最大信号量数。
另外还存在一些缺点: