加入星計(jì)劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴(kuò)散
  • 作品版權(quán)保護(hù)
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長期合作伙伴
立即加入
  • 正文
    • 8.2  管道
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請入駐 產(chǎn)業(yè)圖譜

進(jìn)程間通信之:管道

2013/09/13
1
閱讀需 28 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

?

8.2??管道

8.2.1??管道概述

本書在第2章中介紹“ps”的命令時(shí)提到過管道,當(dāng)時(shí)指出了管道是Linux中一種很重要的通信方式,它是把一個(gè)程序的輸出直接連接到另一個(gè)程序的輸入,這里仍以第2章中的“ps?–ef?|?grep?ntp”為例,描述管道的通信過程,如圖8.2所示。

圖8.2?管道的通信過程

管道是Linux中進(jìn)程間通信的一種方式。這里所說的管道主要指無名管道,它具有如下特點(diǎn)。

n 它只能用于具有親緣關(guān)系的進(jìn)程之間的通信(也就是父子進(jìn)程或者兄弟進(jìn)程之間)。

n 它是一個(gè)半雙工的通信模式,具有固定的讀端和寫端。

n 管道也可以看成是一種特殊的文件,對于它的讀寫也可以使用普通的read()和write()等函數(shù)。但是它不是普通的文件,并不屬于其他任何文件系統(tǒng),并且只存在于內(nèi)核的內(nèi)存空間中。

8.2.2??管道系統(tǒng)調(diào)用

1.管道創(chuàng)建與關(guān)閉說明

管道是基于文件描述符的通信方式,當(dāng)一個(gè)管道建立時(shí),它會創(chuàng)建兩個(gè)文件描述符fds[0]和fds[1],其中fds[0]固定用于讀管道,而fd[1]固定用于寫管道,如圖8.3所示,這樣就構(gòu)成了一個(gè)半雙工的通道。

圖8.3??Linux中管道與文件描述符的關(guān)系

管道關(guān)閉時(shí)只需將這兩個(gè)文件描述符關(guān)閉即可,可使用普通的close()函數(shù)逐個(gè)關(guān)閉各個(gè)文件描述符。

注意

當(dāng)一個(gè)管道共享多對文件描述符時(shí),若將其中的一對讀寫文件描述符都刪除,則該管道就失效。

2.管道創(chuàng)建函數(shù)

創(chuàng)建管道可以通過調(diào)用pipe()來實(shí)現(xiàn),表8.1列出了pipe()函數(shù)的語法要點(diǎn)。

表8.1 pipe()函數(shù)語法要點(diǎn)

所需頭文件

#include?<unistd.h>

函數(shù)原型

int?pipe(int?fd[2])

函數(shù)傳入值

fd[2]:管道的兩個(gè)文件描述符,之后就可以直接操作這兩個(gè)文件描述符

函數(shù)返回值

成功:0

出錯:-1

?

3.管道讀寫說明

用pipe()函數(shù)創(chuàng)建的管道兩端處于一個(gè)進(jìn)程中,由于管道是主要用于在不同進(jìn)程間通信的,因此這在實(shí)際應(yīng)用中沒有太大意義。實(shí)際上,通常先是創(chuàng)建一個(gè)管道,再通過fork()函數(shù)創(chuàng)建一子進(jìn)程,該子進(jìn)程會繼承父進(jìn)程所創(chuàng)建的管道,這時(shí),父子進(jìn)程管道的文件描述符對應(yīng)關(guān)系如圖8.4所示。

此時(shí)的關(guān)系看似非常復(fù)雜,實(shí)際上卻已經(jīng)給不同進(jìn)程之間的讀寫創(chuàng)造了很好的條件。父子進(jìn)程分別擁有自己的讀寫通道,為了實(shí)現(xiàn)父子進(jìn)程之間的讀寫,只需把無關(guān)的讀端或?qū)懚说奈募枋龇P(guān)閉即可。例如在圖8.5中將父進(jìn)程的寫端fd[1]和子進(jìn)程的讀端fd[0]關(guān)閉。此時(shí),父子進(jìn)程之間就建立起了一條“子進(jìn)程寫入父進(jìn)程讀取”的通道。

??????????

圖8.4??父子進(jìn)程管道的文件描述符對應(yīng)關(guān)系?????????圖8.5??關(guān)閉父進(jìn)程fd[1]和子進(jìn)程fd[0]

