Interprocess Communication
1. Pipes
Pipes(管道)作为UNIX中时间最长的IPC工具, 有两个缺陷:
- Pipes为half-duplex(半双工, 数据只能向一个方便传递). 虽然已经有系统支持full-duplex pipes, 但为了系统兼容性, 还是应将pipes默认为half-duplex
- Pipes的使用有一个前提: 两个进程有共同的祖先, 一般用于parent process和child process之间的通信
- 被创建的pipe没有名字
- Pipe的buffer空间有限
尽管pipes有诸多缺陷, 但仍是最被常用的IPC工具. 调用pipe()可创建一个pipe:
|
但单个进程中的pipe没有意义, 通常进程会先调用pipe(), 然后调用fork(), 结果如下:
这时parent process和child process都可以向pipe读取或写入数据. 若想要让child process读取来自parent process的数据, 则关闭parent process中的fd[0]和child process中的fd[1]:
当用户关闭pipe的一端时:
- 当write端被关闭后, read端尝试读取会返回0
- 当read端被关闭后, write端尝试写入会产生SIGPIPE signal; 若无视该信号, 则返回-1并将errno设为EPIPE
Kernel中的PIPE_BUF表示pipe的buffer size, POSIX.1规定当进程写入的数据小于PIPE_BUF时, 写入操作必须是原子的; 但若写入数据大于PIPE_BUF, 操作结果有以下几种结果:
- O_NONBLOCK disabled: 以非原子地形式与其他进程交错写入; write()会被阻塞, 直到写入全部数据
- O_NONBLOCK enabled: 若pipe buffer已满, 则write()直接返回-1并将errno设置为EAGAIN; 否则写入部分数据, write()返回还没写入的字节数
2. popen and pclose Functions
|
以下是调用popen()时type为"r"和"w"的结果:
注意: popen()不应用于设置了set-user-ID或set-group-ID的程序中. set-ID会提高用户权限, 可能导致shell执行的命令具有破坏性
3. Coprocesses
Filter作为一种程序, 用于处理或产生stream. UNIX中的filter从standard input中获取数据, 经过处理后将数据写入standard output, 常见的UNIX filter: awk, cat, comm, cut, expand, compress, fold, grep, head, less, more, nl, perl, paste, pr, sed, sh, sort, split, strings, tail, tac, tee, tr, uniq, wc, zcat. 可用Shell的pipe operator ("|")来连接一个或多个filter, Filter的输入和输出也可以重定向为某个文件或其他设备.
Coprocess借鉴filter的思路: 当某个进程既产生filter的输入, 又从filter获取数据时, 该filter就成为这个进程的coprocess. 通常情况下coprocess会创建两个pipe, 一个用于读取其他进程的数据, 另一个用于输出处理过的数据. 相对于popen的单向pipe, coprocess提供了双向数据流.
4. FIFOs
FIFO也被称为named pipes, 因为FIFO以文件形式存放在文件系统中. 即使两个进程不存在共同祖先, 仍可以使用FIFO进行通信.
|
FIFO的使用与pipe并无太大区别, 但使用前需调用open()打开指定FIFO文件. 打开FIFO文件时, O_NONBLOCK flag对open()有以下影响:
- O_NONBLOCK disabled: 对于只读block, open()会等待其他进程写入数据; 对于只写block, open()会等待其他进程读取
- O_NONBLOCK enabled: 对于只读block, 若没有进程读取数据, open()会立即返回; 对于只写block, 若没有进程写入数据吗, open()会返回-1并设置errno为ENXIO
如同pipe, 当向没有reader的FIFO写入数据时会产生SIGPIPE; 当writer关闭FIFO时, reader会得到EOF. FIFO主要有两种用途:
- Shell command使用FIFOs在shell pipeline之间传递数据
- 用于client和server传递数据
5. XSI IPC
XSI IPC定义了三个IPC structure: message queue, semaphones, shared memory. 这三种IPC structure虽然存在一定的差异, 但也有很多相似之处.
5.1 Identifiers and Keys
三种IPC结构体在kernel中都用一个非负整数表示, 作为其identifier. 每创建一个IPC structure, 分配的IPC identifer都加一; 达到最大值后再从0循环使用.
但IPC identifier只在IPC structure内部使用, 进程想要调用IPC structure需要使用IPC structure对应的key. 当创建IPC structure, 进程必须为其指定一个key. Key的类型为key_t, 通常为long integer, kernel会将key转换为IPC identifier. 以下是client和server使用同一个IPC structure的方法:
- Server创建IPC structure时指定key为IPC_PRIVATE, 获得IPC identifier后将其保存在某个文件中方便client获取. IPC_PRIVATE保证server创建的IPC structure是全新的, 而不是绑定在其他IPC structure上; 缺点是需要I/O操作将IPC identifier写入文件, 并让client读取该文件获取IPC identifier. IPC_PRIVATE通常用于parent-child relationship: parent process创建IPC structure后调用fork(), 其child process由于继承IPC identifier, 因此也可以使用该IPC structure且不需要I/O操作
- Server和client事先准备一个key, sever使用该key创建IPC structure. 问题在于, key可能被其他IPC structure使用并在创建时返回错误, 这时server必须删除现有IPC structure并再次尝试创建新的IPC structure
- Server和client可事先准备一个pathname和project ID(character value betweeen 0 and 255)来调用ftok()获取相应的key
/**
* @brief use the identity of the file named by path and
* the least 8 bits of id to generate a key_t type
* key. When two processes use the same path and id,
* they shall get the same value of key
* @return the generated key_t value on success; -1 on
* error and errno is set
*/
key_t ftok(const char *path, int id);
三种不同的IPC structure(message queue, semaphore, shared memeory)都有对应的创建IPC structure的方法: msgget(), semget(), shmget(). 这三个函数都需要key和flag参数, 以下两种情况会创建一个新的IPC structure:
- key为IPC_PRIVATE
- key没有与现有的IPC structure关联, 且flag设置了IPC_CREAT
若想要使用已被创建的IPC structure, 则需要指定一个已经关联的key, 且不能在flag中设置IPC_CREAT.
5.2 Permission Structure
每个IPC structure都有一个关联的ipc_perm structure. 该structure定义了权限和owner ID:
struct ipc_perm { |
ipc_perm structure中的变量都可以通过IPC structure对应的msgctl(), semctl()或shmctl()修改. mode的表示如下:
Permission | Bit |
---|---|
user-read | 0400 |
user-write(alter) | 0200 |
group-read | 0040 |
group-write(alter) | 0020 |
other-read | 0004 |
other-write(alter) | 0002 |
5.3 Configuration Limits
每个IPC structure都有其limit. 以下是各系统查看IPC-related limits的command:
- Linux: ipcs -l
- FreeBSD and Mac OS X: ipcs -T
- Solaris: sysdef -i
5.4 Advantages and Disadvantages
IPC structure的生存周期与系统生存周期相同. 以message queue为例, 进程的终止并不会让message queue被删除, message queue中的内容也会一直保留, 直到其他进程调用msgrcv(), msgctl(), ipcrm()或系统重启. 相比之下, pipe会在最后一个进程退出时会被自动删除; FIFO虽然不会被自动删除, 但FIFO中的数据会在进程退出时被删除.
IPC structure无法被file system获知, 因为IPC structure没有提供file descriptor. XSI IPC通过提供新的system calls(msgget, semop, shmat等)使得进程可以操作IPC structure; 但这也导致IPC无法使用很多I/O functions(例如: select, poll).
IPC structure也提供了很多pipe和FIFO不具备的特性:
IPC type | Connectionless? | Reliable? | Flow control? | Record? | Message types or priorities |
---|---|---|---|---|---|
message queues | no | yes | yes | yes | yes |
STREAMS | no | yes | yes | yes | yes |
UNIX domain stream socket | no | yes | yes | no | no |
UNIX domain datagram socket | yes | yes | no | yes | no |
FIFOs | no | yes | yes | no | no |
6. Message Queues
Message queue是kernel中的linked list, 并用identifier(也称作queue ID)标示. 进程调用msgget()创建一个新的message queue, 可调用msgsnd()将新信息加入到queue尾部; 调用msgrcv()从queue中取出信息. 每个queue都有一个msqid_ds structure关联:
struct msqid_ds { |
以下是message queue常用函数:
|
7. Semaphores
Semaphore作为一个counter用于控制多个进程访问一个共享资源, 争夺资源的过程如下:
- 获取semaphore
- semaphore > 0: 说明进程可以获取资源, 并将semaphore减一
- semaphore = 0: 说明当前没有资源, 进程进入休眠并等待被唤醒
由于会有多个进程同时操作semaphore, 必须保证semaphore的操作为atomic operation. 最常用的semaphore为binary semaphore(只拥有一个资源, 初始值为1). 但XSI semaphore其包含一些不必要的特性:
- Semaphore并不简单地只是一个非负整数, 而是一个或多个semaphore value的集合. 当创建XSI semaphore时, 必须说明集合中有几个semaphore value.
- 创建semaphore和初始化semaphore分别由两个函数完成: semget()和semctl(). 这导致进程无法原子地创建并初始化semaphore
- 由于XSI IPC都没有自我销毁功能, 必须由进程主动释放
Semaphore在kernel中以semid_ds结构体的形式存在:
struct semid_ds { |
以下是XSI semaphore的常用函数:
|
8. Shared Memory
Shared memory允许多个进程使用同一块内存. Kernel中会给shared memory维持一个structure:
struct shmid_ds { |
以下是shared memory的常用函数:
|