本文摘抄自linux基礎(chǔ)編程
管道指的是從一個進(jìn)程連接數(shù)據(jù)流到另一個進(jìn)程。它具有以下特點(diǎn):
- 管道是半雙工的,數(shù)據(jù)只能向一個方向流動;需要雙方通信時,需要建立起兩個管道;
- 只能用于父子進(jìn)程或者兄弟進(jìn)程之間(具有親緣關(guān)系的進(jìn)程);
- 單獨(dú)構(gòu)成一種獨(dú)立的文件系統(tǒng):管道對于管道兩端的進(jìn)程而言,就是一個文件,但它不是普通的文件,它不屬于某種文件系統(tǒng),而是自立門戶,單獨(dú)構(gòu)成一種文件系統(tǒng),并且只存在與內(nèi)存中。
數(shù)據(jù)的讀出和寫入:一個進(jìn)程向管道中寫的內(nèi)容被管道另一端的進(jìn)程讀出。寫入的內(nèi)容每次都添加在管道緩沖區(qū)的末尾,并且每次都是從緩沖區(qū)的頭部讀出數(shù)據(jù)。
本文詳細(xì)介紹管道相關(guān)信息。
管道
基于標(biāo)準(zhǔn)庫popen和pclose的進(jìn)程管道
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen允許一個程序?qū)⒘硪粋€程序作為新進(jìn)程啟動。并可以傳遞數(shù)據(jù)給它或者通過它接受數(shù)據(jù)。type僅能為"r"和“w”兩種。當(dāng)type="r"時,調(diào)用程序可以通過FILE文件流指針從被調(diào)用程序的輸出獲得數(shù)據(jù);當(dāng)type="w"時,調(diào)用程序可以通過fwrite函數(shù)向被調(diào)用程序發(fā)送數(shù)據(jù)。popen函數(shù)不支持任何其他選項。當(dāng)popen啟動的進(jìn)程結(jié)束后,需要利用pclose關(guān)閉與之關(guān)聯(lián)的文件流。如果在調(diào)用pclose時候,進(jìn)程還在運(yùn)行,那么pclose將會阻塞等待進(jìn)程結(jié)束再返回。
static void check_image(char * imagename)
{
FILE * fp;
char command[120];
char buf[200];
bool ret;
sprintf(command, "cksum %s", imagename);
fp = popen(command, "r");
if(fp == NULL) return;
fgets(buf, 200, fp);
fprintf(stdout, "Check Image: %s... Done\n", buf);
fclose(fp);
return;
}
基于系統(tǒng)調(diào)用pipe的進(jìn)程管道
#include <unistd.h>
int pipe(int pipefd[2]);
利用pipe創(chuàng)建的管道包含兩個文件描述符fd[0]以及fd[1],需要注意的是,該處是文件描述符而不是文件流,對該文件描述符進(jìn)行讀寫必須采用read和write系統(tǒng)調(diào)用,管道的兩端是固定了任務(wù)的。即一端只能用于讀,由描述字fd[0]表示,稱其為管道讀端;另一端則只能用于寫,由描述字fd[1]來表示,稱其為管道寫端。如果試圖從管道寫端讀取數(shù)據(jù),或者向管道讀端寫入數(shù)據(jù)都將導(dǎo)致錯誤發(fā)生。結(jié)構(gòu)如下圖:
從管道中讀取數(shù)據(jù):
如果管道的寫端不存在,則認(rèn)為已經(jīng)讀到了數(shù)據(jù)的末尾,讀函數(shù)返回的讀出字節(jié)數(shù)為0;
當(dāng)管道的寫端存在時,如果請求的字節(jié)數(shù)目大于PIPE_BUF,則返回管道中現(xiàn)有的數(shù)據(jù)字節(jié)數(shù),如果請求的字節(jié)數(shù)目不大于PIPE_BUF,則返回管道中現(xiàn)有數(shù)據(jù)字節(jié)數(shù)(此時,管道中數(shù)據(jù)量小于請求的數(shù)據(jù)量);或者返回請求的字節(jié)數(shù)(此時,管道中數(shù)據(jù)量不小于請求的數(shù)據(jù)量)。
向管道中寫入數(shù)據(jù):
向管道中寫入數(shù)據(jù)時,linux將不保證寫入的原子性,管道緩沖區(qū)一有空閑區(qū)域,寫進(jìn)程就會試圖向管道寫入數(shù)據(jù)。如果讀進(jìn)程不讀走管道緩沖區(qū)中的數(shù)據(jù),那么寫操作將一直阻塞。
注:只有在管道的讀端存在時,向管道中寫入數(shù)據(jù)才有意義。否則,向管道中寫入數(shù)據(jù)的進(jìn)程將收到內(nèi)核傳來的SIFPIPE信號,應(yīng)用程序可以處理該信號,也可以忽略(默認(rèn)動作則是應(yīng)用程序終止)。
管道的主要局限性正體現(xiàn)在它的特點(diǎn)上:
只支持單向數(shù)據(jù)流;
只能用于具有親緣關(guān)系的進(jìn)程之間;
沒有名字;
管道的緩沖區(qū)是有限的(管道制存在于內(nèi)存中,在管道創(chuàng)建時,為緩沖區(qū)分配一個頁面大小);
管道所傳送的是無格式字節(jié)流,這就要求管道的讀出方和寫入方必須事先約定好數(shù)據(jù)的格式,比如多少字節(jié)算作一個消息(或命令、或記錄)等等;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main()
{
pid_t pid = 0;
int fds[2], nwr = 0;
char buf[128];
pipe(fds);
pid = fork();
if(pid < 0)
{
printf("Fork error.\n");
return -1;
}else if(pid == 0)
{
printf("This is child process, pid = %d\n", getpid());
//part A
printf("Child:waiting for message...\n");
nwr = read(fds[0], buf, sizeof(buf))
printf("Child:received\"%s\"\n", buf);
//part B
printf("Child:send reply\n");
strcpy(buf, "Reply from child");
nwr = write(fds[1], buf, sizeof(buf));
printf("Child:send %d bytes to parent.\n", nwr);
}else{
printf("This is parent process, pid = %d\n", getpid());
printf("Parent:sending message...\n");
strcpy(buf, "Message from parent");
nwr = write(fds[1], buf, sizeof(buf));
printf("Parent:send %d bytes to child.\n", nwr);
//part C
printf("Parent:waiting for reply from child...\n");
nwr = read(fds[0], buf, sizeof(buf));
printf("Parent:received \"%s\" from child\n", buf);
}
return 0;
}
命名管道
管道應(yīng)用的一個重大限制是它沒有名字,因此,只能用于具有親緣關(guān)系的進(jìn)程間通信,在有名管道(named pipe或FIFO)提出后,該限制
得到了克服。FIFO不同于管道之處在于它提供一個路徑名與之關(guān)聯(lián),以FIFO的文件形式存在于文件系統(tǒng)中。這樣,即使與FIFO的創(chuàng)建進(jìn)程不存在親緣關(guān)系的進(jìn)程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信(能夠訪問該路徑的進(jìn)程以及FIFO的創(chuàng)建進(jìn)程之間),因此,通過FIFO不相關(guān)的進(jìn)程也能交換數(shù)據(jù)。值得注意的是,F(xiàn)IFO嚴(yán)格遵循先進(jìn)先出(first in first out),對管道及FIFO的讀總是從開始處返回數(shù)據(jù),對它們的寫則把數(shù)據(jù)添加到末尾。它們不支持諸如lseek()等文件定位操作。
對命名管道的操作和對文件操作相似,包括創(chuàng)建,打開,讀寫,和關(guān)閉操作。
命名管道的創(chuàng)建
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
該函數(shù)的第一個參數(shù)是一個普通的路徑名,也就是創(chuàng)建后FIFO的名字。第二個參數(shù)與打開普通文件的open()函數(shù)中的mode 參數(shù)相同。如果mkfifo的第一個參數(shù)是一個已經(jīng)存在的路徑名時,會返回EEXIST錯誤,所以一般典型的調(diào)用代碼首先會檢查是否返回該錯誤,如果確實
返回該錯誤,那么只要調(diào)用打開FIFO的函數(shù)就可以了嗎,程序如下:
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);
}
}
res=open(FIFO_NAME,open_mode);
命名管道的打開
打開的FIFO的限制是:由于FIFO是單向數(shù)據(jù)傳輸,程序不能以O(shè)_RDWR方式打開FIFO同時進(jìn)行讀寫操作,只能是O_RDONLY或者O_WRONLY方式,打開函數(shù)如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
打開FIFO文件和普通文件的另外一個差別是:O_NONBLOCK選項對open的阻塞的影響,主要分為下面幾種情況:
flags=O_RDONLY:open將會調(diào)用阻塞,除非有另外一個進(jìn)程以寫的方式打開同一個FIFO,否則一直等待。
flags=O_WRONLY:open將會調(diào)用阻塞,除非有另外一個進(jìn)程以讀的方式打開同一個FIFO,否則一直等待。
flags=O_RDONLY|O_NONBLOCK:如果此時沒有其他進(jìn)程以寫的方式打開FIFO,此時open也會成功返回,此時FIFO被讀打開,而不會返回錯誤。
flags=O_WRONLY|O_NONBLOCK:立即返回,如果此時沒有其他進(jìn)程以讀的方式打開,open會失敗打開,此時FIFO沒有被打開,返回-1。
命名管道創(chuàng)建和打開測試程序:
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#define FIFO_NAME "/tmp/my_fifo"
int main(int argc,char *argv[])
{
int res;
int open_mode=0;
if(argc < 2){
fprintf(stderr,"Usage:%s<some combination of \
O_RDONLY,O_WRONLY,O_NONBLOCK\n",*argv);
exit(EXIT_FAILURE);
}
argv++;
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",10)==0)open_mode|=O_NONBLOCK;
argv++;
if(*argv){
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",10)==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 open FIFO with %d\n",getpid(),open_mode);
res=open(FIFO_NAME,open_mode);
printf("process %d result %d\n",getpid(),res);
sleep(5);
if(res!=-1)close(res);
printf("process %d finished\n",getpid());
exit(EXIT_SUCCESS);
}
命名管道的讀寫
對命名管道的讀寫需要利用系統(tǒng)調(diào)用read函數(shù):
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
讀取管道
是否采用O_NONBLOCK非阻塞標(biāo)志對管道的讀有影響:
對一個空的,阻塞的FIFO文件的read調(diào)用將會等待,直到有數(shù)據(jù)可以讀時才繼續(xù)執(zhí)行。
對一個空的,非阻塞的FIFO的read系統(tǒng)調(diào)用將會立即返回0字節(jié)。
寫管道
需要考慮FIFO可以存在的數(shù)據(jù)長度是有限制的,在limits.h文件中由#definde PIPE_BUF語句定義,通常是4096字節(jié)。
對于設(shè)置了阻塞標(biāo)志的寫操作:
當(dāng)要寫入的數(shù)據(jù)量不大于PIPE_BUF時,linux將保證寫入的原子性。如果此時管道空閑緩沖區(qū)不足以容納要寫入的字節(jié)數(shù),則進(jìn)入睡眠,直到當(dāng)緩沖區(qū)中能夠容納要寫入的字節(jié)數(shù)時,才開始進(jìn)行一次性寫操作。
當(dāng)要寫入的數(shù)據(jù)量大于PIPE_BUF時,linux將不再保證寫入的原子性。FIFO緩沖區(qū)一有空閑區(qū)域,寫進(jìn)程就會試圖向管道寫入數(shù)據(jù),寫操作在寫完所有請求寫的數(shù)據(jù)后返回。
對于沒有設(shè)置阻塞標(biāo)志的寫操作:
當(dāng)要寫入的數(shù)據(jù)量大于PIPE_BUF時,linux將不再保證寫入的原子性。在寫滿所有FIFO空閑緩沖區(qū)后,寫操作返回。
當(dāng)要寫入的數(shù)據(jù)量不大于PIPE_BUF時,linux將保證寫入的原子性。如果當(dāng)前FIFO空閑緩沖區(qū)能夠容納請求寫入的字節(jié)數(shù),寫完后成功返回;如果當(dāng)前FIFO空閑緩沖區(qū)不能夠容納請求寫入的字節(jié)數(shù),則返回EAGAIN錯誤,提醒以后再寫;
命名管道的關(guān)閉
#include <unistd.h>
int close(int fd);
總結(jié)
管道常用于兩個方面:在shell中時常會用到管道(作為輸入輸入的重定向),在這種應(yīng)用方式下,管道的創(chuàng)建對于用戶來說是透明的;用于具有親緣關(guān)系的進(jìn)程間通信,用戶自己創(chuàng)建管道,并完成讀寫操作。FIFO可以說是管道的推廣,克服了管道無名字的限制,使得無親緣關(guān)系的進(jìn)程同樣可以采用先進(jìn)先出的通信機(jī)制進(jìn)行通信。管道和FIFO的數(shù)據(jù)是字節(jié)流,應(yīng)用程序之間必須事先確定特定的傳輸"協(xié)議",采用傳播具有特定意義的消息