同樣,也可以關(guān)閉父進(jìn)程的fd[0]和子進(jìn)程的fd[1],這樣就可以建立一條“父進(jìn)程寫入子進(jìn)程讀取”的通道。另外,父進(jìn)程還可以創(chuàng)建多個(gè)子進(jìn)程,各個(gè)子進(jìn)程都繼承了相應(yīng)的fd[0]和fd[1],這時(shí),只需要關(guān)閉相應(yīng)端口就可以建立其各子進(jìn)程之間的通道。

想一想

為什么無名管道只能在具有親緣關(guān)系的進(jìn)程之間建立?

?

4.管道使用實(shí)例

在本例中,首先創(chuàng)建管道,之后父進(jìn)程使用fork()函數(shù)創(chuàng)建子進(jìn)程,之后通過關(guān)閉父進(jìn)程的讀描述符和子進(jìn)程的寫描述符,建立起它們之間的管道通信。

/*?pipe.c?*/

#include?<unistd.h>

#include?<sys/types.h>

#include?<errno.h>

#include?<stdio.h>

#include?<stdlib.h>

#define?MAX_DATA_LEN???256

#define?DELAY_TIME?1

int?main()

{

????pid_t?pid;

????int?pipe_fd[2];

????char?buf[MAX_DATA_LEN];

????const?char?data[]?=?"Pipe?Test?Program";

????int?real_read,?real_write;

????

????memset((void*)buf,?0,?sizeof(buf));

????/*?創(chuàng)建管道?*/

????if?(pipe(pipe_fd)?<?0)

????{

????????printf("pipe?create?errorn");

????????exit(1);

????}

????

????/*?創(chuàng)建一子進(jìn)程?*/

????if?((pid?=?fork())?==?0)

????{

????????/*?子進(jìn)程關(guān)閉寫描述符,并通過使子進(jìn)程暫停1s等待父進(jìn)程已關(guān)閉相應(yīng)的讀描述符?*/

????????close(pipe_fd[1]);

????????sleep(DELAY_TIME?*?3);

????????

????????/*?子進(jìn)程讀取管道內(nèi)容?*/

????????if?((real_read?=?read(pipe_fd[0],?buf,?MAX_DATA_LEN))?>?0)

????????{

????????????printf("%d?bytes?read?from?the?pipe?is?'%s'n",?real_read,?buf);

????????}

????????

????????/*?關(guān)閉子進(jìn)程讀描述符?*/

????????close(pipe_fd[0]);

????????exit(0);

????}

????else?if?(pid?>?0)

????{

????????/*?父進(jìn)程關(guān)閉讀描述符,并通過使父進(jìn)程暫停1s等待子進(jìn)程已關(guān)閉相應(yīng)的寫描述符?*/

????????close(pipe_fd[0]);

????????sleep(DELAY_TIME);

????????if((real_write?=?write(pipe_fd[1],?data,?strlen(data)))?!=??-1)

????????{

????????????printf("Parent?wrote?%d?bytes?:?'%s'n",?real_write,?data);

????????}

????

????????/*關(guān)閉父進(jìn)程寫描述符*/

????????close(pipe_fd[1]);

????

????????/*收集子進(jìn)程退出信息*/

????????waitpid(pid,?NULL,?0);

????????exit(0);

????}

}

將該程序交叉編譯,下載到開發(fā)板上的運(yùn)行結(jié)果如下所示:

$?./pipe

Parent?wrote?17?bytes?:?'Pipe?Test?Program'

17?bytes?read?from?the?pipe?is?'Pipe?Test?Program'

5.管道讀寫注意點(diǎn)

n 只有在管道的讀端存在時(shí),向管道寫入數(shù)據(jù)才有意義。否則,向管道寫入數(shù)據(jù)的進(jìn)程將收到內(nèi)核傳來的SIGPIPE信號(通常為Broken?pipe錯誤)。

n 向管道寫入數(shù)據(jù)時(shí),Linux將不保證寫入的原子性,管道緩沖區(qū)一有空閑區(qū)域,寫進(jìn)程就會試圖向管道寫入數(shù)據(jù)。如果讀進(jìn)程不讀取管道緩沖區(qū)中的數(shù)據(jù),那么寫操作將會一直阻塞。

n 父子進(jìn)程在運(yùn)行時(shí),它們的先后次序并不能保證,因此,在這里為了保證父子進(jìn)程已經(jīng)關(guān)閉了相應(yīng)的文件描述符,可在兩個(gè)進(jìn)程中調(diào)用sleep()函數(shù),當(dāng)然這種調(diào)用不是很好的解決方法,在后面學(xué)到進(jìn)程之間的同步與互斥機(jī)制之后,請讀者自行修改本小節(jié)的實(shí)例程序。

