程序間通訊之Linux C管道程式設計
管道簡述
管道(pipe)是Unix/Linux中最常見的程序間通訊方式之一,它在兩個程序之間實現一個數據流通的通道,資料以一種資料流的方式在程序間流動。在系統中,管道相當於檔案系統上的一個檔案,用於快取所要傳輸的資料。在某些特性上又不同於檔案,例如當資料讀出後,管道中就沒有資料了,但檔案沒有這個特性。但管道具有兩個缺點:
·部分系統下的管道是半雙工的,資料在同一時間只能向一個方向流動。從實現的角度看,Linux核心採用環形緩衝區實現管道,如果允許同時讀寫,可能會導致資料衝突,Linux採用鎖機制來防止同時讀寫。
·管道通常來說只能在具有親屬關係(父子程序、兄弟程序)的程序間使用。
管道(也稱為匿名管道)是Linux中最古老的程序通訊機制,其應用非常廣泛,也為使用者在shell中提供了相應的管道操作符“|”。操作符“|”將其前後兩個命令連線到一起,前一個命令的輸出成為後一個命令的輸入,且可以支援使用多個“|”連線多個命令。
管道特點
·管道沒有名字,所以也稱為匿名管道
·管道是半雙工的,資料只能向一個方向流動,需要雙向通訊時,需要建立起兩個管道。
·只能用於父子程序或兄弟程序之間。
·管道對於其兩端的程序而言只是一個檔案,但它不是普通的檔案,它不屬於作業系統的某種檔案系統,而是單獨構成一種檔案系統,並且只存在於記憶體中。
·管道傳送的是無格式位元組流,這就要求使用管道的通訊雙方必須事先約定好資料的格式。
管道實現
/** * struct pipe_buffer - a linux kernel pipe buffer * @page: the page containing the data for the pipe buffer * @offset: offset of data inside the @page * @len: length of data inside the @page * @ops: operations associated with this buffer. See @pipe_buf_operations. * @flags: pipe buffer flags. See above. * @private: private data owned by the ops. **/ struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long _private; }; /** * struct pipe_inode_info - a linux kernel pipe * @mutex: mutex protecting the whole thing * @wait: reader/writer wait point in case of empty/full pipe * @nrbufs: the number of non-empty pipe buffers in this pipe * @buffers: total number of buffers (should be a power of 2) * @curbuf: the current pipe buffer entry * @tmp_page: cached released page * @readers: number of current readers of this pipe * @writers: number of current writers of this pipe * @files: number of struct file referring this pipe (protected by ->i_lock) * @waiting_writers: number of writers blocked waiting for room * @r_counter: reader counter * @w_counter: writer counter * @fasync_readers: reader side fasync * @fasync_writers: writer side fasync * @bufs: the circular array of pipe buffers * @user: the user who created this pipe **/ struct pipe_inode_info { struct mutex mutex; wait_queue_head_t wait; unsigned int nrbufs, curbuf, buffers; unsigned int readers; unsigned int writers; unsigned int files; unsigned int waiting_writers; unsigned int r_counter; unsigned int w_counter; struct page *tmp_page; struct fasync_struct *fasync_readers; struct fasync_struct *fasync_writers; struct pipe_buffer *bufs; struct user_struct *user; };
當程序建立一個匿名管道時,Linux核心為匿名管道準備了兩個檔案描述符:一個用於管道的輸入,即在管道中寫入資料;另一個用於管道的輸出,也就是從管道中讀出資料。抽象圖如下:
如果一個管道只與一個程序相聯絡,只實現程序自身的內部通訊,則這個管道是毫無意義的。通常情況下,一個建立管道的程序接著就會建立其子程序,由於父子程序可以共享開啟檔案,子程序將從父程序那裡繼承到讀寫管道的檔案描述符,這樣便建立了父子程序之間的通訊管道。
由於管道是半雙工,因此在程式設計時使用管道,需要確定資料的傳輸方向,父子程序分別關閉與之無關的描述符。例如資料從子程序傳送到父程序,則子程序關閉讀管道的描述符。
管道的讀寫規則
由於管道是半雙工通訊,因此在進行相應操作時需要注意以下幾點:
·如果從一個寫描述符關閉的管道中讀資料,當讀完所有的資料後,read函式返回0,表明已到達檔案末尾。嚴格來說,只有當沒有資料繼續寫入後,才可以說達到了檔案末尾。所以應該分清到底是暫時沒有資料輸入還是已經到達檔案末尾。如果是前者,該程序應該等待。若為多程序寫、單程序讀的情況就更為複雜。
·如果向一個讀描述符關閉的管道中寫資料,就會產生SIGPIPE訊號,不管是忽略這個訊號還是處理它,write函式都返回-1。
·常數PIPE_BUF規定了核心中管道緩衝區的大小,因為管道被設計為環形緩衝區,所以在寫管道時要注意寫入的資料大小。如果超過規定大小就會產生交錯現象,從而導致資料丟失。
Linux的管道操作
建立
Linux核心提供了函式pipe用於建立一個管道,它接收一個長度為2的int型別陣列,用於核心寫入檔案描述符。fd[0]為讀出端的描述符,fd[1]為寫入端的描述符。以下示例程式碼在同一個程序中讀寫同一個管道,這並無意義,僅作為示例。
#include <iostream>
#include <cstdlib>
#include <unistd.h>
int main()
{
int fd[2]; //建立檔案描述符陣列
char writebuf[20] = {"Hello World!"}; //寫緩衝區
char readbuf[20]; //讀緩衝區
if (pipe(fd) < 0)
{
std::cerr << "建立管道失敗\n";
exit(0);
}
write(fd[1], writebuf, sizeof(writebuf)); //向管道寫入端寫入資料
write(fd[0], readbuf, sizeof(readbuf)); //向管道讀出端讀出資料
std::cout << readbuf << "\n";
std::cout << "管道的讀fd是" << fd[0] << "\n寫fd是" << fd[1] << std::endl;
return 0;
}
管道通訊
管道的用途是為了在不同的程序中進行資料互動,但僅限於在有親屬關係的程序間使用,即父子程序、兄弟程序等。以下程式碼以父子程序間通訊為例。
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int fd[2];
pid_t pid;
char buf[20];
if (pipe(fd) < 0)
{
std::cerr << "建立管道失敗\n";
exit(0);
}
if ((pid = fork()) < 0)
{
std::cerr << "建立子程序失敗\n";
exit(0);
}
else if (pid > 0) //父程序
{
close(fd[0]); //關閉讀出端
write(fd[1], "I'm your father.\n", 17);
}
else //子程序
{
close(fd[1]); //關閉寫入端
read(fd[0], buf, sizeof(buf));
std::cout << buf;
exit(0);
}
if (waitpid(pid, NULL, 0) != pid)
{
cerr << "銷燬程序失敗\n";
}
exit(0);
}
管道高階應用
從上一段程式碼示例可看出,管道建立函式pipe通常是和程序建立函式fork或fork配合使用。Linux核心提供了將兩者“合二為一”的函式popen和pclose。
FILE *popen(const char *cmdstring, const char *type);
int pclose(FILE *stream);
與fopen和fclose函式一樣,popen和pclose必須配合使用。
函式popen用於建立管道,內部呼叫fork和exec函式執行第一個引數中指出的命令,返回一個FILE結構的指標,即用於訪問管道的指標。第二個引數則用於指出管道的型別,如果管道是以型別“r”開啟的,那麼這個管道的輸入端將連線到命令列cmdstring的標準輸出端,此時命令列的輸出可以從管道中讀入。反之,如果是以型別“w”開啟的,那麼這個管道的輸出端將連線到命令列的標準輸入端,此時向管道中寫入的資料就成為命令列的輸入資料。