管道的用途
管道的操作
管道非法read與write內核實現解析
管道通信原理及其親戚通信解析
父子進程通信解析
親緣關系的進程管道通信解析
管道的注意事項及其性質
管道有以下三條性質
shell管道的實現
與shell命令進行通信
system函數與popen函數區別
管道的定義
- 管道是第一個廣泛應用的進程間通信手段。日常在終端執行shell命令時,會大量用到管道。但管道的缺陷在於只能在有親緣關系(有共同的祖先)的進程之間使用。為了突破這個限制,后來引入了命名管道。

管道的用途
- 管道是最早出現的進程間通信的手段。在shell中執行命令,經常會將上一個命令的輸出作為下一個命令的輸入,由多個命令配合完成一件事情。而這就是通過管道來實現的。
在圖9-3中,進程who的標准輸出,通過管道傳遞給下游的wc進程作為標准輸入,從而通過相互配合完成了一件任務。

管道的操作
- 管道的作用是在具有親緣關系的進程之間傳遞消息,所謂有親緣關系,是指有同一個祖先。所以管道並不是只可以用於父子進程通信,也可以在兄弟進程之間還可以用在祖孫之間等,反正只要共同的祖先調用了pipe函數,打開的管道文件就會在fork之后,被各個后代所共享。
- 不過由於管道是字節流通信,沒有消息邊界,多個進程同時發送的字節流混在一起,則無法分辨消息,所有管道一般用於2個進程之間通信,另外管道的內容讀完后不會保存,管道是單向的,一邊要么讀,一邊要么寫,不可以又讀又寫,想要一邊讀一邊寫,那就創建2個管道,如下圖

- 管道是一種文件,可以調用read、write和close等操作文件的接口來操作管道。另一方面管道又不是一種普通的文件,它屬於一種獨特的文件系統:pipefs。管道的本質是內核維護了一塊緩沖區與管道文件相關聯,對管道文件的操作,被內核轉換成對這塊緩沖區內存的操作。下面我們來看一下如何使用管道。
#include<unistd.h>
int pipe(int fd[2])
如果成功,則返回值是0,如果失敗,則返回值是-1,並且設置errno。
成功調用pipe函數之后,會返回兩個打開的文件描述符,一個是管道的讀取端描述符pipefd[0],另一個是管道的寫入端描述符pipefd[1]。管道沒有文件名與之關聯,因此程序沒有選擇,只能通過文件描述符來訪問管道,只有那些能看到這兩個文件描述符的進程才能夠使用管道。那么誰能看到進程打開的文件描述符呢?只有該進程及該進程的子孫進程才能看到。這就限制了管道的使用范圍。
- 成功調用pipe函數之后,可以對寫入端描述符pipefd[1]調用write,向管道里面寫入數據,代碼如下所示:
write(pipefd[1],wbuf,count);
一旦向管道的寫入端寫入數據后,就可以對讀取端描述符pipefd[0]調用read,讀出管道里面的內容。如下所示,管道上的read調用返回的字節數等於請求字節數和管道中當前存在的字節數的最小值。如果當前管道為空,那么read調用會阻塞(如果沒有設置O_NONBLOCK標志位的話)。
管道非法read與write內核實現解析
調用pipe函數返回的兩個文件描述符中,讀取端pipefd[0]支持的文件操作定義在read_pipefifo_fops,寫入端pipefd[1]支持的文件操作定義在write_pipefifo_fops,其定義如下:
const struct file_operations read_pipefifo_fops = { //讀端相關操作
.llseek = no_llseek,
.read = do_sync_read,
.aio_read = pipe_read,
.write = bad_pipe_w, //一旦寫,將調用bad_pipe_w
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.open = pipe_read_open,
.release = pipe_read_release,
.fasync = pipe_read_fasync,
};
const struct file_operations write_pipefifo_fops = {//寫端相關操作
.llseek = no_llseek,
.read = bad_pipe_r, //一旦讀,將調用bad_pipe_r
.write = do_sync_write,
.aio_write = pipe_write,
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.open = pipe_write_open,
.release = pipe_write_release,
.fasync = pipe_write_fasync,
};
我們可以看到,對讀取端描述符執行write操作,內核就會執行bad_pipe_w函數;對寫入端描述符執行read操作,內核就會執行bad_pipe_r函數。這兩個函數比較簡單,都是直接返回-EBADF。因此對應的read和write調用都會失敗,返回-1,並置errno為EBADF。
static ssize_t
bad_pipe_r(struct file filp, char __user buf, size_t count, loff_t ppos)
{
return -EBADF; //返回錯誤
}
static ssize_t
bad_pipe_w(struct file filp, const char __user buf, size_t count,loff_t ppos)
{
return -EBADF;
}
管道通信原理及其親戚通信解析
父子進程通信解析
我們只介紹了pipe函數接口,至今尚看不出來該如何使用pipe函數進行進程間通信。調用pipe之后,進程發生了什么呢?請看圖9-5。
可以看到,調用pipe函數之后,系統給進程分配了兩個文件描述符,即pipe函數返回的兩個描述符。該進程既可以往寫入端描述符寫入信息,也可以從讀取端描述符讀出信息。可是一個進程管道,起不到任何通信的作用。這不是通信,而是自言自語。
如果調用pipe函數的進程隨后調用fork函數,創建了子進程,情況就不一樣了。fork以后,子進程復制了父進程打開的文件描述符(如圖9-6所示),兩條通信的通道就建立起來了。此時,可以是父進程往管道里寫,子進程從管道里面讀;也可以是子進程往管道里寫,父進程從管道里面讀。這兩條通路都是可選的,但是不能都選。原因前面介紹過,管道里面是字節流,父子進程都寫、都讀,就會導致內容混在一起,對於讀管道的一方,解析起來就比較困難。常規的使用方法是父子進程一方只能寫入,另一方只能讀出,管道變成一個單向的通道,以方便使用。如圖9-7所示,父進程放棄讀,子進程放棄寫,變成父進程寫入,子進程讀出,成為一個通信的通道…

