再談IPC中的管道
進程間通信
每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核,在內核中開辟一塊緩沖區,進程1把數據從用戶空間拷到內核緩沖區,進程2再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通信(IPC,InterProcess Communication)。如下圖所示。
圖 30.6. 進程間通信
管道
管道是一種最基本的IPC機制,由pipe
函數創建:
#include <unistd.h> int pipe(int filedes[2]);
調用pipe
函數時在內核中開辟一塊緩沖區(稱為管道)用於通信,它有一個讀端一個寫端,然後通過filedes
filedes[0]
指向管道的讀端,filedes[1]
指向管道的寫端(很好記,就像0是標準輸入1是標準輸出一樣)。所以管道在用戶程序看起來就像一個打開的文件,通過read(filedes[0]);
或者write(filedes[1]);
向這個文件讀寫數據其實是在讀寫內核緩沖區。pipe
函數調用成功返回0,調用失敗返回-1。
開辟了管道之後如何實現兩個進程間的通信呢?比如可以按下面的步驟通信(下圖出自[APUE2e])。
圖 30.7. 管道
-
父進程調用
pipe
開辟管道,得到兩個文件描述符指向管道的兩端。 -
父進程調用
fork
創建子進程,那麽子進程也有兩個文件描述符指向同一管道。 -
父進程關閉管道讀端,子進程關閉管道寫端。父進程可以往管道裏寫,子進程可以從管道裏讀,管道是用環形隊列實現的,數據從寫端流入從讀端流出,這樣就實現了進程間通信。
例 30.7. 管道
#include <stdlib.h> #include <unistd.h> #define MAXLINE 80 int main(void) { int n; int fd[2]; pid_t pid; char line[MAXLINE]; if (pipe(fd) < 0) { perror("pipe"); exit(1); } if ((pid = fork()) < 0) { perror("fork"); exit(1); } if (pid > 0) { /* parent */ close(fd[0]); write(fd[1], "hello world\n", 12); wait(NULL); } else { /* child */ close(fd[1]); n = read(fd[0], line, MAXLINE); write(STDOUT_FILENO, line, n); } return 0; }
使用管道有一些限制:
-
兩個進程通過一個管道只能實現單向通信,比如上面的例子,父進程寫子進程讀,如果有時候也需要子進程寫父進程讀,就必須另開一個管道。請讀者思考,如果只開一個管道,但是父進程不關閉讀端,子進程也不關閉寫端,雙方都有讀端和寫端,為什麽不能實現雙向通信?
-
管道的讀寫端通過打開的文件描述符來傳遞,因此要通信的兩個進程必須從它們的公共祖先那裏繼承管道文件描述符。上面的例子是父進程把文件描述符傳給子進程之後父子進程之間通信,也可以父進程
fork
兩次,把文件描述符傳給兩個子進程,然後兩個子進程之間通信,總之需要通過fork
傳遞文件描述符使兩個進程都能訪問同一管道,它們才能通信。
其它IPC機制
進程間通信必須通過內核提供的通道,而且必須有一種辦法在進程中標識內核提供的某個通道,上一節講的管道是用打開的文件描述符來標識的。如果要互相通信的幾個進程沒有從公共祖先那裏繼承文件描述符,它們怎麽通信呢?內核提供一條通道不成問題,問題是如何標識這條通道才能使各進程都可以訪問它?文件系統中的路徑名是全局的,各進程都可以訪問,因此可以用文件系統中的路徑名來標識一個IPC通道。
FIFO和Unix Domain Socket這兩種IPC機制都是利用文件系統中的特殊文件來標識的。可以用mkfifo
命令創建一個FIFO文件:
$ mkfifo hello $ ls -l hello prw-r--r-- 1 djkings djkings 0 2008-10-30 10:44 hello
FIFO文件在磁盤上沒有數據塊,僅用來標識內核中的一條通道,各進程可以打開這個文件進行read
/write
,實際上是在讀寫內核通道(根本原因在於這個file
結構體所指向的read
、write
函數和常規文件不一樣),這樣就實現了進程間通信。Unix Domain Socket和FIFO的原理類似,也需要一個特殊的socket文件來標識內核中的通道,例如/var/run
目錄下有很多系統服務的socket文件
再談IPC中的管道