在前一篇文章——Linux進程間通信——使用匿名管道中,我們看到了如何使用匿名管道來在進程之間傳遞數據,同時也看到了這個方式的一個缺陷,就是這些進程都由一個共同的祖先進程啟動,這給我們在不相關的的進程之間交換數據帶來了不方便。這裡將會介紹進程的另一種通信方式——命名管道,來解決不相關進程間的通信問題。
一、什麼是命名管道
命名管道也被稱為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 mode | S_IFIFO, (dev_t)0);
這兩個函數都能創建一個FIFO文件,注意是創建一個真實存在於文件系統中的文件,filename指定了文件名,而mode則指定了文件的讀寫權限。
mknod是比較老的函數,而使用mkfifo函數更加簡單和規范,所以建議在可能的情況下,盡量使用mkfifo而不是mknod。
三、訪問命名管道
1、打開FIFO文件
與打開其他文件一樣,FIFO文件也可以使用open調用來打開。注意,mkfifo函數只是創建一個FIFO文件,要使用命名管道還是將其打開。
但是有兩點要注意,1、就是程序不能以O_RDWR模式打開FIFO文件進行讀寫操作,而其行為也未明確定義,因為如一個管道以讀/寫方式打開,進程就會讀回自己的輸出,同時我們通常使用FIFO只是為了單向的數據傳遞。2、就是傳遞給open調用的是FIFO的路徑名,而不是正常的文件。
打開FIFO文件通常有四種方式,
open(const char *path, O_RDONLY);//1 open(const char *path, O_RDONLY | O_NONBLOCK);//2 open(const char *path, O_WRONLY);//3 open(const char *path, O_WRONLY | O_NONBLOCK);//4
在open函數的調用的第二個參數中,你看到一個陌生的選項O_NONBLOCK,選項O_NONBLOCK表示非阻塞,加上這個選項後,表示open調用是非阻塞的,如果沒有這個選項,則表示open調用是阻塞的。
open調用的阻塞是什麼一回事呢?很簡單,對於以只讀方式(O_RDONLY)打開的FIFO文件,如果open調用是阻塞的(即第二個參數為O_RDONLY),除非有一個進程以寫方式打開同一個FIFO,否則它不會返回;如果open調用是非阻塞的的(即第二個參數為O_RDONLY | O_NONBLOCK),則即使沒有其他進程以寫方式打開同一個FIFO文件,open調用將成功並立即返回。
對於以只寫方式(O_WRONLY)打開的FIFO文件,如果open調用是阻塞的(即第二個參數為O_WRONLY),open調用將被阻塞,直到有一個進程以只讀方式打開同一個FIFO文件為止;如果open調用是非阻塞的(即第二個參數為O_WRONLY | O_NONBLOCK),open總會立即返回,但如果沒有其他進程以只讀方式打開同一個FIFO文件,open調用將返回-1,並且FIFO也不會被打開。
四、使用FIFO實現進程間的通信
說了這麼多,下面就用一個例子程序來說明一下,兩個進程如何通過FIFO實現通信吧。這裡有兩個源文件,一個fifowrite.c,它在需要時創建管道,然後向管道寫入數據,數據由文件Data.txt提供,大小為10M,內容全是字符‘0’。另一個源文件為fiforead.c,它從FIFO中讀取數據,並把讀到的數據保存到另一個文件DataFormFIFO.txt中。為了讓程序更加簡潔,忽略了有些函數調用是否成功的檢查。
fifowrite.c的源代碼如下:
#include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <limits.h> #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <string.h> int main() { const char *fifo_name = "/tmp/my_fifo"; int pipe_fd = -1; int data_fd = -1; int res = 0; const int open_mode = O_WRONLY; int bytes_sent = 0; char buffer[PIPE_BUF + 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()); //以只寫阻塞方式打開FIFO文件,以只讀方式打開數據文件 pipe_fd = open(fifo_name, open_mode); data_fd = open("Data.txt", O_RDONLY); printf("Process %d result %d\n", getpid(), pipe_fd); if(pipe_fd != -1) { int bytes_read = 0; //向數據文件讀取數據 bytes_read = read(data_fd, buffer, PIPE_BUF); buffer[bytes_read] = '\0'; while(bytes_read > 0) { //向FIFO文件寫數據 res = write(pipe_fd, buffer, bytes_read); if(res == -1) { fprintf(stderr, "Write error on pipe\n"); exit(EXIT_FAILURE); } //累加寫的字節數,並繼續讀取數據 bytes_sent += res; bytes_read = read(data_fd, buffer, PIPE_BUF); buffer[bytes_read] = '\0'; } close(pipe_fd); close(data_fd); } else exit(EXIT_FAILURE); printf("Process %d finished\n", getpid()); exit(EXIT_SUCCESS); }
 