?

8.2.4??標(biāo)準(zhǔn)流管道

1.標(biāo)準(zhǔn)流管道函數(shù)說明

與Linux的文件操作中有基于文件流的標(biāo)準(zhǔn)I/O操作一樣,管道的操作也支持基于文件流的模式。這種基于文件流的管道主要是用來創(chuàng)建一個(gè)連接到另一個(gè)進(jìn)程的管道,這里的“另一個(gè)進(jìn)程”也就是一個(gè)可以進(jìn)行一定操作的可執(zhí)行文件,例如,用戶執(zhí)行“l(fā)s?-l”或者自己編寫的程序“./pipe”等。由于這一類操作很常用,因此標(biāo)準(zhǔn)流管道就將一系列的創(chuàng)建過程合并到一個(gè)函數(shù)popen()中完成。它所完成的工作有以下幾步。

n 創(chuàng)建一個(gè)管道。

n fork()一個(gè)子進(jìn)程。

n 在父子進(jìn)程中關(guān)閉不需要的文件描述符。

n 執(zhí)行exec函數(shù)族調(diào)用。

n 執(zhí)行函數(shù)中所指定的命令。

這個(gè)函數(shù)的使用可以大大減少代碼的編寫量,但同時(shí)也有一些不利之處,例如,它不如前面管道創(chuàng)建的函數(shù)那樣靈活多樣,并且用popen()創(chuàng)建的管道必須使用標(biāo)準(zhǔn)I/O函數(shù)進(jìn)行操作,但不能使用前面的read()、write()一類不帶緩沖的I/O函數(shù)。

與之相對應(yīng),關(guān)閉用popen()創(chuàng)建的流管道必須使用函數(shù)pclose()來關(guān)閉該管道流。該函數(shù)關(guān)閉標(biāo)準(zhǔn)I/O流,并等待命令執(zhí)行結(jié)束。

2.函數(shù)格式

popen()和pclose()函數(shù)格式如表8.2和表8.3所示。

表8.2 popen()函數(shù)語法要點(diǎn)

所需頭文件

#include?<stdio.h>

函數(shù)原型

FILE?*popen(const?char?*command,?const?char?*type)

函數(shù)傳入值

command:指向的是一個(gè)以null結(jié)束符結(jié)尾的字符串,這個(gè)字符串包含一個(gè)shell命令,并被送到/bin/sh以-c參數(shù)執(zhí)行,即由shell來執(zhí)行

type:

“r”:文件指針連接到command的標(biāo)準(zhǔn)輸出,即該命令的結(jié)果產(chǎn)生輸出
“w”:文件指針連接到command的標(biāo)準(zhǔn)輸入,即該命令的結(jié)果產(chǎn)生輸入

函數(shù)返回值

成功:文件流指針

出錯:-1

表8.3 pclose()函數(shù)語法要點(diǎn)

所需頭文件

#include?<stdio.h>

函數(shù)原型

int?pclose(FILE?*stream)

函數(shù)傳入值

stream:要關(guān)閉的文件流

函數(shù)返回值

成功:返回由popen()所執(zhí)行的進(jìn)程的退出碼

出錯:-1

?

3.函數(shù)使用實(shí)例

在該實(shí)例中,使用popen()來執(zhí)行“ps?-ef”命令??梢钥闯?,popen()函數(shù)的使用能夠使程序變得短小精悍。

/*?standard_pipe.c?*/

#include?<stdio.h>

#include?<unistd.h>

#include?<stdlib.h>

#include?<fcntl.h>

#define?BUFSIZE?1024

int?main()

{

????FILE?*fp;

????char?*cmd?=?"ps?-ef";

????char?buf[BUFSIZE];

????

????/*調(diào)用popen()函數(shù)執(zhí)行相應(yīng)的命令*/

????if?((fp?=?popen(cmd,?"r"))?==?NULL)

????????{

????????printf("Popen?errorn");

????????exit(1);

????}

????

????while?((fgets(buf,?BUFSIZE,?fp))?!=?NULL)

????{

????????printf("%s",buf);

????}

????pclose(fp);

????exit(0);

}

下面是該程序在目標(biāo)板上的執(zhí)行結(jié)果。

$?./standard_pipe

??PID?TTY??????????Uid????????Size?State?Command

????1??????????????root???????1832???S???init

????2??????????????root??????????0???S???[keventd]

????3??????????????root??????????0???S???[ksoftirqd_CPU0]

????……

???74??????????????root???????1284???S???./standard_pipe

