Linux通過匿名管道進行進程間通信
本文研究的主要是Linux通過匿名管道進行進程間通信的相關(guān)內(nèi)容,具體介紹如下。
在前面,介紹了一種進程間的通信方式:使用信號,我們創(chuàng)建通知事件,并通過它引起響應(yīng),但傳遞的信息只是一個信號值。這里將介紹另一種進程間通信的方式——匿名管道,通過它進程間可以交換更多有用的數(shù)據(jù)。
一、什么是管道
如果你使用過Linux的命令,那么對于管道這個名詞你一定不會感覺到陌生,因為我們通常通過符號“|"來使用管道,但是管理的真正定義是什么呢?管道是一個進程連接數(shù)據(jù)流到另一個進程的通道,它通常是用作把一個進程的輸出通過管道連接到另一個進程的輸入。
舉個例子,在shell中輸入命令:ls -l | grep string,我們知道ls命令(其實也是一個進程)會把當(dāng)前目錄中的文件都列出來,但是它不會直接輸出,而是把本來要輸出到屏幕上的數(shù)據(jù)通過管道輸出到grep這個進程中,作為grep這個進程的輸入,然后這個進程對輸入的信息進行篩選,把存在string的信息的字符串(以行為單位)打印在屏幕上。
二、使用popen函數(shù)
1、popen函數(shù)和pclose函數(shù)介紹
有靜就有動,有開就有關(guān),與此相同,與popen函數(shù)相對應(yīng)的函數(shù)是pclose函數(shù),它們的原型如下:
#include <stdio.h> FILE* popen (const char *command, const char *open_mode); int pclose(FILE *stream_to_close);
poen函數(shù)允許一個程序?qū)⒘硪粋€程序作為新進程來啟動,并可以傳遞數(shù)據(jù)給它或者通過它接收數(shù)據(jù)。command是要運行的程序名和相應(yīng)的參數(shù)。open_mode只能是"r(只讀)"和"w(只寫)"的其中之一。注意,popen函數(shù)的返回值是一個FILE類型的指針,而Linux把一切都視為文件,也就是說我們可以使用stdio I/O庫中的文件處理函數(shù)來對其進行操作。
如果open_mode是"r",主調(diào)用程序就可以使用被調(diào)用程序的輸出,通過函數(shù)返回的FILE指針,就可以能過stdio函數(shù)(如fread)來讀取程序的輸出;如果open_mode是"w",主調(diào)用程序就可以向被調(diào)用程序發(fā)送數(shù)據(jù),即通過stdio函數(shù)(如fwrite)向被調(diào)用程序?qū)憯?shù)據(jù),而被調(diào)用程序就可以在自己的標(biāo)準(zhǔn)輸入中讀取這些數(shù)據(jù)。
pclose函數(shù)用于關(guān)閉由popen創(chuàng)建出的關(guān)聯(lián)文件流。pclose只在popen啟動的進程結(jié)束后才返回,如果調(diào)用pclose時被調(diào)用進程仍在運行,pclose調(diào)用將等待該進程結(jié)束。它返回關(guān)閉的文件流所在進程的退出碼。
2、例子
很多時候,我們根本就不知道輸出數(shù)據(jù)的長度,為了避免定義一個非常大的數(shù)組作為緩沖區(qū),我們可以以塊的方式來發(fā)送數(shù)據(jù),一次讀取一個塊的數(shù)據(jù)并發(fā)送一個塊的數(shù)據(jù),直到把所有的數(shù)據(jù)都發(fā)送完。下面的例子就是采用這種方式的數(shù)據(jù)讀取和發(fā)送方式。源文件名為popen.c,代碼如下:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main() { FILE *read_fp = NULL; FILE *write_fp = NULL; char buffer[BUFSIZ + 1]; int chars_read = 0; //初始化緩沖區(qū) memset(buffer, '\0', sizeof(buffer)); //打開ls和grep進程 read_fp = popen("ls -l", "r"); write_fp = popen("grep rwxrwxr-x", "w"); //兩個進程都打開成功 if(read_fp && write_fp) { //讀取一個數(shù)據(jù)塊 chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp); while(chars_read > 0) { buffer[chars_read] = '\0'; //把數(shù)據(jù)寫入grep進程 fwrite(buffer, sizeof(char), chars_read, write_fp); //還有數(shù)據(jù)可讀,循環(huán)讀取數(shù)據(jù),直到讀完所有數(shù)據(jù) chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp); } //關(guān)閉文件流 pclose(read_fp); pclose(write_fp); exit(EXIT_SUCCESS); } exit(EXIT_FAILURE); }
運行結(jié)果如下:
從運行結(jié)果來看,達(dá)到了信息篩選的目的。程序在進程ls中讀取數(shù)據(jù),再把數(shù)據(jù)發(fā)送到進程grep中進行篩選處理,相當(dāng)于在shell中直接輸入命令:ls -l | grep rwxrwxr-x。
3、popen的實現(xiàn)方式及優(yōu)缺點
當(dāng)請求popen調(diào)用運行一個程序時,它首先啟動shell,即系統(tǒng)中的sh命令,然后將command字符串作為一個參數(shù)傳遞給它。
這樣就帶來了一個優(yōu)點和一個缺點。優(yōu)點是:在Linux中所有的參數(shù)擴展都是由shell來完成的。所以在啟動程序(command中的命令程序)之前先啟動shell來分析命令字符串,也就可以使各種shell擴展(如通配符)在程序啟動之前就全部完成,這樣我們就可以通過popen啟動非常復(fù)雜的shell命令。
而它的缺點就是:對于每個popen調(diào)用,不僅要啟動一個被請求的程序,還要啟動一個shell,即每一個popen調(diào)用將啟動兩個進程,從效率和資源的角度看,popen函數(shù)的調(diào)用比正常方式要慢一些。
三、pipe調(diào)用
如果說popen是一個高級的函數(shù),pipe則是一個底層的調(diào)用。與popen函數(shù)不同的是,它在兩個進程之間傳遞數(shù)據(jù)不需要啟動一個shell來解釋請求命令,同時它還提供對讀寫數(shù)據(jù)的更多的控制。
pipe函數(shù)的原型如下:
#include <unistd.h> int pipe(int file_descriptor[2]);
我們可以看到pipe函數(shù)的定義非常特別,該函數(shù)在數(shù)組中墻上兩個新的文件描述符后返回0,如果返回返回-1,并設(shè)置errno來說明失敗原因。
數(shù)組中的兩個文件描述符以一種特殊的方式連接起來,數(shù)據(jù)基于先進先出的原則,寫到file_descriptor[1]的所有數(shù)據(jù)都可以從file_descriptor[0]讀回來。由于數(shù)據(jù)基于先進先出的原則,所以讀取的數(shù)據(jù)和寫入的數(shù)據(jù)是一致的。
特別提醒:
1、從函數(shù)的原型我們可以看到,它跟popen函數(shù)的一個重大區(qū)別是,popen函數(shù)是基于文件流(FILE)工作的,而pipe是基于文件描述符工作的,所以在使用pipe后,數(shù)據(jù)必須要用底層的read和write調(diào)用來讀取和發(fā)送。
2、不要用file_descriptor[0]寫數(shù)據(jù),也不要用file_descriptor[1]讀數(shù)據(jù),其行為未定義的,但在有些系統(tǒng)上可能會返回-1表示調(diào)用失敗。數(shù)據(jù)只能從file_descriptor[0]中讀取,數(shù)據(jù)也只能寫入到file_descriptor[1],不能倒過來。
例子:
首先,我們在原先的進程中創(chuàng)建一個管道,然后再調(diào)用fork創(chuàng)建一個新的進程,最后通過管道在兩個進程之間傳遞數(shù)據(jù)。源文件名為pipe.c,代碼如下:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main() { int data_processed = 0; int filedes[2]; const char data[] = "Hello pipe!"; char buffer[BUFSIZ + 1]; pid_t pid; //清空緩沖區(qū) memset(buffer, '\0', sizeof(buffer)); if(pipe(filedes) == 0) { //創(chuàng)建管道成功 //通過調(diào)用fork創(chuàng)建子進程 pid = fork(); if(pid == -1) { fprintf(stderr, "Fork failure"); exit(EXIT_FAILURE); } if(pid == 0) { //子進程中 //讀取數(shù)據(jù) data_processed = read(filedes[0], buffer, BUFSIZ); printf("Read %d bytes: %s\n", data_processed, buffer); exit(EXIT_SUCCESS); } else { //父進程中 //寫數(shù)據(jù) data_processed = write(filedes[1], data, strlen(data)); printf("Wrote %d bytes: %s\n", data_processed, data); //休眠2秒,主要是為了等子進程先結(jié)束,這樣做也只是純粹為了輸出好看而已 //父進程其實沒有必要等等子進程結(jié)束 sleep(2); exit(EXIT_SUCCESS); } } exit(EXIT_FAILURE); }
運行結(jié)果為:
可見,子進程讀取了父進程寫到filedes[1]中的數(shù)據(jù),如果在父進程中沒有sleep語句,父進程可能在子進程結(jié)束前結(jié)束,這樣你可能將看到兩個輸入之間有一個命令提示符分隔。
四、把管道用作標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出
下面來介紹一種用管道來連接兩個進程的更簡潔方法,我們可以把文件描述符設(shè)置為一個已知值,一般是標(biāo)準(zhǔn)輸入0或標(biāo)準(zhǔn)輸出1。這樣做最大的好處是可以調(diào)用標(biāo)準(zhǔn)程序,即那些不需要以文件描述符為參數(shù)的程序。
為了完成這個工作,我們還需要兩個函數(shù)的輔助,它們分別是dup函數(shù)或dup2函數(shù),它們的原型如下
#include <unistd.h> int dup(int file_descriptor); int dup2(int file_descriptor_one, int file_descriptor_two);
dup調(diào)用創(chuàng)建一個新的文件描述符與作為它的參數(shù)的那個已有文件描述符指向同一個文件或管道。對于dup函數(shù)而言,新的文件描述總是取最小的可用值。而dup2所創(chuàng)建的新文件描述符或者與int file_descriptor_two相同,或者是第一個大于該參數(shù)的可用值。所以當(dāng)我們首先關(guān)閉文件描述符0后調(diào)用dup,那么新的文件描述符將是數(shù)字0.
例子
在下面的例子中,首先打開管道,然后fork一個子進程,然后在子進程中,使標(biāo)準(zhǔn)輸入指向讀管道,然后關(guān)閉子進程中的讀管道和寫管道,只留下標(biāo)準(zhǔn)輸入,最后調(diào)用execlp函數(shù)來啟動一個新的進程od,但是od并不知道它的數(shù)據(jù)來源是管道還是終端。父進程則相對簡單,它首先關(guān)閉讀管道,然后在寫管道中寫入數(shù)據(jù),再關(guān)閉寫管道就完成了它的任務(wù)。源文件為pipe2.c,代碼如下:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main() { int data_processed = 0; int pipes[2]; const char data[] = "123"; pid_t pid; if(pipe(pipes) == 0) { pid = fork(); if(pid == -1) { fprintf(stderr, "Fork failure!\n"); exit(EXIT_FAILURE); } if(pid == 0) { //子進程中 //使標(biāo)準(zhǔn)輸入指向fildes[0] close(0); dup(pipes[0]); //關(guān)閉pipes[0]和pipes[1],只剩下標(biāo)準(zhǔn)輸入 close(pipes[0]); close(pipes[1]); //啟動新進程od execlp("od", "od", "-c", 0); exit(EXIT_FAILURE); } else { //關(guān)閉pipes[0],因為父進程不用讀取數(shù)據(jù) close(pipes[0]); data_processed = write(pipes[1], data, strlen(data)); //寫完數(shù)據(jù)后,關(guān)閉pipes[1] close(pipes[1]); printf("%d - Wrote %d bytes\n", getpid(), data_processed); } } exit(EXIT_SUCCESS); }
運行結(jié)果為:
從運行結(jié)果中可以看出od進程正確地完成了它的任務(wù),與在shell中直接輸入od -c和123的效果一樣。
五、關(guān)于管道關(guān)閉后的讀操作的討論
現(xiàn)在有這樣一個問題,假如父進程向管道file_pipe[1]寫數(shù)據(jù),而子進程在管道file_pipe[0]中讀取數(shù)據(jù),當(dāng)父進程沒有向file_pipe[1]寫數(shù)據(jù)時,子進程則沒有數(shù)據(jù)可讀,則子進程會發(fā)生什么呢?再者父進程把file_pipe[1]關(guān)閉了,子進程又會有什么反應(yīng)呢?
當(dāng)寫數(shù)據(jù)的管道沒有關(guān)閉,而又沒有數(shù)據(jù)可讀時,read調(diào)用通常會阻塞,但是當(dāng)寫數(shù)據(jù)的管道關(guān)閉時,read調(diào)用將會返回0而不是阻塞。注意,這與讀取一個無效的文件描述符不同,read一個無效的文件描述符返回-1。
六、匿名管道的缺陷
看了這么多相信大家也知道它的一個缺點,就是通信的進程,它們的關(guān)系一定是父子進程的關(guān)系,這就使得它的使用受到了一點的限制,但是我們可以使用命名管道來解決這個問題。命名管道將在下一篇文章:Linux進程間通信——使用命名管道中介紹。
總結(jié)
以上就是本文關(guān)于Linux通過匿名管道進行進程間通信的全部內(nèi)容,希望對大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。感謝朋友們對本站的支持!
相關(guān)文章
Linux上下行網(wǎng)速測試工具_(dá)speedtest-cli安裝使用方法
speedtest-cli是Linux下的一個上下行網(wǎng)速測試工具,是一個用Python寫的命令行腳本,需要的朋友可以參考下2017-03-03centos 7中設(shè)置tomcat 7為系統(tǒng)服務(wù)的方法詳解
這篇文章主要給大家介紹了關(guān)于在centos 7中設(shè)置tomcat 7為系統(tǒng)服務(wù)的相關(guān)資料,文中介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來跟著小編一起學(xué)習(xí)學(xué)習(xí)吧。2017-06-06apache的AllowOverride以及Options使用詳解
通常利用Apache的rewrite模塊對 URL 進行重寫的時候, rewrite規(guī)則會寫在 .htaccess 文件里。但要使 apache 能夠正常的讀取.htaccess 文件的內(nèi)容,就必須對.htaccess 所在目錄進行配置2012-11-11Linux基礎(chǔ)學(xué)習(xí)之文件查找find的常見用法
這篇文章主要給大家介紹了關(guān)于Linux基礎(chǔ)學(xué)習(xí)之文件查找find的常見用法的相關(guān)資料,例如:根據(jù)文件名查找、根據(jù)正則表達(dá)式查找、根據(jù)路徑查找、根據(jù)文件類型查找以及根據(jù)文件大小等等,文中給出了詳細(xì)的示例代碼,需要的朋友可以參考借鑒。2017-12-12Linux系統(tǒng)查看當(dāng)前網(wǎng)絡(luò)連接數(shù)的方法小結(jié)
在日常的運維和開發(fā)中,網(wǎng)絡(luò)連接數(shù)的監(jiān)控是一個非常重要的指標(biāo),當(dāng)你需要排查網(wǎng)絡(luò)問題或優(yōu)化系統(tǒng)性能時,查看連接數(shù)是關(guān)鍵的一步,不同的 Linux 環(huán)境和發(fā)行版可能需要使用不同的方法來獲取連接信息,本文給大家介紹了Linux系統(tǒng)查看當(dāng)前網(wǎng)絡(luò)連接數(shù)的方法2024-12-12基于centos7 安裝python3.6.4出錯的解決方法
下面小編就為大家分享一篇基于centos7 安裝python3.6.4出錯的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01