源文件fiforead.c的代碼如下:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <limits.h> #include <string.h> int main() { const char *fifo_name = "/tmp/my_fifo"; int pipe_fd = -1; int data_fd = -1; int res = 0; int open_mode = O_RDONLY; char buffer[PIPE_BUF + 1]; int bytes_read = 0; int bytes_write = 0; //清空緩沖數組 memset(buffer, '\0', sizeof(buffer)); printf("Process %d opening FIFO O_RDONLY\n", getpid()); //以只讀阻塞方式打開管道文件,注意與fifowrite.c文件中的FIFO同名 pipe_fd = open(fifo_name, open_mode); //以只寫方式創建保存數據的文件 data_fd = open("DataFormFIFO.txt", O_WRONLY|O_CREAT, 0644); printf("Process %d result %d\n",getpid(), pipe_fd); if(pipe_fd != -1) { do { //讀取FIFO中的數據,並把它保存在文件DataFormFIFO.txt文件中 res = read(pipe_fd, buffer, PIPE_BUF); bytes_write = write(data_fd, buffer, res); bytes_read += res; }while(res > 0); close(pipe_fd); close(data_fd); } else exit(EXIT_FAILURE); printf("Process %d finished, %d bytes read\n", getpid(), bytes_read); exit(EXIT_SUCCESS); }
運行結果如下:
分析:兩個程序都使用阻塞模式的FIFO,為了讓大家更清楚地看清楚阻塞究竟是怎麼一回事,首先我們運行fifowrite.exe,並把它放到後台去運行。這時調用jobs命令,可以看到它確實在後台運行著,過了5秒後,再調用jobs命令,可以看到進程fifowrite.exe還沒有結束,它還在繼續運行。因為fifowrite.exe進程的open調用是阻塞的,在fiforead.exe還沒有運行時,也就沒有其他的進程以讀方式打開同一個FIFO,所以它就一直在等待,open被阻塞,沒有返回。然後,當我們進程fiforead.exe運行時(為了查看性能,在time命令中運行),fifowrite.exe中的open調用返回,進程開始繼續工作,然後結束進程。而fiforead.exe的open調用雖然也是阻塞模式,但是fifowrite.exe早已運行,即早有另一個進程以寫方式打開同一個FIFO,所以open調用立即返回。
從time中的輸出來看,管道的傳遞效率是非常高的,因為fiforead.exe既要讀取數據,還要寫數據到文件DataFormFIFO.txt中,10M的數據只用了0.1秒多一點。
此外,如果此時,你在shell中輸入如下命令,ls -l /tmp/my_fifo,可以看到如下結果:
證明FIFO文件確實是存在於文件系統中的文件,文件屬性的第一個字符為‘p',表示該文件是一個管道。
五、命名管道的安全問題
前面的例子是兩個進程之間的通信問題,也就是說,一個進程向FIFO文件寫數據,而另一個進程則在FIFO文件中讀取數據。試想這樣一個問題,只使用一個FIFO文件,如果有多個進程同時向同一個FIFO文件寫數據,而只有一個讀FIFO進程在同一個FIFO文件中讀取數據時,會發生怎麼樣的情況呢,會發生數據塊的相互交錯是很正常的?而且個人認為多個不同進程向一個FIFO讀進程發送數據是很普通的情況。
為了解決這一問題,就是讓寫操作的原子化。怎樣才能使寫操作原子化呢?答案很簡單,系統規定:在一個以O_WRONLY(即阻塞方式)打開的FIFO中, 如果寫入的數據長度小於等待PIPE_BUF,那麼或者寫入全部字節,或者一個字節都不寫入。如果所有的寫請求都是發往一個阻塞的FIFO的,並且每個寫記請求的數據長度小於等於PIPE_BUF字節,系統就可以確保數據決不會交錯在一起。
六、命名管道與匿名管道的對比
使用匿名管道,則通信的進程之間需要一個父子關系,通信的兩個進程一定是由一個共同的祖先進程啟動。但是匿名管道沒有上面說到的數據交叉的問題。
與使用匿名管道相比,我們可以看到fifowrite.exe和fiforead.exe這兩個進程是沒有什麼必然的聯系的,如果硬要說他們具有某種聯系,就只能說是它們都訪問同一個FIFO文件。它解決了之前在匿名管道中出現的通信的兩個進程一定是由一個共同的祖先進程啟動的問題。但是為了數據的安全,我們很多時候要采用阻塞的FIFO,讓寫操作變成原子操作。