- 父進程如何放棄讀,子進程又如何放棄寫?其實很簡單,父進程把讀端口pipefd[0]這個文件描述符關閉掉,子進程把寫端口pipefd[1]這個文件描述符關閉掉就可以了,示例代碼如下:
int pipefd[2];
pipe(pipefd);
switch(fork())
{
case -1:
/fork failed, error handler here/
case 0: /子進程/
close(pipefd[1]) ; /關閉掉寫入端對應的文件描述符/
/子進程可以對pipefd[0]調用read/
break;
default: /父進程/
close(pipefd[0]); /父進程關閉掉讀取端對應的文件描述符/
/父進程可以對pipefd[1]調用write, 寫入想告知子進程的內容/
break
}
親緣關系的進程管道通信解析
- 圖9-8也講述了如何在兄弟進程之間通過管道通信。如圖9-8所示,父進程再次創建一個子進程B,子進程B就持有管道寫入端,這時候兩個子進程之間就可以通過管道通信了。父進程為了不干擾兩個子進程通信,很自覺地關閉了自己的寫入端。從此管道成為了兩個子進程之間的單向的通信通道。在shell中執行管道命令就是這種情景,只是略有特殊之處,其特殊的地方是管道描述符占用了標准輸入和標准輸出兩個文件描述符
管道的注意事項及其性質
管道有以下三條性質
- 只有當所有的寫入端描述符都已經關閉了,而且管道中的數據都被讀出,對讀取描述符調用read函數才返回0(及讀到EOF標志)。
- 如果所有的讀取端描述符都已經關閉了,此時進程再次往管道里面寫入數據,寫操作將會失敗,並且內核會像進程發送一個SIGPIPE信號(默認殺死進程)。
- 當所有的讀端與寫端都已經關閉時,管道才會關閉.
- 就因為有這些特性,我們要即使關閉沒用的管道文件描述符
shell管道的實現
- shell編程會大量使用管道,我們經常看到前一個命令的標准輸出作為后一個命令的標准輸入,來協作完成任務,如圖9-9所示。管道是如何做到的呢?
兄弟進程可以通過管道來傳遞消息,這並不稀奇,前面已經圖示了做法。關鍵是如何使得一個程序的標准輸出被重定向到管道中,而另一個程序的標准輸入從管道中讀取呢?
答案就是復制文件描述符。
對於第一個子進程,執行dup2之后,標准輸出對應的文件描述符1,也成為了管道的寫入端。這時候,管道就有了兩個寫入端,按照前面的建議,需要關閉不相干的寫入端,使讀取端可以順利地讀到EOF,所以應將剛開始分配的管道寫入端的文件描述符pipefd[1]關閉掉。
if(pipefd[1] != STDOUT_FILENO)
{
dup2(pipefd[1],STDOUT_FILENO);
close(pipefd[1]);
}
同樣的道理,對於第二個子進程,如法炮制:
if(pipefd[0] != STDIN_FILENO)
{
dup2(pipefd[0],STDIN_FILENO);
close(pipefd[0]);
}
簡單來說,就是第一個子進程的標准輸出被綁定到了管道的寫入端,於是第一個命令的輸出,寫入了管道,而第二個子進程管道將其標准輸入綁定到管道的讀取端,只要管道里面有了內容,這些內容就成了標准輸入。
兩個示例代碼,為什么要判斷管道的文件描述符是否等於標准輸入和標准輸出呢?原因是,在調用pipe時,進程很可能已經關閉了標准輸入和標准輸出,調用pipe函數時,內核會分配最小的文件描述符,所以pipe的文件描述符可能等於0或1。在這種情況下,如果沒有if判斷加以保護,代碼就變成了:
dup2(1,1);
close(1);
這樣的話,第一行代碼什么也沒做,第二行代碼就把管道的寫入端給關閉了,於是便無法傳遞信息了
與shell命令進行通信
道的一個重要作用是和外部命令進行通信。在日常編程中,經常會需要調用一個外部命令,並且要獲取命令的輸出。而有些時候,需要給外部命令提供一些內容,讓外部命令處理這些輸入。Linux提供了popen接口來幫助程序員做這些事情。
就像system函數,即使沒有system函數,我們通過fork、exec及wait家族函數一樣也可以實現system的功能。但終歸是不方便,system函數為我們提供了一些便利。同樣的道理,只用pipe函數及dup2等函數,也能完成popen要完成的工作,但popen接口給我們提供了便利。
popen接口定義如下:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen函數會創建一個管道,並且創建一個子進程來執行shell,shell會創建一個子進程來執行command。根據type值的不同,分成以下兩種情況。
如果type是r:command執行的標准輸出,就會寫入管道,從而被調用popen的進程讀到。通過對popen返回的FILE類型指針執行read或fgets等操作,就可以讀取到command的標准輸出,如圖9-10所示。
如果type是w:調用popen的進程,可以通過對FILE類型的指針fp執行write、fputs等操作,負責往管道里面寫入,寫入的內容經過管道傳給執行command的進程,作為命令的輸入,如圖9-11所示
popen函數成功時,會返回stdio庫封裝的FILE類型的指針,失敗時會返回NULL,並且設置errno。常見的失敗有fork失敗,pipe失敗,或者分配內存失敗。
I/O結束了以后,可以調用pclose函數來關閉管道,並且等待子進程的退出。盡管popen函數返回的是FILE類型的指針,也不應調用fclose函數來關閉popen函數打開的文件流指針,因為fclose不會等待子進程的退出。pclose函數成功時會返回子進程中shell的終止狀態。popen函數和system函數類似,如果command對應的命令無法執行,就如同執行了exit(127)一樣。如果發生其他錯誤,pclose函數則返回-1。可以從errno中獲取到失敗的原因。
下面給出一個簡單的例子,來示范下popen的用法:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<sys/wait.h>
#include<signal.h>
#define MAX_LINE_SIZE 8192
void print_wait_exit(int status)
{
printf("status = %d\n",status);
if(WIFEXITED(status))
{
printf("normal termination,exit status = %d\n",WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))
{
printf("abnormal termination,signal number =%d%s\n",
WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status)?"core file generated" : "");
#else
"");
#endif
}
}
int main(int argc ,char* argv[])
{
FILE *fp = NULL ;
char command[MAX_LINE_SIZE],buffer[MAX_LINE_SIZE];
if(argc != 2 )
{
fprintf(stderr,"Usage: %s filename \n",argv[0]);
exit(1);
}
snprintf(command,sizeof(command),"cat %s",argv[1]);
fp = popen(command,"r");
if(fp == NULL)
{
fprintf(stderr,"popen failed (%s)",strerror(errno));
exit(2);
}
while(fgets(buffer,MAX_LINE_SIZE,fp) != NULL)
{
fprintf(stdout,"%s",buffer);
}
int ret = pclose(fp);
if(ret == 127 )
{
fprintf(stderr,"bad command : %s\n",command);
exit(3);
}
else if(ret == -1)
{
fprintf(stderr,"failed to get child status (%s)\n",
strerror(errno));
exit(4);
}
else
{
print_wait_exit(ret);
}
exit(0);
}
- 將文件名作為參數傳遞給程序,執行cat filename的命令。popen創建子進程來負責執行cat filename的命令,子進程的標准輸出通過管道傳給父進程,父進程可以通過fgets來讀取command的標准輸出。
system函數與popen函數區別
- popen函數和system有很多相似的地方,但是也有顯著的不同。調用system函數時,shell命令的執行被封裝在了函數內部,所以若system函數不返回,調用system的進程就不再繼續執行。但是popen函數不同,一旦調用popen函數,調用進程和執行command的進程便處於並行狀態。然后pclose函數才會關閉管道,等待執行command的進程退出。換句話說,在popen之后,pclose之前,調用popen的進程和執行command的進程是並行的,這種差異帶來了兩種顯著的不同:
- 在並行期間,調用popen的進程可能會創建其他子進程,所以標准規定popen不能阻塞SIGCHLD信號.這也意味着,popen創建的子進程可能被提前執行的等待操作所捕獲。若發生這種情況,調用pclose函數時,已經無法等待command子進程的退出,這種情況下,將返回-1,並且errno為ECHILD。
- 調用進程和command子進程是並行的,所以標准要求popen不能忽略SIGINT和SIGQUIT信號。如果是從鍵盤產生的上述信號,那么,調用進程和command子進程都會收到信號。
