进程间通信:管道和命名管道(FIFO)
目錄
概述
IPC 對象的持續性
什么是管道
讀取外部程序的輸出
將輸出送往 popen
傳遞更多的數據
如何實現 popen
pipe 調用
跨越 fork 調用管道
父進程和子進程
管道關閉后的讀操作
把管道用作標準輸入和標準輸出
命名管道:FIFO
創建命名管道
打開 FIFO 文件
使用 FIFO 實現進程間通信
使用 FIFO 的客戶/服務器應用程序
概述
IPC (interprocess communication)是進程間通信的簡稱。該術語的意思是運行在某操作系統上的不同進程間的消息傳遞方式。這里還需要理解同步概念,因為像共享內存這樣的通信方式需要某種形式的同步參與運作。學習這塊知識的時候,會常常參閱 Unix 方面的書籍,在談論 Unix 進程時,有親緣關系的說法意味著所討論的進程具有某個共同的祖先。
按照傳統的 Unix 編程模型,在一個系統上運行多個進程,每個進程都有各自的地址空間。Unix 進程間的信息共享可以有多種方式。
(1) 第一種是兩個進程共享文件系統中某個文件上的某些信息。通過這種方式獲取信息,每個進程都要穿越內核 (如read、write、lseek等)。當一個文件有待更新時,某種形式的同步是必要的,這樣既可以保護多個寫入者不會互相干擾,也可以保護一個或多個讀者防止寫者的干擾。
(2) 第二種是兩個進程共享駐留在內核中的某些信息,這種類型的典例是管道,消息隊列和信號量也是。它們在訪問共享信息的每次操作涉及對內核的一次系統調用。
(3) 第三種是兩個進程有一個雙方都能訪問的共享內存區域。每個進程一旦設置好該共享內存區,就能不涉及內核而訪問其中的數據。使用共享內存的進程需要某種形式的同步。
沒有任何事物限制任何 IPC 技術只能在使用兩個進程間使用。IPC 技術適用于任意數目的進程。因為共享內存共享信息時不涉及內核,所以它的速度是最快的。
?
IPC 對象的持續性
可以把任意類型的 IPC 的持續性定義成該類型的一個對象一直存在多長時間。
(1) 隨進程持續的 IPC 對象一直存在到打開著該對象的最后一個進程關閉該對象為止。例如管道和 FIFO?就是這種對象。
(2)隨內核持續的 IPC 對象一直存在到內核重新自舉(內核自舉就是把主引導記錄加載到內存,并跳轉執行這段內存)或顯式刪除該對象為止。例如消息隊列、信號量和共享內存就是此類對象。Posix 的消息隊列、信號量和共享內存必須至少是隨內核持續的,但也可以是隨文件系統持續的,具體取決于實現。
(3)隨文件系統持續的 IPC 對象一直存在到顯式刪除該對象為止。即使內核重新自舉了,該對象還是保持其值。Posix 消息隊列、信號量和共享內存如果是使用映射文件實現的,那么它們就是隨文件系統持續的。
在定義一個 IPC 對象的持續性時需要特別小心,因為它并不總是像看起來的那樣。例如管道內的數據是在內核中維護的,但管道具備的是隨進程的持續性而不是隨內核的持續性:最后一個進程關閉該管道后,內核將丟棄所有的數據并刪除該管道。類似地,盡管 FIFO 在文件系統中有名字,它們也只是具備隨進程的持續性,因為最后一個將 FIFO 打開的進程關閉該 FIFO 后,其中的數據就會被丟棄。
操作系統中的同步和異步:https://blog.csdn.net/qq_38289815/article/details/81012826
進程間通信:信號量 ?https://blog.csdn.net/qq_38289815/article/details/104762940
進程間通信:共享內存 ?https://blog.csdn.net/qq_38289815/article/details/104776076
進程間通信:消息隊列 ?https://blog.csdn.net/qq_38289815/article/details/104786412
?
什么是管道
當一個進程連接數據流到另一個進程時,我們使用管道(pipe)。我們通常是把一個進程的輸出通過管道連接到另一個進程的輸入。最簡單的在兩個程序之間傳遞數據的方法是使用 popen 和 pclose 函數。它們的原型如下:
#include?<stdio.h> FILE?*?popen?(?const?char?*?command?,?const?char?*?type?); int?pclose?(?FILE?*?stream?);?
popen 函數
popen 函數允許一個程序將另一個程序作為新進程啟動,并可以傳遞數據給它或者通過它接收數據。command 字符串是要運行的程序名和相應的參數。open_mode 必須是 r 或者 w。
如果 open_mode 是 r,被調用程序的輸出就可以被調用程序使用,調用程序利用 popen 函數返回的 FILE* 文件流指針,就可以通過常用的 stdio 庫函數(如fread)來讀取被調用程序的輸出。如果 open_mode 是 w,調用程序就可以用 fwrite 調用向被調用程序發送數據,而被調用程序可以在自己的標準輸入上讀取這些數據。被調用的程序通常不會意識到自己正在從另一個進程讀取數據,它只是在標準輸入流上讀取數據,然后做出相應的操作。
每個 popen 調用必須指定 r 或 w,在 popen 上述的標準實現中不支持任何其他選項。這意味著我們不能調用另一個程序并同時對它進行讀寫操作。popen 函數在失敗時返回一個空指針。如果想通過管道實現雙向通信,最普通的解決辦法是使用兩個管道,每個管道負責一個方向的數據流。
?
pclose 函數
用 popen 啟動的進程結束時,我們可以用 pclose 函數關閉與之關聯的文件流。pclose 調用只在 popen 啟動的進程結束后才返回。如果調用 pclose 時它仍在運行,pclose 調用將等待該進程的結束。
pclose 調用的返回值通常是它所關閉的文件流所在進程的退出碼。如果調用進程在調用 pclose 之前執行了一個 wait 語句,被調用進程的退出狀態會丟失,因為被調用進程已結束。此時,pclose 將返回 -1 并設置 errno 為 ECHILD。
?
讀取外部程序的輸出
以下程序使用 popen 訪問 uname 命令給出的信息。命令 uname -a 的作用是打印系統信息,包括計算機型號、操作系統名稱、版本和發行號,以及計算機的網絡名。完成程序初始化工作后,打開一個連接到 uname 命令的管道,先把管道設置為可讀方式并讓 read_fp 指向該命令的輸出。最后,關閉 read_fp 指向的管道。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h>int main(void) {FILE *read_fp;int chars_read;char buffer[BUFSIZ + 1];read_fp = popen("uname -a", "r");memset(buffer, '\0', sizeof(buffer));if (NULL != read_fp){chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);if (chars_read > 0){printf("Output:%s\n", buffer);}pclose(read_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0; }這個程序用 popen 調用啟動帶有 -a 選項的 uname 命令。然后用返回的文件流讀取最多 BUFSIZ 個字符(這個常量是在 stdio.h 中定義的)的數據,并將它們打印出來顯示在屏幕上。因為我們是在程序內部捕獲 uname 命令的輸出,所以可以處理它。
?
將輸出送往 popen
看到上述例子后,再來看一個將輸出發送到外部程序的實例,它將數據通過管道送往另一個程序。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h>int main(void) {FILE *write_fp;char buffer[BUFSIZ + 1];sprintf(buffer, "ABC");write_fp = popen("od -c", "w");if (NULL != write_fp){fwrite(buffer, sizeof(char), strlen(buffer), write_fp);pclose(write_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0; }程序使用帶有參數 w 的 popen 啟動 od -c 命令,這樣就可以向該命令發送數據了。然后它給 od -c 命令發送一個字符串,該命令接收并處理它,最后把處理結果打印到自己的標準輸出上。
?
傳遞更多的數據
目前所使用的機制都只是將所有數據通過一次 fread 或 fwrite 調用來發送或接收。有時,我們希望以塊方式發送數據,或者是不知道輸出數據的長度。為了避免定義一個非常大的緩沖區,我們可以用多個 fread 或 fwrite 調用來將數據分為幾部分處理。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h>int main(void) {FILE *read_fp;char buffer[BUFSIZ + 1];int chars_read;memset(buffer, '\0', sizeof(buffer));read_fp = popen("ps ax", "r");if (NULL != read_fp){chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);while (chars_read > 0){buffer[chars_read - 1] = '\0';printf("Rreading %d:-\n %s\n", BUFSIZ, buffer);chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);}pclose(read_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0; }這個程序調用 popen 函數時使用 r 參數,這次它連續從文件流中讀取數據,知道沒有數據可讀為止。注意,雖然 ps 命令的執行要花費一些時間,但 Linux 會安排好進程將的調度,讓兩個程序在可以運行時繼續運行。如果讀進程 popen 沒有數據可讀,它將被掛起直到有數據到達。如果寫進程 ps 產生的輸出超過了可用緩沖區長度,它也會被掛起知道讀進程讀取了一些數據。在本例中,你可能看不到 Reading:信息的第二次出現。如果 BUFSIZ 的值超過了 ps 命令輸出的長度,這種情況就會發生。這里的信息真的是太多,截圖也只是冰山一角而已。
?
如何實現 popen
請求 popen 調用運行一個程序時,它首先啟動 shell,即系統中的 sh 命令,然后將 command 字符串作為一個參數傳遞給它。
在 Linux (以及所有的類 UNIX 系統)中,所有的參數擴展都是由 shell 來完成的。所以,在啟動程序之前先啟動 shell 來分析命令字符串,就可以使各種 shell 擴展在程序啟動之前就全部完成。這個功能非常有用,它允許我們通過 popen 啟動非常復雜的 shell 命令。而其他一些創建進程的函數調用起來就復雜的多,因為調用進程必須自己去完成 shell 擴展。
使用 shell 的一個不太好的影響是,針對每個 popen 調用,不僅要啟動一個被請求的程序,還要啟動一個 shell,即每個 popen 調用將多啟動兩個進程。從節省系統資源的角度來看,popen 函數的調用成本略高,而且對目標命令的調用比正常方式要慢一些。下面這個程序演示 popen 函數的行為,這個程序用來統計 asd3.c 這個文件的總行數。其實在終端下輸入相應的 cat asd3.c | wc -l? 便能獲得結果,不僅簡單而且更有效率,但這個例子展示了 popen 函數的工作原理。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h>int main(void) {FILE *read_fp;char buffer[BUFSIZ + 1];int chars_read;memset(buffer, '\0', sizeof(buffer));read_fp = popen("cat asd3.c | wc -l", "r");if (NULL != read_fp){chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);while (chars_read > 0){buffer[chars_read - 1] = '\0';printf("Reading:-\n %s\n", buffer);chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);}pclose(read_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0; }?
pipe 調用
看過了高級的 popen 函數之后,再來看看底層 pipe 函數。通過這個函數在兩個程序之間傳遞數據不需要啟動一個 shell 來解釋請求的命令。它同事還提供了對讀寫數據的更多控制。
pipe 函數原型如下:
#include <unistd.h> int pipe(int file_descriptor[2]);pipe 函數的參數是一個有兩個整數類型的文件描述符組成的數組的指針。該函數在數組中填上兩個新的文件描述符后返回 0,如果失敗則返回 -1 并設置 errno 來表明失敗的原因。Linux 手冊中定義了下面一些錯誤。
EMFILE:進程使用的文件描述符過多。 ENFILE:系統文件表已滿。 EFAULT:文件描述符無效。兩個返回的文件描述符以一種特殊的方式連接起來。寫到 file_descriptor[1] 的所有數據都可以從 file_descriptor[0] 讀出來。數據基于先進先出的原則進行處理,這意味著如果寫入順序為1、2、3,則讀出順序也是1、2、3。
特別注意:這里使用的是文件描述符而不是文件流,所以我們必須用底層的 read 和 write 調用來訪問數據,而不是用文件流庫函數 fread 和 fwrite。
#include <unistd.h> //pipe1 #include <stdlib.h> #include <stdio.h> #include <string.h>int main() {int data_processed;int file_pipes[2];const char some_data[] = "123";char buffer[BUFSIZ + 1];memset(buffer, '\0', sizeof(buffer));if (pipe(file_pipes) == 0){data_processed = write(file_pipes[1], some_data,strlen(some_data));printf("Wrote %d bytes\n", data_processed);data_processed = read(file_pipes[0], buffer, BUFSIZ);printf("Read %d bytes: %s\n", data_processed, buffer);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0; } 輸出為: Wrote 3 bytes Read 3 bytes: 123這個程序用數組 file_pipes[] 中的兩個文件描述符創建一個管道。然后它用文件描述符 file_pipes[1] 向管道中寫數據,再從 file_pipes[0] 讀出數據。注意,管道有一些內置的緩沖區,它在 write 和 read 調用之間保存數據。
如果你嘗試用 file_pipes[0] 寫入數據或用 file_pipes[1] 讀取數據,其后果并未在文檔中明確定義,所以其行為可能會非常的奇怪,并隨著系統的不同,其行為可能會發生變化。
管道真正優勢體現在,當程序用 fork 調用創建新進程時,原先打開的文件描述符仍將保持打開狀態。如果在原先的進程中創建一個管道,然后在調用 fork 創建新進程,便可通過管道在兩個進程之間傳遞數據。
?
跨越 fork 調用管道
#include <unistd.h> //pipe2 #include <stdlib.h> #include <stdio.h> #include <string.h>int main() {int data_processed;int file_pipes[2];const char some_data[] = "123";char buffer[BUFSIZ + 1];pid_t fork_result;memset(buffer, '\0', sizeof(buffer));if (pipe(file_pipes) == 0){fork_result = fork();if (fork_result == -1){fprintf(stderr, "Fork failure");exit(EXIT_FAILURE);}if (fork_result == 0){//確認fork調用成功后,如果fork_result等于零,說明是子進程data_processed = read(file_pipes[0], buffer, BUFSIZ);printf("Read %d bytes: %s\n", data_processed, buffer);exit(EXIT_SUCCESS);}else{//父進程data_processed = write(file_pipes[1], some_data,strlen(some_data));printf("Wrote %d bytes\n", data_processed);}}exit(EXIT_FAILURE);return 0; } 輸出為: Wrote 3 bytes Read 3 bytes: 123這個程序首先用 pipe 調用創建一個管道,接著用 fork 調用創建一個新進程。如果 fork 調用成功,父進程就寫數據到管道中,而子進程從管道中讀取數據。父子進程都在只調用一次 write 或 read 之后就退出。如果父進程在子進程之前退出,便會看到在兩部分輸出內容之間看到 shell 提示符。
?
父進程和子進程
在接下來的對 pipe 調用的研究中,將學習如何在子進程中進行一個與其父進程完全不同的另外一個程序,而不是僅僅運行一個相同程序。我們用 exec 調用來完成這一工作。這里的一個難點是,通過 exec 調用的進程需要知道應該訪問哪個文件描述符。在前面的例子中,因為子進程本身有 file_pipes 數據的一份副本,所以這并不成為問題。但經過 exec 調用后,情況就不一樣了,因為原先的進程已經被新的子進程替換了。為了解決這個問題,可以將文件描述符(它實際上就是一個數字)作為一個參數傳遞給 exec 啟動的程序。為了演示它是如何工作的,我們需要使用兩個程序,一個是數據生產者,它負責創建管道和啟動子進程,而后者是數據消費者。
#include <unistd.h> //pipe3 #include <stdlib.h> #include <stdio.h> #include <string.h>int main() {int data_processed;int file_pipes[2];const char some_data[] = "123";char buffer[BUFSIZ + 1];pid_t fork_result;memset(buffer, '\0', sizeof(buffer));if (pipe(file_pipes) == 0){fork_result = fork();if (fork_result == -1){fprintf(stderr, "Fork failure");exit(EXIT_FAILURE);}if (fork_result == 0){//確認fork調用成功后,如果fork_result等于零,說明是子進程sprintf(buffer, "%d", file_pipes[0]);(void)execl("pipe3", "pipe3", buffer, (char*)0);exit(EXIT_FAILURE);}else{//父進程data_processed = write(file_pipes[1], some_data,strlen(some_data));printf("%d - wrote %d bytes\n", getpid(), data_processed);}}exit(EXIT_SUCCESS);return 0; } #include <unistd.h> //pipe4 #include <stdlib.h> #include <stdio.h> #include <string.h>int main(int argc, char *argv[]) {int data_processed;char buffer[BUFSIZ + 1];int file_descriptor;memset(buffer, '\0', sizeof(buffer));sscanf(argv[1], "%d", &file_descriptor);data_processed = read(file_descriptor, buffer, BUFSIZ);printf("%d - read %d bytes: %s\n", getpid(),data_processed, buffer);exit(EXIT_SUCCESS);return 0; }pipe3 在開始部分和前面例子一樣,用 pipe 調用創建一個管道,然后用 fork 調用創建一個新進程。接下來,它用 sprintf 把讀取管道數據的文件描述符保存到一個緩沖區,該緩沖區中的內容將構成 pipe4 的一個參數。
這里通過 execl 調用來啟動 pipe4 程序,execl 的參數如下:
(1)要啟動的程序的路徑。 (2)argv[0]:程序名。 (3)argv[1]:包含我們想讓被調用程序去讀的文件描述符。 (4)(char *)0:這個參數的作用是終止被調用程序的參數列表。?
管道關閉后的讀操作
目前,程序一直采用的是讓讀進程讀取一些數據然后直接退出的方式,并假設 Linux 會把清理文件當作是在進程結束時應該做的工作的一部分。但大多數從標準輸入讀取數據的程序采用的卻是與我們到目前為止見到的例子不同的做法。通常它們并不知道有多少數據需要讀取,所以往往采用循環的方式,讀取數據——處理數據——讀取更多的數據,知道沒有數據可讀為止。
當沒有數據可讀時,read 調用通常會阻塞,即它將暫停進程來等待直到有數據到達為止。如果管道的另一端已被關閉,也就是說,沒有進程打開這個管道并向它寫數據,這時 read 調用就會阻塞。但這樣的阻塞不是很有用,因此對一個已關閉寫數據的管道做 read 調用將返回 0 而不是阻塞。這就使得讀進程能夠像檢測文件結束一樣,對管道進行檢測并做出相應的動作。注意,這與讀取一個無效的文件描述符不同,read 把無效的文件描述符看作一個錯誤并返回 -1。
如果跨越 fork 調用使用管道,就會有兩個不同的文件描述符可以用于向管道寫數據,一個在父進程中,一個在子進程中。只有把父子進程中的針對管道的寫文件描述符都關閉,管道才能被認為是關閉了,對管道的 read 調用才會失敗。
?
把管道用作標準輸入和標準輸出
下面來看一種使用管道連接兩個進程的方法。把其中一個管道文件描述符設置為一個已知值,一般是標準輸入 0 或標準輸出 1。在父進程中做這個設置稍微有點復雜,但它使得子程序的編寫變得非常簡單。這樣做的最大好處是我們可以調用標準程序,即那些不需要以文件描述符為參數的程序。
#include<unistd.h> int dup(int file_descriptor); int dup2(int file_descriptor_one,int file_descriptor_two);dup 調用的目的是打開一個新的文件描述符,這與 open 有點類似。不同的是,dup 調用創建的新文件描述符與作為它的參數的那個已有文件描述符指向同一個文件(或管道)。對于 dup 函數來說,新的文件描述符總是取最小的可用值。而對于 dup2 函數來說,它所創建的新文件描述符或者與參數 file_descriptor_two 相同,或者是第一個大于該參數的可用值。
那么 dup 是如何幫助進程傳遞數據的?訣竅在于,標準輸入的文件描述符總是 0,而 dup 返回的新的文件描述符又總是使用最小可用的數字。因此,如果我們首先關閉文件描述符 0,然后調用 dup,那么新的文件描述符就將是數字 0。因為新的文件描述符是復制一個已有的文件描述符,所以標準輸入就會改為指向一個我們傳遞給 dup 函數的文件描述符所對應的文件或管道。我們創建了兩個文件描述符,它們指向同一個文件或管道,而且其中之一是標準輸入。
用 close 和 dup 函數對文件描述符進行處理
理解當我們關閉文件描述符 0,然后調用 dup 究竟發生了什么的方法是查看開頭的4個文件描述符的狀態在這一過程中的變化情況。
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h>int main() {int data_processed;int file_pipes[2];const char some_data[] = "123";pid_t fork_result;if (pipe(file_pipes) == 0){fork_result = fork();if(fork_result == (pid_t)-1){fprintf(stderr, "Fork failure");exit(EXIT_FAILURE);}if (fork_result == (pid_t)0){close(0);dup(file_pipes[0]);close(file_pipes[0]);close(file_pipes[1]);execlp("od", "od", "-c", (char*)0);exit(EXIT_FAILURE);}else{close(file_pipes[0]);data_processed = write(file_pipes[1], some_data, strlen(some_data));close(file_pipes[1]);printf("%d - wrote %d bytes\n", (int)getpid(), data_processed);}}exit(EXIT_SUCCESS);return 0; } 輸出為: 5009 - wrote 3 bytes 0000000 1 2 3 0000003與往常一樣,這個程序創建一個管道,然后通過 fork 創建一個子進程。此時,父子進程都可以訪問管道的文件描述符,一個用于讀數據,一個用于寫數據,所以總共有4個打開的文件描述符。
子進程的情況:子進程先用 close(0) 關閉它的標準輸入,然后調用 dup(file_pipes[0]) 把與管道的讀取端關聯的文件描述符復制為文件描述符0,即標準輸入。接下來,子進程關閉原先的用來從管道讀取數據的文件描述符 file_pipes[0] 。因為子進程不向管道寫數據,所以它把與管道關聯的寫操作文件描述符 file_pipes[1] 也關閉了?,F在,它只有一個與管道關聯的文件描述符,即文件描述符0,它的標準輸入。
接下來,子進程就可以用 exec 來啟動任何從標準輸入讀取數據的程序了。在本例中,我們使用的是 od 命令。od 命令將等待數據的到來,就好像它在等待來自用戶終端的輸入一樣。事實上,如果沒有明確使用檢測這兩者之間的特殊代碼,它并不知道輸入是來自一個管道,而不是來自一個終端。
父進程的情況:父進程首先關閉管道的讀取端 file_pipes[0],因為它不會從管道讀取數據。接著它向管道寫入數據。當所有數據都寫完后,父進程關閉管道的寫入端并退出。因為現在已沒有打開的文件描述符可以向管道寫數據了,od 程序讀取寫到管道中的3個字節數據后,后續的讀操作將返回 0 字節,表示已到達文件尾。當讀取操作返回 0 時,od 程序就退出運行。
?
命名管道:FIFO
上述的例子還只是在相關的程序之間傳遞數據。如果想在不相關的進程間交換數據,還不是很方便。要想完成不同進程間傳遞數據可以使用 FIFO 文件來完成這項工作,它通常也被稱為命名管道。命名管道是一種特殊類型的文件(Linux下皆文件),它在文件系統中以文件名的形式存在,但它的行為卻和沒有名字的管道類似。
在程序中,我們可以使用兩個不同的函數調用:
#include <sys/types.h> #include <sys/stat.h>int mkfifo(const char *filename, mode_t mode); int mknod(const char *filename, mode_t | S_IFIFO, (dev_t) 0);我們可以使用 mknod 函數建立許多特殊類型的文件。想要通過這個函數創建一個命名管道,位移具有可移植性的方法是使用一個 dev_t 類型的值 0,并將文件訪問模式與 S_IFIFO 按位或。下面的例子將簡單的使用 mkfifo 函數。
?
創建命名管道
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h>int main() {int res = mkfifo("my_fifo", 0777);if (res == 0)printf("FIFO created\n");exit(EXIT_SUCCESS);return 0; }這個程序使用 mkfifo 函數創建一個特殊文件,雖然要求的文件模式是 0777,但它被用戶掩碼設置給改變了,這與普通文件的創建是一樣的,所以文件的最終模式是 755。可以像刪除一個普通文件那樣用 rm 命令刪除 FIFO 文件,或者也可以在程序中用 unlink 系統調用來刪除它。
?
打開 FIFO 文件
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <fcntl.h>#define FIFO_NAME "my_fifo"int main(int argc, char *argv[]) {int res;int open_mode = 0;int i;if (argc < 2){fprintf(stderr, "Usage: %s < some combination of O_RDONLY O_WRONLY O_NONBLOCK >\n", *argv);exit(EXIT_FAILURE);}for(i = 1; i < argc; ++i){if (strncmp(*++argv, "O_RDONLY", 8) == 0)open_mode |= O_RDONLY;if (strncmp(*++argv, "O_WRONLY", 8) == 0)open_mode |= O_WRONLY;if (strncmp(*++argv, "O_NONBLOCK", 8) == 0)open_mode |= O_NONBLOCK;}if (access(FIFO_NAME, F_OK) == -1){res = mkfifo(FIFO_NAME, 0777);if (res != 0){fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);exit(EXIT_FAILURE);}}printf("Process %d opening FIFO\n", getpid());res = open(FIFO_NAME, open_mode);printf("Process %d result %d\n", getpid(), res);sleep(5);if (res != -1)(void)close(res);printf("Process %d finished\n", getpid());exit(EXIT_SUCCESS);return 0;}通信這個程序能夠在命令行上指定我們希望使用的 O_RDONLY、O_WRONLY 和 O_NONBLOCK 的組合方式。它會把命令行參數與程序中的常量字符串進行比較,如果匹配,就(用 |= 操作符)設置相應的標志。程序使用 access 函數來檢查 FIFO 文件是否存在,如果不存在就創建它。
?
使用 FIFO 實現進程間通信
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <fcntl.h> #include <limits.h>#define FIFO_NAME "my_fifo" #define BUFFER_SIZE PIPE_BUF #define TEN_MEG (1024 * 1024 * 10)int main() {int pipe_fd;int res;int open_mode = O_WRONLY;int bytes_sent = 0;char buffer[BUFFER_SIZE + 1];if (access(FIFO_NAME, F_OK) == -1){res = mkfifo(FIFO_NAME, 0777);if (res != 0){fprintf(stderr, "Could not create fifo &s\n", FIFO_NAME);exit(EXIT_FAILURE);}}printf("Process %d opening FIFO O_WRONLY\n", getpid());pipe_fd = open(FIFO_NAME, open_mode);printf("Process %d result %d\n", getpid(), pipe_fd);if (pipe_fd != -1){while (bytes_sent < TEN_MEG){res = write(pipe_fd, buffer, BUFFER_SIZE);if (res == -1){fprintf(stderr, "Write error on pipe\n");exit(EXIT_FAILURE);}bytes_sent += res;}(void)close(pipe_fd);}elseexit(EXIT_FAILURE);printf("Process %d finished\n", getpid());exit(EXIT_SUCCESS);return 0;} #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <fcntl.h> #include <limits.h>#define FIFO_NAME "my_fifo" #define BUFFER_SIZE PIPE_BUFint main() {int pipe_fd;int res;int open_mode = O_RDONLY;char buffer[BUFFER_SIZE + 1];int bytes_read = 0;memset(buffer, '\0', sizeof(buffer));printf("Process %d opening FIFO O_RDONLY\n", getpid());pipe_fd = open(FIFO_NAME, open_mode);printf("Process %d result %d\n", getpid(), pipe_fd);if (pipe_fd != -1){do{res = read(pipe_fd, buffer, BUFFER_SIZE);bytes_read += res;} while(res > 0);(void)close(pipe_fd);}elseexit(EXIT_FAILURE);printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);exit(EXIT_SUCCESS);return 0;}兩個程序使用的都是阻塞模式的 FIFO。首先啟動 fifo3(寫進程/生產者),它將阻塞以等待讀進程打開這個 FIFO。fifo4 (消費者)啟動以后,生產者解除阻塞并開始向管道寫數據。同時,讀進程也開始從管道中讀取數據。Time 命令的輸出顯示,讀進程只運行了不到 0.1 秒的時間,卻讀取了 10MB 的數據。這說明管道在程序之間傳遞數據是很有效率的。
?
使用FIFO的客戶/服務器應用程序
作為學習 FIFO 的最后一部分內容,我們來考慮怎樣通過命名管道來編寫一個非常簡單的客戶/服務器應用程序。我們只用一個服務器進程來接受請求,對它們進行處理,最后把結果數據返回給客戶。
我們想允許多個客戶進程都可以向服務器發送數據。為了使問題簡單化,我們假設被處理的數據可以被拆分為一個個數據塊,每個長度都小于 PIPE_BUF 字節。當然,我們可以用很多方法來實現這個系統,但這里我們只考慮一種方式,即使用命名管道實現它。
因為服務器每次只能處理一個數據塊,所以只使用一個 FIFO 應該是合乎邏輯的,服務器通過它讀取數據,每個客戶向它寫數據。只要將 FIFO 以阻塞模式打開,服務器和客戶就會根據需要自動被阻塞。
將處理后的數據返回給客戶稍微有點困難。我們需要為每個客戶安排第二個管道來接收返回的數據。通過在傳遞給服務器的原先數據中加上客戶的進程標識符(PID),雙方就可以使用它來為返回數據的管道生成唯一的名字。
#include <unistd.h> //client.h #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <string.h> #include <limits.h> #include <sys/types.h> #include <sys/stat.h>#define SERVER_FIFO_NAME "serv_fifo" #define CLIENT_FIFO_NAME "cli_fifo"#define BUFFER_SIZE 20struct data_to_pass_st {pid_t client_pid;char some_data[BUFFER_SIZE - 1]; }; #include <ctype.h> //server.c #include "client.h"int main() {int server_fifo_fd, client_fifo_fd;struct data_to_pass_st my_data;int read_res;char client_fifo[256];char *tmp_char_ptr;mkfifo(SERVER_FIFO_NAME, 0777);server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);if (server_fifo_fd == -1){fprintf(stderr, "Server fifo failure\n");exit(EXIT_FAILURE);}sleep(10);do{read_res = read(server_fifo_fd, &my_data, sizeof(my_data));if (read_res > 0){tmp_char_ptr = my_data.some_data;while (*tmp_char_ptr){*tmp_char_ptr = toupper(*tmp_char_ptr);tmp_char_ptr++;}sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);client_fifo_fd = open(client_fifo, O_WRONLY);if (client_fifo_fd != -1){write(client_fifo_fd, &my_data, sizeof(my_data));close(client_fifo_fd);}}}while (read_res > 0);close(server_fifo_fd);unlink(SERVER_FIFO_NAME);exit(EXIT_SUCCESS);return 0; } #include <ctype.h> //client.c #include "client.h"int main() {int server_fifo_fd, client_fifo_fd;struct data_to_pass_st my_data;int times_to_send;char client_fifo[256];server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY);if (server_fifo_fd == -1){fprintf(stderr, "Sorry, no server\n");exit(EXIT_FAILURE);}my_data.client_pid = getpid();sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);if (mkfifo(client_fifo, 0777) == -1){fprintf(stderr, "Sorry, can not make %s\n", client_fifo);exit(EXIT_FAILURE);}for (times_to_send = 0; times_to_send < 5; times_to_send++){sprintf(my_data.some_data, "Hello from %d", my_data.client_pid);printf("%d sent %s, ", my_data.client_pid, my_data.some_data);write(server_fifo_fd, &my_data, sizeof(my_data));client_fifo_fd = open(client_fifo, O_RDONLY);if (client_fifo_fd != -1){if (read(client_fifo_fd, &my_data, sizeof(my_data)) > 0){printf("received:%s\n", my_data.some_data);}close(client_fifo_fd);}}close(server_fifo_fd);unlink(client_fifo);exit(EXIT_SUCCESS);return 0; }實驗解析
服務器以只讀模式創建它的 FIFO 并阻塞,知道第一個客戶以寫方式打開同一個 FIFO 來創建連接。此時,服務器進程解除阻塞并執行 sleep 語句,這使得來自客戶的數據排隊等候。在實際的應用程序中,應該把 sleep 語句刪除。我們在這里使用它只是為了演示當多個客戶的請求同事到達時,程序的正確操作方法。
與此同時,在客戶打開了服務器 FIFO 或它創建自己唯一的一個命名管道來讀取服務器返回的數據。完成這些工作后,客戶發送數據給服務器,然后阻塞在對自己的 FIFO 的 read 調用上,等待服務器的響應。
接收到來自客戶的數據后,服務器處理它,然后以寫方式打開客戶管道并將處理后的數據返回,這樣解除客戶的阻塞狀態。客戶被解除阻塞后,它即可從自己的管道中讀取服務器返回的數據。
整個過程不斷重復,直到最后一個客戶關閉服務器管道為止,這將使服務器 read 調用失敗,因為沒有進程以寫方式打開服務器管道了。如果這是一個真正的服務器進程,它還需要繼續等待客戶的請求,我們就需要對它進行修改,有兩種方式:
(1) 對它自己的服務器管道打開一個文件描述符,這樣 read 調用將總是阻塞而不是返回 0。
(2) 當 read 調用返回 0 時,關閉并重新打開服務器管道,這使服務器進程阻塞在 open 調用處以等待客戶的到來,就像它最初啟動時那樣。
總結
以上是生活随笔為你收集整理的进程间通信:管道和命名管道(FIFO)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 进程及 fork() 系统调用详解
- 下一篇: 进程间通信:共享内存概念及代码