linux内核剖析(八)进程间通信之-管道
管道
管道是一種兩個進程間進行單向通信的機制。
因為管道傳遞數(shù)據(jù)的單向性,管道又稱為半雙工管道。
管道的這一特點決定了器使用的局限性。管道是Linux支持的最初Unix IPC形式之一,具有以下特點:
-
數(shù)據(jù)只能由一個進程流向另一個進程(其中一個讀管道,一個寫管道);如果要進行雙工通信,需要建 立兩個管道。
-
管道只能用于父子進程或者兄弟進程間通信。,也就是說管道只能用于具有親緣關系的進程間通信。
除了以上局限性,管道還有其他一些不足,如管道沒有名字(匿名管道),管道的緩沖區(qū)大小是受限制的。管道所傳輸?shù)氖菬o格式的字節(jié)流。這就需要管道輸入方和輸出方事先約定好數(shù)據(jù)格式。雖然有那么多不足,但對于一些簡單的進程間通信,管道還是完全可以勝任的。
信號和消息的區(qū)別
我們知道,進程間的信號通信機制在傳遞信息時是以信號為載體的,但管道通信機制的信息載體是消息。那么信號和消息之間的區(qū)別在哪里呢?
- 1
- 2
首先,在數(shù)據(jù)內容方面,信號只是一些預定義的代碼,用于表示系統(tǒng)發(fā)生的某一狀況;消息則為一組連續(xù)語句或符號,不過量也不會太大。在作用方面,信號擔任進程間少量信息的傳送,一般為內核程序用來通知用戶進程一些異常情況的發(fā)生;消息則用于進程間交換彼此的數(shù)據(jù)。
在發(fā)送時機方面,信號可以在任何時候發(fā)送;信息則不可以在任何時刻發(fā)送。在發(fā)送者方面,信號不能確定發(fā)送者是誰;信息則知道發(fā)送者是誰。在發(fā)送對象方面,信號是發(fā)給某個進程;消息則是發(fā)給消息隊列。在處理方式上,信號可以不予理會;消息則是必須處理的。在數(shù)據(jù)傳輸效率方面,信號不適合進大量的信息傳輸,因為它的效率不高;消息雖然不適合大量的數(shù)據(jù)傳送,但它的效率比信號強,因此適于中等數(shù)量的數(shù)據(jù)傳送。
管道-流管道-命名管道的區(qū)別
我們知道,命名管道和管道都可以在進程間傳送消息,但它們也是有區(qū)別的。
管道這種通訊方式有兩種限制,
-
一是半雙工的通信,數(shù)據(jù)只能單向流動
-
二是只能在具有親緣關系的進程間使用。
進程的親緣關系通常是指父子進程關系。
流管道s_pipe去除了第一種限制,可以雙向傳輸。
管道可用于具有親緣關系進程間的通信,命名管道name_pipe克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關系進程間的通信;
管道技術只能用于連接具有共同祖先的進程,例如父子進程間的通信,它無法實現(xiàn)不同用戶的進程間的信息共享。再者,管道不能常設,當訪問管道的進程終止時,管道也就撤銷。這些限制給它的使用帶來不少限制,但是命名管道卻克服了這些限制。
命名管道也稱為FIFO,是一種永久性的機構。FIFO文件也具有文件名、文件長度、訪問許可權等屬性,它也能像其它Linux文件那樣被打開、關閉和刪除,所以任何進程都能找到它。換句話說,即使是不同祖先的進程,也可以利用命名管道進行通信。
如果想要全雙工通信,那最好使用Sockets API(linux并不支持s_pipe流管道)。下面我們分別介紹通過管道命令解析管道的技術模型,然后詳細說明用來進行管道編程的編程接口和系統(tǒng)級命令。
管道技術模型
管道技術是Linux操作系統(tǒng)中歷來已久的一種進程間通信機制。
所有的管道技術,無論是半雙工的匿名管道,還是命名管道,它們都是利用FIFO排隊模型來指揮進程間的通信。
對于管道,我們可以形象地把它們當作是連接兩個實體的一個單向連接器。
使用管道進行通信時,兩端的進程向管道讀寫數(shù)據(jù)是通過創(chuàng)建管道時,系統(tǒng)設置的文件描述符進行的。從本質上說,管道也是一種文件,但它又和一般的文件有所不同,可以克服使用文件進行通信的兩個問題,這個文件只存在內存中。
通過管道通信的兩個進程,一個進程向管道寫數(shù)據(jù),另外一個從中讀數(shù)據(jù)。寫入的數(shù)據(jù)每次都添加到管道緩沖區(qū)的末尾,讀數(shù)據(jù)的時候都是從緩沖區(qū)的頭部讀出數(shù)據(jù)的。
管道命令詳解
參見
linux shell 管道命令(pipe)使用及與shell重定向區(qū)別
例如,請看下面的命令
管道符號,是unix功能強大的一個地方,符號是一條豎線:”|”,
用法: command 1 | command 2
他的功能是把第一個命令command 1執(zhí)行的結果作為command 2的輸入傳給command 2
注意:
管道命令只處理前一個命令正確輸出,不處理錯誤輸出
管道命令右邊命令,必須能夠接收標準輸入流命令才行。
例如:
ls -l | more
該命令列出當前目錄中的任何文檔,并把輸出送給more命令作為輸入,more命令分頁顯示文件列表。
管道命令與重定向區(qū)別
區(qū)別是:
-
左邊的命令應該有標準輸出 | 右邊的命令應該接受標準輸入
-
左邊的命令應該有標準輸出 > 右邊只能是文件
-
左邊的命令應該需要標準輸入 < 右邊只能是文件
-
管道觸發(fā)兩個子進程執(zhí)行”|”兩邊的程序;而重定向是在一個進程內執(zhí)行
重定向與管道在使用時候很多時候可以通用
其實,在shell里面,經(jīng)常是條條大路通羅馬的。
一般如果是命令間傳遞參數(shù),還是管道的好,如果處理輸出結果需要重定向到文件,還是用重定向輸出比較好。
前面的例子實際上就是在兩個命令之間建立了一根管道(有時我們也將之稱為命令的流水線操作)。
第一個命令ls執(zhí)行后產生的輸出作為了第二個命令more的輸入。
這是一個半雙工通信,因為通信是單向的。兩個命令之間的連接的具體工作,是由內核來完成的。
當然內核也為我們提供了一套接口(系統(tǒng)調用),除了命令之外,應用程序也可以使用管道進行連接。
管道編程技術
參考 http://www.cppblog.com/jackdongy/archive/2013/01/07/197055.html
http://blog.chinaunix.net/uid-26495963-id-3066282.html
管道的接口
無名管道pipe
創(chuàng)建管道pipe
函數(shù)原型`int pipe(int filedes[2]);
-
pipe()會建立管道,并將文件描述詞由參數(shù) filedes 數(shù)組返回。
-
filedes[0]為管道里的讀取端,所以pipe用read調用的。
-
filedes[1]則為管道的寫入端。使用write進行寫入操作。
返回值
- 若成功則返回零,否則返回-1,錯誤原因存于 errno 中。
錯誤代碼
-
EMFILE 進程已用完文件描述詞最大量
-
ENFILE 系統(tǒng)已無文件描述詞可用。
-
EFAULT 參數(shù) filedes 數(shù)組地址不合法。
當調用成功時,函數(shù)pipe返回值為0,否則返回值為-1。成功返回時,數(shù)組fds被填入兩個有效的文件描述符。數(shù)組的第一個元素中的文件描述符供應用程序讀取之用,數(shù)組的第二個元素中的文件描述符可以用來供應用程序寫入。
關閉管道close
- 關閉管道只是將兩個文件描述符關閉即可,可以使用普通的close函數(shù)逐個關閉。
如果管道的寫入端關閉,但是還有進程嘗試從管道讀取的話,將被返回0,用來指出管道已不可用,并且應當關閉它。如果管道的讀出端關閉,但是還有進程嘗試向管道寫入的話,試圖寫入的進程將收到一個SIGPIPE信號,至于信號的具體處理則要視其信號處理程序而定了。
dup函數(shù)和dup2函數(shù)
dup和dup2也是兩個非常有用的調用,它們的作用都是用來復制一個文件的描述符。
它們經(jīng)常用來重定向進程的stdin、stdout和stderr。
這兩個函數(shù)的原型如下所示:
#include <unistd.h>int dup( int oldfd );int dup2( int oldfd, int targetfd )?
dup函數(shù)
利用函數(shù)dup,我們可以復制一個描述符。傳給該函數(shù)一個既有的描述符,它就會返回一個新的描述符,這個新的描述符是傳給它的描述符的拷貝。這意味著,這兩個描述符共享同一個數(shù)據(jù)結構。
例如,如果我們對一個文件描述符執(zhí)行l(wèi)seek操作,得到的第一個文件的位置和第二個是一樣的。下面是用來說明dup函數(shù)使用方法的代碼片段:
int fd1, fd2; fd2 = dup( fd1 );?
需要注意的是,我們可以在調用fork之前建立一個描述符,這與調用dup建立描述符的效果是一樣的,子進程也同樣會收到一個復制出來的描述符。
dup2函數(shù)
dup2函數(shù)跟dup函數(shù)相似,但dup2函數(shù)允許調用者規(guī)定一個有效描述符和目標描述符的id。
dup2函數(shù)成功返回時,目標描述符(dup2函數(shù)的第二個參數(shù))將變成源描述符(dup2函數(shù)的第一個參數(shù))的復制品,換句話說,兩個文件描述符現(xiàn)在都指向同一個文件,并且是函數(shù)第一個參數(shù)指向的文件。
下面我們用一段代碼加以說明:
int oldfd; oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 ); dup2( oldfd, 1 ); close( oldfd );?
我們打開了一個新文件,稱為“app_log”,并收到一個文件描述符,該描述符叫做fd1。我們調用dup2函數(shù),參數(shù)為oldfd和1,這會導致用我們新打開的文件描述符替換掉由1代表的文件描述符(即stdout,因為標準輸出文件的id為1)。任何寫到stdout的東西,現(xiàn)在都將改為寫入名為“app_log”的文件中。需要注意的是,dup2函數(shù)在復制了oldfd之后,會立即將其關閉,但不會關掉新近打開的文件描述符,因為文件描述符1現(xiàn)在也指向它。
命名管道m(xù)kfifo
mkfifo函數(shù)的作用是在文件系統(tǒng)中創(chuàng)建一個文件,該文件用于提供FIFO功能,即命名管道。前邊講的那些管道都沒有名字,因此它們被稱為匿名管道,或簡稱管道。對文件系統(tǒng)來說,匿名管道是不可見的,它的作用僅限于在父進程和子進程兩個進程間進行通信。而命名管道是一個可見的文件,因此,它可以用于任何兩個進程之間的通信,不管這兩個進程是不是父子進程,也不管這兩個進程之間有沒有關系。Mkfifo函數(shù)的原型如下所示:
#include <sys/types.h> #include <sys/stat.h> int mkfifo( const char *pathname, mode_t mode );?
mkfifo函數(shù)需要兩個參數(shù),第一個參數(shù)(pathname)是將要在文件系統(tǒng)中創(chuàng)建的一個專用文件。第二個參數(shù)(mode)用來規(guī)定FIFO的讀寫權限。Mkfifo函數(shù)如果調用成功的話,返回值為0;如果調用失敗返回值為-1。下面我們以一個實例來說明如何使用mkfifo函數(shù)建一個fifo,具體代碼如下所示:
int ret;ret = mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );if (ret == 0) { // 成功建立命名管道 } else { // 創(chuàng)建命名管道失敗 }?
在這個例子中,利用/tmp目錄中的cmd_pipe文件建立了一個命名管道(即fifo)。之后,就可以打開這個文件進行讀寫操作,并以此進行通信了。命名管道一旦打開,就可以利用典型的輸入輸出函數(shù)從中讀取內容。舉例來說,下面的代碼段向我們展示了如何通過fgets函數(shù)來從管道中讀取內容:
pfp = fopen( "/tmp/cmd_pipe", "r" );ret = fgets( buffer, MAX_LINE, pfp );?
我們還能向管道中寫入內容,下面的代碼段向我們展示了利用fprintf函數(shù)向管道寫入的具體方法:
pfp = fopen( "/tmp/cmd_pipe", "w+ );ret = fprintf( pfp, "Here’s a test string!\n" );?
對命名管道來說,除非寫入方主動打開管道的讀取端,否則讀取方是無法打開命名管道的。Open調用執(zhí)行后,讀取方將被鎖住,直到寫入方出現(xiàn)為止。盡管命名管道有這樣的局限性,但它仍不失為一種有效的進程間通信工具。
無名管道
無名管道為建立管道的進程及其子孫提供一條以比特流方式傳送消息的通信管道。
該管道再邏輯上被看作管道文件,在物理上則由文件系統(tǒng)的高速緩沖區(qū)構成,而很少啟動外設。
發(fā)送進程利用文件系統(tǒng)的系統(tǒng)調用write(fd[1],buf,size),把buf 中的長度為size字符的消息送入管道入口fd[1],
接收進程則使用系統(tǒng)調用read(fd[0],buf,size)從管道出口fd[0]出口讀出size字符的消息置入buf中。
這里,管道按FIFO(先進先出)方式傳送消息,且只能單向傳送消息(如圖)。
無名管道pipe讀寫
管道用于不同進程間通信。通常先創(chuàng)建一個管道,再通過fork函數(shù)創(chuàng)建一個子進程,該子進程會繼承父進程創(chuàng)建的管道。注意事項:必須在系統(tǒng)調用fork()前調用pipe(),否則子進程將不會繼承文件描述符。否則,會創(chuàng)建兩個管道,因為父子進程共享同一段代碼段,都會各自調用pipe(),即建立兩個管道,出現(xiàn)異常錯誤。
無名管道讀寫過程如圖所示
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #define MAX_DATA_LEN 256 #define DELAY_TIME 1 int main(void) { pid_t pid; char buf[MAX_DATA_LEN]; const char *data="Pipe Test program"; int real_read, real_write; int pipe_fd[2]; memset((void*)buf, 0, sizeof(buf)); if(pipe(pipe_fd) < 0) { perror("Pipe create error...\n"); exit(1); } else { printf("Pipe create success...\n"); } if ((pid = fork()) < 0) { perror("Fork error!\n"); exit(1); } else if (pid == 0) { printf("I am the child process, PID = %d, PPID = %d", getpid(), getppid()); close(pipe_fd[1]); sleep(DELAY_TIME * 3); if ((real_read=read(pipe_fd[0],buf, MAX_DATA_LEN)) > 0) { printf("Child receive %d bytes from pipe: '%s'.\n", real_read, buf); } close(pipe_fd[0]); exit(0); } else { printf("I am the parent process, PID = %d, PPID = %d", getpid(), getppid()); close(pipe_fd[0]); sleep(DELAY_TIME); if ((real_write = write(pipe_fd[1], data, strlen(data))) > 0) { printf("Parent write %d bytes into pipe: '%s'.\n", real_write, data); } close(pipe_fd[1]); waitpid(pid,NULL,0); exit(0); } return EXIT_SUCCESS; }?
多進程管道讀寫
建立一個管道。同時,父進程生成子進程P1,P2,這兩個進程分別向管道中寫入各自的字符串,父進程讀出它們(如圖)。
#include < stdio.h> main( ) { int I,r,p1,p2,fd[2]; char buf[50],s[50]; pipe(fd); /*父進程建立管道*/ while((p1=fork()) = = -1); if(p1 = = 0 ) { lockf(fd[1],1,0); /*加鎖鎖定寫入端*/ sprinrf(buf, ”child process P1 is sending messages! \n”); printf(“child process P1! \n”); write(fd[1],buf, 50); /*把buf中的50個字符寫入管道*/ sleep(5); lockf(fd[1],0,0); /*釋放管道寫入端*/ exit(0); /*關閉P1*/ } else /*從父進程返回,執(zhí)行父進程*/ { while((p2=fork()) = = -1); /*創(chuàng)建子進程P2,失敗時循環(huán)*/ if(p2 = = 0) /*從子進程P2返回,執(zhí)行P2*/ { lockf(fd[1],1,0); / *鎖定寫入端*/ sprintf(buf, ”child process P2 is sending messages \n”); printf(“child process P2 ! \n”); write(fd[1],buf,50); /*把buf中字符寫入管道*/ sleep(5); /* 睡眠等待*/ lockf (fd[1],0,0); /*釋放管道寫入端*/ exit(0); /*關閉P2*/ } wait(0); if (r = read(fd[0],s 50) = = -1) printf(“can’t read pipe \n”); else printf(“%s\n”,s); wait(0); if(r = read(fd[0],s,50)= = -1) printf(“can’t read pipe \n”); else printf((“%s\n”,s); exit(0); } }?
使用dup函數(shù)實現(xiàn)指令流水
我們的子進程把它的輸出重定向的管道的輸入,然后,父進程將它的輸入重定向到管道的輸出。這在實際的應用程序開發(fā)中是非常有用的一種技術。
#include <stdio.h> #include <stdlib.h> #include <unistd.h>int main() {int pfds[2]; if ( pipe(pfds) == 0 ) { if ( fork() == 0 ) { close(1); dup2( pfds[1], 1 ); close( pfds[0] ); execlp( "ls", "ls", "-1", NULL ); } else { close(0); dup2( pfds[0], 0 ); close( pfds[1] ); execlp( "wc", "wc", "-l", NULL ); } } return 0; }?
命名管道
write端
#include <stdio.h> #include <stdlib.h> #include <string.h>#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <unistd.h> #include <fcntl.h> #define FIFO "myfifo" #define BUFF_SIZE 1024 int main(int argc,char* argv[]) { char buff[BUFF_SIZE]; int real_write; int fd; if(argc <= 1) { printf("Usage: %s string\n", argv[0]); exit(1); } else { printf("%s at PID = %d\n", argv[0], getpid()); } sscanf(argv[1], "%s", buff); // 測試FIFO是否存在,若不存在,mkfifo一個FIFO if(access(FIFO, F_OK) == -1) { if((mkfifo(FIFO, 0666) < 0) && (errno != EEXIST)) { printf("Can NOT create fifo file!\n"); exit(1); } } // 調用open以只寫方式打開FIFO,返回文件描述符fd if((fd = open(FIFO, O_WRONLY)) == -1) { printf("Open fifo error!\n"); exit(1); } // 調用write將buff寫到文件描述符fd指向的FIFO中 if ((real_write = write(fd, buff, BUFF_SIZE)) > 0) { printf("Write into pipe: '%s'.\n", buff); exit(1); } close(fd); exit(0); }?
read端
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <unistd.h> #include <fcntl.h> #define FIFO "myfifo" #define BUFF_SIZE 1024 int main(int argc, char *argv[]) { char buff[BUFF_SIZE]; int real_read; int fd; printf("%s at PID = %d ", argv[0], getpid()); // access確定文件或文件夾的訪問權限。即,檢查某個文件的存取方式 // 如果指定的存取方式有效,則函數(shù)返回0,否則函數(shù)返回-1 // 若不存在FIFO,則創(chuàng)建一個 if(access(FIFO, F_OK) == -1) { if((mkfifo(FIFO, 0666) < 0) && (errno != EEXIST)) { printf("Can NOT create fifo file!\n"); exit(1); } } // 以只讀方式打開FIFO,返回文件描述符fd if((fd = open(FIFO, O_RDONLY)) == -1) { printf("Open fifo error!\n"); exit(1); } // 調用read將fd指向的FIFO的內容,讀到buff中,并打印 while(1) { memset(buff, 0, BUFF_SIZE); if ((real_read = read(fd, buff, BUFF_SIZE)) > 0) { printf("Read from pipe: '%s'.\n",buff); } } close(fd); exit(0); }總結
以上是生活随笔為你收集整理的linux内核剖析(八)进程间通信之-管道的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis 和 I/O 多路复用
- 下一篇: linux 其他常用命令