???75??????????????root???????1836???S???sh?-c?ps?-ef

???76??????????????root???????2020???R???ps?–ef

?

8.2.5??FIFO

1.有名管道說明

前面介紹的管道是無名管道,它只能用于具有親緣關(guān)系的進(jìn)程之間,這就大大地限制了管道的使用。有名管道的出現(xiàn)突破了這種限制,它可以使互不相關(guān)的兩個(gè)進(jìn)程實(shí)現(xiàn)彼此通信。該管道可以通過路徑名來指出,并且在文件系統(tǒng)中是可見的。在建立了管道之后,兩個(gè)進(jìn)程就可以把它當(dāng)作普通文件一樣進(jìn)行讀寫操作,使用非常方便。不過值得注意的是,F(xiàn)IFO是嚴(yán)格地遵循先進(jìn)先出規(guī)則的,對管道及FIFO的讀總是從開始處返回?cái)?shù)據(jù),對它們的寫則把數(shù)據(jù)添加到末尾,它們不支持如lseek()等文件定位操作。

有名管道的創(chuàng)建可以使用函數(shù)mkfifo(),該函數(shù)類似文件中的open()操作,可以指定管道的路徑和打開的模式。

小知識

用戶還可以在命令行使用“mknod?管道名?p”來創(chuàng)建有名管道。

在創(chuàng)建管道成功之后,就可以使用open()、read()和write()這些函數(shù)了。與普通文件的開發(fā)設(shè)置一樣,對于為讀而打開的管道可在open()中設(shè)置O_RDONLY,對于為寫而打開的管道可在open()中設(shè)置O_WRONLY,在這里與普通文件不同的是阻塞問題。由于普通文件的讀寫時(shí)不會出現(xiàn)阻塞問題,而在管道的讀寫中卻有阻塞的可能,這里的非阻塞標(biāo)志可以在open()函數(shù)中設(shè)定為O_NONBLOCK。下面分別對阻塞打開和非阻塞打開的讀寫進(jìn)行討論。

(1)對于讀進(jìn)程。

n 若該管道是阻塞打開,且當(dāng)前FIFO內(nèi)沒有數(shù)據(jù),則對讀進(jìn)程而言將一直阻塞到有數(shù)據(jù)寫入。

n 若該管道是非阻塞打開,則不論FIFO內(nèi)是否有數(shù)據(jù),讀進(jìn)程都會立即執(zhí)行讀操作。即如果FIFO內(nèi)沒有數(shù)據(jù),則讀函數(shù)將立刻返回0。

(2)對于寫進(jìn)程。

n 若該管道是阻塞打開,則寫操作將一直阻塞到數(shù)據(jù)可以被寫入。

n 若該管道是非阻塞打開而不能寫入全部數(shù)據(jù),則讀操作進(jìn)行部分寫入或者調(diào)用失敗。

2.mkfifo()函數(shù)格式

表8.4列出了mkfifo()函數(shù)的語法要點(diǎn)。

表8.4 mkfifo()函數(shù)語法要點(diǎn)

所需頭文件

#include?<sys/types.h>
#include?<sys/state.h>

函數(shù)原型

int?mkfifo(const?char?*filename,mode_t?mode)

函數(shù)傳入值

filename:要創(chuàng)建的管道

函數(shù)傳入值

mode:

O_RDONLY:讀管道

O_WRONLY:寫管道

O_RDWR:讀寫管道

O_NONBLOCK:非阻塞

函數(shù)傳入值

mode:

O_CREAT:如果該文件不存在,那么就創(chuàng)建一個(gè)新的文件,并用第三個(gè)參數(shù)為其設(shè)置權(quán)限

O_EXCL:如果使用O_CREAT時(shí)文件存在,那么可返回錯誤消息。這一參數(shù)可測試文件是否存在

函數(shù)返回值

成功:0?

出錯:-1

表8.5再對FIFO相關(guān)的出錯信息做一歸納,以方便用戶查錯。

表8.5 FIFO相關(guān)的出錯信息

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.使用實(shí)例

下面的實(shí)例包含了兩個(gè)程序,一個(gè)用于讀管道,另一個(gè)用于寫管道。其中在讀管道的程序里創(chuàng)建管道,并且作為main()函數(shù)里的參數(shù)由用戶輸入要寫入的內(nèi)容。讀管道的程序會讀出用戶寫入到管道的內(nèi)容,這兩個(gè)程序采用的是阻塞式讀寫管道模式。

以下是寫管道的程序:

