Linux內核解析:進程間通訊:管道

管道的定義

  • 管道是第一個普遍應用的進程間通訊手段。平常在終端執行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標誌位的話)。shell

管道非法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爲EBADFasync

 
 
 
 
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所示,父進程放棄讀,子進程放棄寫,變成父進程寫入,子進程讀出,成爲一個通訊的通道…
spa

  • 父進程如何放棄讀,子進程又如何放棄寫?其實很簡單,父進程把讀端口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]);}

一樣的道理,對於第二個子進程,如法炮製:3d

 
 
 
 
if(pipefd[0] != STDIN_FILENO){dup2(pipefd[0],STDIN_FILENO);close(pipefd[0]);}
  • 簡單來講,就是第一個子進程的標準輸出被綁定到了管道的寫入端,因而第一個命令的輸出,寫入了管道,而第二個子進程管道將其標準輸入綁定到管道的讀取端,只要管道里面有了內容,這些內容就成了標準輸入。指針

  • 兩個示例代碼,爲何要判斷管道的文件描述符是否等於標準輸入和標準輸出呢?緣由是,在調用pipe時,進程極可能已經關閉了標準輸入和標準輸出,調用pipe函數時,內核會分配最小的文件描述符,因此pipe的文件描述符可能等於0或1。在這種狀況下,若是沒有if判斷加以保護,代碼就變成了:code

 
 
 
 
dup2(1,1);close(1);

這樣的話,第一行代碼什麼也沒作,第二行代碼就把管道的寫入端給關閉了,因而便沒法傳遞信息了orm

與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 8192void 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子進程都會收到信號。


相關文章
相關標籤/搜索