程序通訊(1)管道
程序之間通訊的方式有很多種,主要包括
- 管道
- 命名管道
- 訊號
- 訊息佇列
- 共享記憶體
- 訊號量
- 套接字
其中,管道是最早的一種程序間通訊機制,主要適用於具有親緣關係之間的程序間通訊,比如,父程序與子程序之間,或者同一個父程序的兩個子程序之間。同時,管道是一中半雙工的通訊,資料只能單向流動,從一段寫入,另外一段讀出。下面通過幾個例子來看一下管道如何使用。
1. 函式原型
#include <unistd.h>
int pipe(int fd[2]);
pipe函式建立一個管道,其宣告在unistd.h當中,傳入引數是一個int[2]陣列,返回值如果為0表示,pipe建立成功,同時fd陣列中儲存兩個檔案描述符,fd[1]指向管道的寫端,fd[0]指向管道的讀端;如果小於0,表示建立失敗。
2. 第一個例子
下面看一個最簡單的例子
#include<unistd.h> #include<stdio.h> int main() { int n ; int fd[2]; char buf[1024]; // 建立管道 if (pipe(fd) < 0) { perror("pipe error"); } write(fd[1], "hello world\n", 12); n = read(fd[0], buf, 1024); printf("%s",buf); return 0; }
上面的程式碼很簡單,就是建立了一個管道,然後向寫端寫入“hello world\n”字串,然後從讀端讀出,儲存到buf陣列中,最後列印到螢幕。這個例子可以用下面的示意圖來表示:
管道像一根單向的水管,資料像水一樣從一端流入,從另一端流出。管道也是有緩衝空間的,如果一直寫入不讀取,那麼緩衝空間會被佔滿,再往裡面寫資料就會失敗(就像水管的流出端被關閉,水不能再流入一樣),同樣的,如果只讀不寫,那麼資料被讀完之後,就沒有東西可讀了,再次讀取也會失敗。這個例子顯然是沒什麼用途的,但是可以幫助我們理解什麼是管道。
2. 第二個例子 程序間通訊
再來看第二個例子
#include<unistd.h> #include<stdio.h> int main() { int n ; int status; pid_t pid; int fd[2]; char buf[1024]; // 建立管道 if (pipe(fd) < 0) { perror("pipe error"); } // 建立子程序 if ((pid = fork()) < 0) { perror("fork error"); } else if (pid == 0) { // 子程序讀取管道 n = read(fd[0], buf, 1024); printf("%s",buf); } else { // 父程序寫入管道 write(fd[1], "hello world\n", 12); // 父程序等待子程序結束 if (wait(&status) < 0) { perror("wait error"); } } return 0; }
第二個例子比第一個例子稍微複雜了一些。首先,父程序建立了一個管道,然後fork出一個子程序。在子程序中讀取管道內容,並列印內容,然後返回結束程序;在父程序中向管道寫入字串,然後等待子程序結束,最後結束程序。這個例子可以用下面的圖來表示
執行fork之後,主程序建立了一個子程序,子程序完全複製父程序的虛擬記憶體空間(這個說法其實不嚴謹,見後文),同時繼承父程序開啟的檔案等資源,所以兩個描述符也被繼承下來,兩個程序的fd陣列具有相同的值,並且指向同樣的pipe埠。因此,父子程序之前可以通過pipe實現通訊。一般地,我們會在pipe寫入端程序關閉讀端,在pipe讀入端關閉寫端。在上面的程式碼中,加入兩行
#include<unistd.h>
#include<stdio.h>
int main()
{
int n ;
int status;
pid_t pid;
int fd[2];
char buf[1024];
// 建立管道
if (pipe(fd) < 0) {
perror("pipe error");
}
// 建立子程序
if ((pid = fork()) < 0) {
perror("fork error");
} else if (pid == 0) {
// 關閉寫入端檔案描述符
close(fd[1]);
// 子程序讀取管道
n = read(fd[0], buf, 1024);
printf("%s",buf);
} else {
// 關閉寫入端檔案描述符
close(fd[0]);
// 父程序寫入管道
write(fd[1], "hello world\n", 12);
// 父程序等待子程序結束
if (wait(&status) < 0) {
perror("wait error");
}
}
return 0;
}
那麼程序模型變成這樣
3. 第三個例子,管道與重定向
第三個例子,我們把子程序的標準輸入重定向到管道的讀埠
#include<unistd.h>
#include<stdio.h>
int main()
{
int n ;
int status;
pid_t pid;
int fd[2];
char buf[1024];
if (pipe(fd) < 0) {
perror("pipe error");
}
if ((pid = fork()) < 0) {
perror("fork error");
} else if (pid == 0) {
close(fd[1]);
if (fd[0] != STDIN_FILENO) {
// 把stdin重定向到管道的輸入端
if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) {
perror("dup2 error");
}
// 重定向成功的話,那麼讀端就有了兩個檔案描述符,
//分別是STDIN_FILENO和fd[0],此時可以關閉fd[0],保留STDIN_FILENO即可
close(fd[0]);
}
//此時可以通過STDIN_FILENO讀取管道內容
n = read(STDIN_FILENO, buf, 1024);
printf("%s",buf);
} else {
close(fd[0]);
write(fd[1], "hello world\n", 12);
if (wait(&status) < 0) {
perror("wait error");
}
}
return 0;
}
TIPS:
主程序建立了一個子程序,子程序完全複製父程序的虛擬記憶體空間這個說法其實是不嚴謹的,父子程序的程式碼段實際上是共用的,另外完全複製父程序的虛擬記憶體空間,會造成時間和記憶體上的浪費,有的時候根本沒有必要完全複製,因此出現了cow(copy on write)技術,也就是“寫時複製”,就是當子程序對某個變數進行寫操作時,才進行復制。這個技術對使用者程式是不可見的,因此,在使用者程式層面上,認為子程序完全複製父程序的虛擬記憶體空間是完全沒有問題的。