/*?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>

#define?MYFIFO??????????????"/tmp/myfifo"???????/*?有名管道文件名*/

#define?MAX_BUFFER_SIZE????????PIPE_BUF????????/*定義在于limits.h中*/

int?main(int?argc,?char?*?argv[])?/*參數(shù)為即將寫入的字符串*/

{

????int?fd;

????char?buff[MAX_BUFFER_SIZE];

????int?nwrite;

????

????if(argc?<=?1)

????{

????????printf("Usage:?./fifo_write?stringn");

????????exit(1);

????}

????sscanf(argv[1],?"%s",?buff);

????

????/*?以只寫阻塞方式打開FIFO管道?*/

????fd?=?open(MYFIFO,?O_WRONLY);

????if?(fd?==?-1)

????{

????????printf("Open?fifo?file?errorn");

????????exit(1);

????}

????

????/*向管道中寫入字符串*/

????if?((nwrite?=?write(fd,?buff,?MAX_BUFFER_SIZE))?>?0)

????{

????????printf("Write?'%s'?to?FIFOn",?buff);

????}

????close(fd);

????exit(0);

}

以下是讀管道程序:

/*fifo_read.c*/

(頭文件和宏定義同fifo_write.c)

int?main()

{

????char?buff[MAX_BUFFER_SIZE];

????int??fd;

????int??nread;

????/*?判斷有名管道是否已存在,若尚未創(chuàng)建,則以相應(yīng)的權(quán)限創(chuàng)建*/

????if?(access(MYFIFO,?F_OK)?==?-1)?

????{

????????if?((mkfifo(MYFIFO,?0666)?<?0)?&&?(errno?!=?EEXIST))

????????{

????????????printf("Cannot?create?fifo?filen");

????????????exit(1);

????????}

????}

????/*?以只讀阻塞方式打開有名管道?*/

????fd?=?open(MYFIFO,?O_RDONLY);

????if?(fd?==?-1)

????{

????????printf("Open?fifo?file?errorn");

????????exit(1);

????}

????

????while?(1)

????{

????????memset(buff,?0,?sizeof(buff));

????????if?((nread?=?read(fd,?buff,?MAX_BUFFER_SIZE))?>?0)

????????{

????????????printf("Read?'%s'?from?FIFOn",?buff);

????????}????????

????}????

????close(fd);????

????exit(0);

}????

為了能夠較好地觀察運(yùn)行結(jié)果,需要把這兩個(gè)程序分別在兩個(gè)終端里運(yùn)行,在這里首先啟動讀管道程序。讀管道進(jìn)程在建立管道之后就開始循環(huán)地從管道里讀出內(nèi)容,如果沒有數(shù)據(jù)可讀,則一直阻塞到寫管道進(jìn)程向管道寫入數(shù)據(jù)。在啟動了寫管道程序后,讀進(jìn)程能夠從管道里讀出用戶的輸入內(nèi)容,程序運(yùn)行結(jié)果如下所示。

終端一:

$?./fifo_read

Read?'FIFO'?from?FIFO

Read?'Test'?from?FIFO

Read?'Program'?from?FIFO

……

終端二:

$?./fifo_write?FIFO

Write?'FIFO'?to?FIFO

$?./fifo_write?Test

Write?'Test'?to?FIFO

$?./fifo_write?Program

Write?'Program'?to?FIFO

……

相關(guān)推薦

電子產(chǎn)業(yè)圖譜

華清遠(yuǎn)見(www.farsight.com.cn)是國內(nèi)領(lǐng)先嵌入師培訓(xùn)機(jī)構(gòu),2004年注冊于中國北京海淀高科技園區(qū),除北京總部外,上海、深圳、成都、南京、武漢、西安、廣州均有直營分公司。華清遠(yuǎn)見除提供嵌入式相關(guān)的長期就業(yè)培訓(xùn)、短期高端培訓(xùn)、師資培訓(xùn)及企業(yè)員工內(nèi)訓(xùn)等業(yè)務(wù)外,其下屬研發(fā)中心還負(fù)責(zé)嵌入式、Android及物聯(lián)網(wǎng)方向的教學(xué)實(shí)驗(yàn)平臺的研發(fā)及培訓(xùn)教材的出版,截止目前為止已公開出版70余本嵌入式/移動開發(fā)/物聯(lián)網(wǎng)相關(guān)圖書。企業(yè)理念:專業(yè)始于專注 卓識源于遠(yuǎn)見。企業(yè)價(jià)值觀:做良心教育、做專業(yè)教育,更要做受人尊敬的職業(yè)教育。