進程間通信之:管道
本書在第2章中介紹“ps”的命令時提到過管道,當時指出了管道是Linux中一種很重要的通信方式,它是把一個程序的輸出直接連接到另一個程序的輸入,這里仍以第2章中的“ps–ef|grepntp”為例,描述管道的通信過程,如圖8.2所示。
圖8.2管道的通信過程
管道是Linux中進程間通信的一種方式。這里所說的管道主要指無名管道,它具有如下特點。
n 它只能用于具有親緣關系的進程之間的通信(也就是父子進程或者兄弟進程之間)。
n 它是一個半雙工的通信模式,具有固定的讀端和寫端。
n 管道也可以看成是一種特殊的文件,對于它的讀寫也可以使用普通的read()和write()等函數(shù)。但是它不是普通的文件,并不屬于其他任何文件系統(tǒng),并且只存在于內(nèi)核的內(nèi)存空間中。
8.2.2管道系統(tǒng)調(diào)用1.管道創(chuàng)建與關閉說明管道是基于文件描述符的通信方式,當一個管道建立時,它會創(chuàng)建兩個文件描述符fds[0]和fds[1],其中fds[0]固定用于讀管道,而fd[1]固定用于寫管道,如圖8.3所示,這樣就構(gòu)成了一個半雙工的通道。
圖8.3Linux中管道與文件描述符的關系
管道關閉時只需將這兩個文件描述符關閉即可,可使用普通的close()函數(shù)逐個關閉各個文件描述符。
注意
當一個管道共享多對文件描述符時,若將其中的一對讀寫文件描述符都刪除,則該管道就失效。
2.管道創(chuàng)建函數(shù)創(chuàng)建管道可以通過調(diào)用pipe()來實現(xiàn),表8.1列出了pipe()函數(shù)的語法要點。
表8.1 pipe()函數(shù)語法要點
所需頭文件
#include<unistd.h>
函數(shù)原型
intpipe(intfd[2])
函數(shù)傳入值
fd[2]:管道的兩個文件描述符,之后就可以直接操作這兩個文件描述符
函數(shù)返回值
成功:0
出錯:-1
3.管道讀寫說明用pipe()函數(shù)創(chuàng)建的管道兩端處于一個進程中,由于管道是主要用于在不同進程間通信的,因此這在實際應用中沒有太大意義。實際上,通常先是創(chuàng)建一個管道,再通過fork()函數(shù)創(chuàng)建一子進程,該子進程會繼承父進程所創(chuàng)建的管道,這時,父子進程管道的文件描述符對應關系如圖8.4所示。
此時的關系看似非常復雜,實際上卻已經(jīng)給不同進程之間的讀寫創(chuàng)造了很好的條件。父子進程分別擁有自己的讀寫通道,為了實現(xiàn)父子進程之間的讀寫,只需把無關的讀端或?qū)懚说奈募枋龇P閉即可。例如在圖8.5中將父進程的寫端fd[1]和子進程的讀端fd[0]關閉。此時,父子進程之間就建立起了一條“子進程寫入父進程讀取”的通道。
圖8.4父子進程管道的文件描述符對應關系圖8.5關閉父進程fd[1]和子進程fd[0]
同樣,也可以關閉父進程的fd[0]和子進程的fd[1],這樣就可以建立一條“父進程寫入子進程讀取”的通道。另外,父進程還可以創(chuàng)建多個子進程,各個子進程都繼承了相應的fd[0]和fd[1],這時,只需要關閉相應端口就可以建立其各子進程之間的通道。
想一想
為什么無名管道只能在具有親緣關系的進程之間建立?
4.管道使用實例在本例中,首先創(chuàng)建管道,之后父進程使用fork()函數(shù)創(chuàng)建子進程,之后通過關閉父進程的讀描述符和子進程的寫描述符,建立起它們之間的管道通信。
/*pipe.c*/
#include<unistd.h>
#include<sys/types.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#defineMAX_DATA_LEN256
#defineDELAY_TIME1
intmain()
{
pid_tpid;
intpipe_fd[2];
charbuf[MAX_DATA_LEN];
constchardata[]="PipeTestProgram";
intreal_read,real_write;
memset((void*)buf,0,sizeof(buf));
/*創(chuàng)建管道*/
if(pipe(pipe_fd)<0)
{
printf("pipecreateerrorn");
exit(1);
}
/*創(chuàng)建一子進程*/
if((pid=fork())==0)
{
/*子進程關閉寫描述符,并通過使子進程暫停1s等待父進程已關閉相應的讀描述符*/
close(pipe_fd[1]);
sleep(DELAY_TIME*3);
/*子進程讀取管道內(nèi)容*/
if((real_read=read(pipe_fd[0],buf,MAX_DATA_LEN))>0)
{
printf("%dbytesreadfromthepipeis'%s'n",real_read,buf);
}
/*關閉子進程讀描述符*/
close(pipe_fd[0]);
exit(0);
}
elseif(pid>0)
{
/*父進程關閉讀描述符,并通過使父進程暫停1s等待子進程已關閉相應的寫描述符*/
close(pipe_fd[0]);
sleep(DELAY_TIME);
if((real_write=write(pipe_fd[1],data,strlen(data)))!=-1)
{
printf("Parentwrote%dbytes:'%s'n",real_write,data);
}
/*關閉父進程寫描述符*/
close(pipe_fd[1]);
/*收集子進程退出信息*/
waitpid(pid,NULL,0);
exit(0);
}
}
將該程序交叉編譯,下載到開發(fā)板上的運行結(jié)果如下所示:
$./pipe
Parentwrote17bytes:'PipeTestProgram'
17bytesreadfromthepipeis'PipeTestProgram'
5.管道讀寫注意點n 只有在管道的讀端存在時,向管道寫入數(shù)據(jù)才有意義。否則,向管道寫入數(shù)據(jù)的進程將收到內(nèi)核傳來的SIGPIPE信號(通常為Brokenpipe錯誤)。
n 向管道寫入數(shù)據(jù)時,Linux將不保證寫入的原子性,管道緩沖區(qū)一有空閑區(qū)域,寫進程就會試圖向管道寫入數(shù)據(jù)。如果讀進程不讀取管道緩沖區(qū)中的數(shù)據(jù),那么寫操作將會一直阻塞。
n 父子進程在運行時,它們的先后次序并不能保證,因此,在這里為了保證父子進程已經(jīng)關閉了相應的文件描述符,可在兩個進程中調(diào)用sleep()函數(shù),當然這種調(diào)用不是很好的解決方法,在后面學到進程之間的同步與互斥機制之后,請讀者自行修改本小節(jié)的實例程序。
8.2.4標準流管道1.標準流管道函數(shù)說明與Linux的文件操作中有基于文件流的標準I/O操作一樣,管道的操作也支持基于文件流的模式。這種基于文件流的管道主要是用來創(chuàng)建一個連接到另一個進程的管道,這里的“另一個進程”也就是一個可以進行一定操作的可執(zhí)行文件,例如,用戶執(zhí)行“ls-l”或者自己編寫的程序“./pipe”等。由于這一類操作很常用,因此標準流管道就將一系列的創(chuàng)建過程合并到一個函數(shù)popen()中完成。它所完成的工作有以下幾步。
n 創(chuàng)建一個管道。
n fork()一個子進程。
n 在父子進程中關閉不需要的文件描述符。
n 執(zhí)行exec函數(shù)族調(diào)用。
n 執(zhí)行函數(shù)中所指定的命令。
這個函數(shù)的使用可以大大減少代碼的編寫量,但同時也有一些不利之處,例如,它不如前面管道創(chuàng)建的函數(shù)那樣靈活多樣,并且用popen()創(chuàng)建的管道必須使用標準I/O函數(shù)進行操作,但不能使用前面的read()、write()一類不帶緩沖的I/O函數(shù)。
與之相對應,關閉用popen()創(chuàng)建的流管道必須使用函數(shù)pclose()來關閉該管道流。該函數(shù)關閉標準I/O流,并等待命令執(zhí)行結(jié)束。
2.函數(shù)格式popen()和pclose()函數(shù)格式如表8.2和表8.3所示。
表8.2 popen()函數(shù)語法要點
所需頭文件
#include<stdio.h>
函數(shù)原型
FILE*popen(constchar*command,constchar*type)
函數(shù)傳入值
command:指向的是一個以null結(jié)束符結(jié)尾的字符串,這個字符串包含一個shell命令,并被送到/bin/sh以-c參數(shù)執(zhí)行,即由shell來執(zhí)行
type:
“r”:文件指針連接到command的標準輸出,即該命令的結(jié)果產(chǎn)生輸出
“w”:文件指針連接到command的標準輸入,即該命令的結(jié)果產(chǎn)生輸入
函數(shù)返回值
成功:文件流指針
出錯:-1
表8.3 pclose()函數(shù)語法要點
所需頭文件
#include<stdio.h>
函數(shù)原型
intpclose(FILE*stream)
函數(shù)傳入值
stream:要關閉的文件流
函數(shù)返回值
成功:返回由popen()所執(zhí)行的進程的退出碼
出錯:-1
3.函數(shù)使用實例在該實例中,使用popen()來執(zhí)行“ps-ef”命令??梢钥闯?,popen()函數(shù)的使用能夠使程序變得短小精悍。
/*standard_pipe.c*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#defineBUFSIZE1024
intmain()
{
FILE*fp;
char*cmd="ps-ef";
charbuf[BUFSIZE];
/*調(diào)用popen()函數(shù)執(zhí)行相應的命令*/
if((fp=popen(cmd,"r"))==NULL)
{
printf("Popenerrorn");
exit(1);
}
while((fgets(buf,BUFSIZE,fp))!=NULL)
{
printf("%s",buf);
}
pclose(fp);
exit(0);
}
下面是該程序在目標板上的執(zhí)行結(jié)果。
$./standard_pipe
PIDTTYUidSizeStateCommand
1root1832Sinit
2root0S[keventd]
3root0S[ksoftirqd_CPU0]
……
74root1284S./standard_pipe
75root1836Ssh-cps-ef
76root2020Rps–ef
8.2.5FIFO1.有名管道說明前面介紹的管道是無名管道,它只能用于具有親緣關系的進程之間,這就大大地限制了管道的使用。有名管道的出現(xiàn)突破了這種限制,它可以使互不相關的兩個進程實現(xiàn)彼此通信。該管道可以通過路徑名來指出,并且在文件系統(tǒng)中是可見的。在建立了管道之后,兩個進程就可以把它當作普通文件一樣進行讀寫操作,使用非常方便。不過值得注意的是,F(xiàn)IFO是嚴格地遵循先進先出規(guī)則的,對管道及FIFO的讀總是從開始處返回數(shù)據(jù),對它們的寫則把數(shù)據(jù)添加到末尾,它們不支持如lseek()等文件定位操作。
有名管道的創(chuàng)建可以使用函數(shù)mkfifo(),該函數(shù)類似文件中的open()操作,可以指定管道的路徑和打開的模式。
小知識
用戶還可以在命令行使用“mknod管道名p”來創(chuàng)建有名管道。
在創(chuàng)建管道成功之后,就可以使用open()、read()和write()這些函數(shù)了。與普通文件的開發(fā)設置一樣,對于為讀而打開的管道可在open()中設置O_RDONLY,對于為寫而打開的管道可在open()中設置O_WRONLY,在這里與普通文件不同的是阻塞問題。由于普通文件的讀寫時不會出現(xiàn)阻塞問題,而在管道的讀寫中卻有阻塞的可能,這里的非阻塞標志可以在open()函數(shù)中設定為O_NONBLOCK。下面分別對阻塞打開和非阻塞打開的讀寫進行討論。
(1)對于讀進程。
n 若該管道是阻塞打開,且當前FIFO內(nèi)沒有數(shù)據(jù),則對讀進程而言將一直阻塞到有數(shù)據(jù)寫入。
n 若該管道是非阻塞打開,則不論FIFO內(nèi)是否有數(shù)據(jù),讀進程都會立即執(zhí)行讀操作。即如果FIFO內(nèi)沒有數(shù)據(jù),則讀函數(shù)將立刻返回0。
(2)對于寫進程。
n 若該管道是阻塞打開,則寫操作將一直阻塞到數(shù)據(jù)可以被寫入。
n 若該管道是非阻塞打開而不能寫入全部數(shù)據(jù),則讀操作進行部分寫入或者調(diào)用失敗。
2.mkfifo()函數(shù)格式表8.4列出了mkfifo()函數(shù)的語法要點。
表8.4 mkfifo()函數(shù)語法要點
所需頭文件
#include<sys/types.h>
#include<sys/state.h>
函數(shù)原型
intmkfifo(constchar*filename,mode_tmode)
函數(shù)傳入值
filename:要創(chuàng)建的管道
函數(shù)傳入值
mode:
O_RDONLY:讀管道
O_WRONLY:寫管道
O_RDWR:讀寫管道
O_NONBLOCK:非阻塞
函數(shù)傳入值
mode:
O_CREAT:如果該文件不存在,那么就創(chuàng)建一個新的文件,并用第三個參數(shù)為其設置權(quán)限
O_EXCL:如果使用O_CREAT時文件存在,那么可返回錯誤消息。這一參數(shù)可測試文件是否存在
函數(shù)返回值
成功:0
出錯:-1
表8.5再對FIFO相關的出錯信息做一歸納,以方便用戶查錯。
表8.5 FIFO相關的出錯信息
EACCESS
參數(shù)filename所指定的目錄路徑無可執(zhí)行的權(quán)限
EEXIST
參數(shù)filename所指定的文件已存在
ENAMETOOLONG
參數(shù)filename的路徑名稱太長
ENOENT
參數(shù)filename包含的目錄不存在
ENOSPC
文件系統(tǒng)的剩余空間不足
ENOTDIR
參數(shù)filename路徑中的目錄存在但卻非真正的目錄
EROFS
參數(shù)filename指定的文件存在于只讀文件系統(tǒng)內(nèi)
3.使用實例下面的實例包含了兩個程序,一個用于讀管道,另一個用于寫管道。其中在讀管道的程序里創(chuàng)建管道,并且作為main()函數(shù)里的參數(shù)由用戶輸入要寫入的內(nèi)容。讀管道的程序會讀出用戶寫入到管道的內(nèi)容,這兩個程序采用的是阻塞式讀寫管道模式。
以下是寫管道的程序:
/*fifo_write.c*/
#include<sys/types.h>
#include<sys/stat.h>
#include<errno.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
#defineMYFIFO"/tmp/myfifo"/*有名管道文件名*/
#defineMAX_BUFFER_SIZEPIPE_BUF/*定義在于limits.h中*/
intmain(intargc,char*argv[])/*參數(shù)為即將寫入的字符串*/
{
intfd;
charbuff[MAX_BUFFER_SIZE];
intnwrite;
if(argc<=1)
{
printf("Usage:./fifo_writestringn");
exit(1);
}
sscanf(argv[1],"%s",buff);
/*以只寫阻塞方式打開FIFO管道*/
fd=open(MYFIFO,O_WRONLY);
if(fd==-1)
{
printf("Openfifofileerrorn");
exit(1);
}
/*向管道中寫入字符串*/
if((nwrite=write(fd,buff,MAX_BUFFER_SIZE))>0)
{
printf("Write'%s'toFIFOn",buff);
}
close(fd);
exit(0);
}
以下是讀管道程序:
/*fifo_read.c*/
(頭文件和宏定義同fifo_write.c)
intmain()
{
charbuff[MAX_BUFFER_SIZE];
intfd;
intnread;
/*判斷有名管道是否已存在,若尚未創(chuàng)建,則以相應的權(quán)限創(chuàng)建*/
if(access(MYFIFO,F_OK)==-1)
{
if((mkfifo(MYFIFO,0666)<0)&&(errno!=EEXIST))
{
printf("Cannotcreatefifofilen");
exit(1);
}
}
/*以只讀阻塞方式打開有名管道*/
fd=open(MYFIFO,O_RDONLY);
if(fd==-1)
{
printf("Openfifofileerrorn");
exit(1);
}
while(1)
{
memset(buff,0,sizeof(buff));
if((nread=read(fd,buff,MAX_BUFFER_SIZE))>0)
{
printf("Read'%s'fromFIFOn",buff);
}
}
close(fd);
exit(0);
}
為了能夠較好地觀察運行結(jié)果,需要把這兩個程序分別在兩個終端里運行,在這里首先啟動讀管道程序。讀管道進程在建立管道之后就開始循環(huán)地從管道里讀出內(nèi)容,如果沒有數(shù)據(jù)可讀,則一直阻塞到寫管道進程向管道寫入數(shù)據(jù)。在啟動了寫管道程序后,讀進程能夠從管道里讀出用戶的輸入內(nèi)容,程序運行結(jié)果如下所示。
終端一:
$./fifo_read
Read'FIFO'fromFIFO
Read'Test'fromFIFO
Read'Program'fromFIFO
……
終端二:
$./fifo_writeFIFO
Write'FIFO'toFIFO
$./fifo_writeTest
Write'Test'toFIFO
$./fifo_writeProgram
Write'Program'toFIFO
……