还剩20页未读,继续阅读
本资源只提供10页预览,全部文档请下载后查看!喜欢就下载吧,查找使用更方便
文本内容:
UNIX/Linux进程间通信入门
(1)管道重读了《Linux程序设计(第三版)》,对进程间通信做个总结本文将介绍Unix/Linux环境下的使用管道进行的进程间通信,以主从以下几个方面进行介绍.管道的定义.进程管道.管道调用.父进程和子进程.命名管道FIFO.客户/服务器架构.什么是管道当从一个进程连接数据流到另一个进程时,我们使用术语管道(pipe)我们通常是把一个进程的输出通过管道连接到另••个进程的输入大多数Linux用户都使用过shell不可避免地会使用管道进行一些数据的分析和处理,命令格式如下cmd1|cmd2shell负责安排两个命令的标准输入和标准输出,这样cmdl的标准输入来自终端键盘cmdl的标准输出传递给cmd2作为它的标准输入cmd2的标准输出连接到终端屏幕shell所做的工作从效果上看是对标准输出和标准输出流进行了重新连接,使数据流从键盘输出通过两个命令最终输出到屏幕上当然这样的是利用shell的命令行形式,我们完成了Linux环境下的进程间管道通信,如何用C语言实现,我们接下来将会看到.进程管道最简单的两个程序之间的数据传递的方法就是使用popen和pclose函数它们的原型如卜.#includestdio.hFILE*popcn(constcharcommandconstchar*opcn_modc);
一、popen函数popen函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据command字符串要运行的程序名和相应的参数opcn_modc必须是•”或者w如果pen」110de是“r“,被调用程序的输出就可以被调用程序使用,调用程序利用popen函数返回的FILE*文件流指针,就可以通过常用的s【di库函数(如fread)来读取被调用程序的输出如果open_mode是“w”,调用程序就可以用fwrite调用向被调用命令发送数据,而被调用程序可以在自己的标准输入上读取这些数据被调用的程序通常不会意识到自己正在从另一个进程读取数据,它只是简单在标准输入上读取数据,然后做出相应的操作每个popen调用都必须指定”r“或”w,在popen函数的标准实现中不支持任何其他选项这就意味着我们不能调用另一个程序并同时对它进行读写操作popen函数在失败时返回一个空指针如果你想通过管道实现双向通信,最普通的解决方法是使用两个管道,每个管道负责一个方向的数据流
二、pclose函数用popen启动的进程结束时,我们可以用pclose函数关闭与之关联的文件流pclose调用只在popen启动的进程结束后才返回如果调用pclose时它仍在运行,pclose调用将等待该进程结束}〃”数据消费者”程序pipc
4.c负责读取数据,它的代码要简单的多,如下所示#includeunistd.h#includestdlib.h#includestdio.h#includestring.h
48.intmainintargcchar*argv[]intlength=0;intfilc_dcscriptor;charbufferfBUFSIZ+1];mcmsctbuffcr0sizcofbuffcr;sscanfargv
[1]%dfile_descriptor;length=readfile_descriptorbufferBUFSIZ;printf%d-read%dbytes:%s/ngetpidlengthbuffer;returnEXIT_SUCCESS;}.编译程序gccpipc
4.c-opipc4gccpipe
3.c-opipe3运行程序
65../pipe
366.10733-wrote13bytes
67.10734-read13bytes:MONKEY.D.MENG实验解析pipc3程序的开始部分和前面的例子一样,用pipe调用创建一个管道,然后用fork调用创建一个新进程接下来,它用sprinf把读取管道数据的文件描述符保存到一个缓存区中,该缓存区中的内容将构成pipe4程序的一个参数
一、管道关闭后的读操作在继续学习之前,我们再来仔细研究一下打开的文件描述符到此,我们一直采取的是让读进程简单的读取一些数据然后直接退出的方式,并假设Linux会把清理文件当作是在进程结束时应该做的工作的一部分但大多数从标准输入读取的数据的程序采用的却是与我们迄今为止见到的例子非常不同的另外一种做法通常它们并不知道有多少数据需要读取,所以往往采用循环的方法,读取数据…•处理数据•一读取更多的数据,直到没有数据可读为止当没有数据可读时,read调用通常会阻塞,即它将暂停进程以等待直到有数据到达为止如果管道的另一端已经被关闭,也就是说没有进程打开这个管道并向它写数据了,这时read调用就会被阻塞但这样的阻塞不是很有用,因此对一个已经关闭写数据的管道做read调用将返回0而不是阻塞这就使读进程能够像检测文件结束一样,对管道进行检测并作出相应的动作注意,这与读取一个无效的文件描述符不同,read把无效的文件描述符看作一个错误并返回-1如果我们跨越fork调用使用管道,就会有两个不同的文件描述符可以用于向管道写数据,一个在父进程中,一个在子进程只有把父子进程中的针对管道的写文件描述符都关闭,管道才会被认为是关闭了对管道的read调用才会失败我们还将对这一问题做更深入的讨论,在学习到O_NONBLOCK标志和FIFO时,我们将看到一个这样的例子
二、把管道用作标准输入和标准输出现在,知道了如何使得一个空管道的读操作失败,下面我们来看一种用管道连接两个进程的更简洁的方法我们把其中一个管道文件描述符设置为一个已知值,一般是标准输入0或标准输出1在父进程中做这个设置稍微有点复杂,但它使得子程序的编写变得非常简单这样做的最大好处是我们可以调用标准程序,即那些不需要以文件描述符为参数的程序为了完成这个工作,我们需要使用dup函数dup函数有两个紧密关联的版本,它们的原型如下所示#includcunistd.hindupintfile_descriptor;intdup2intfi1e_descriptor_oneintfile_descriptor_two;dup调用的目的是打开一个新的文件描述符,这与pen调用有点类似不同之处在于,dup调用创建的新文件描述符与作为它的参数的那个已有文件描述符指向同一个文件或管道对于dup函数来说新的文件描述符总是取最小的可用值,而对于dup2函数来说,它所创建的新文件描述符或者与参数file_descriptor_two相同,或者是第一个大于该参数的可用值我们可以使用更通用的fcntl调用command参数设置为F_DUPFD来达到与调用dup和dup2相同的效果虽然如此,但dup调用更易于使用,因为它是专门用于复制文件描述符的而且它的使用非常普遍,你可以发现,在已有程序中,它的使用比fcntl和F_DUPFD更频繁那么,dup是如何帮助我们在进程间传递数据的呢?诀窍在于,标准输入的文件描述符总是0而dup返回的新文件描述符又总是使用最小可用的数字因此,我们首先关闭文件描述符0然后调用dup那么新的文件描述符就是数字0因为新的文件描述符是复制一个已有的文件描述符,所以标准输入就会改为指向一个我们传递给dup函数的文件描述符所对应的文件或管道我们创建了两个文件描述符,它们指向同一个文件或管道,而且其中之一是标准输入这次,我们把子程序的stdin文件描述符替换为我们创建的管道的读取端我们还将对文件描述符做一些整理,使得子程序可以正确地检测到管道中数据的结束与往常一样,为了简洁起见,我们省略了一些错误检查[cpp]viewplaincopy#includcunistd.h#inckidestdlib.h#includestdio.h#includcstring.hintmain1intlength=0;intpipcs|2J;constchardata[]=MONKEY.D.MENG;pid_tchild_pid;ifpipepipes==01child_pid=fork;ifchild_pid==-1fprintfstderrForkfailure!;returnEXIT_FAILURE;}ifchild_pid==0{closeO;duppipcsK];closepipes0];//子进程的写文件描述符关闭,管道不会关闭closepipes[l];execlpododH-cchar*0;returnEXIT_SUCCESS;}else{closepipes
[0];length=writcpipcs[l]datastrlcndata;//父进程的写文件描述符关闭,管道才会关闭closepipes[l];printf%d-wrote%dbytes/ngetpidlength;}returnEXIT_SUCCESS;编程程序gccpipe
5.c-opipc5运行程序5l../pipe
552.10874-wrote13bytes
53.0000000MONKEY.D.MENG
54.0000015实验解析与往常一样,这个程序创建一个管道,然后通过fork创建一个子进程此时,父子进程都有可以访问管道的文件描述符,一个用于读数据,一个用于写数据,所以总共有四个打开的文件描述符我们首先看子进程子进程先用close0关闭它的标准输入,然后调用duppipe
[0]把与管道的读取端关联的文件描述符复制为文件描述符,即标准输入接卜.来,子进程关闭原先的用来从管道读取数据的文件描述符pipes
[0]因为子进程不会向管道写数据,所以它把与管道关联的写操作文件描述符pipes]也关闭了现在,它只有一个与管道关联的文件描述符,即文件描述符0它的标准输入接下来,子进程就可以用cxec函数启动任何从标准输入读取数据的程序了在本例中我们使用的是od命令od命令将等待数据的到来,就好像它在等待来自用户终端的输入一样事实上,如果没有明确使用检测这两者之间不同的特殊代码,它并不知道输入是来自一个管道,而不是来自一个终端父进程首先关闭管道的读取端pipes[O]因为它不会从管道读取数据接着它向管道写入数据当所有数据都写完后,父进程关闭管道的写入端并退出因为现在已经没有打开的文件描述符可以向管道定数据了,od程序读取写到管道中的三个字节数据后,后续的读操作将返回0字节,表示已经到达文件尾当读操作返回时.,od程序就退出运行这类似于在终端上运行od命令,然后按下Cirl+D组合键发送文件尾标志
6.命名管道FIFO到此,我们还只能在相关的程序之间传递数据,即这些程序是由一个共同的祖先进程启动的但如果我们想在不相关的进程之间交换数据,这还不是很方便的我们可以用FIFO文件来完成这项工作,它通常也被称为命名管道命名管道是一种特殊类型的文件不要忘记了Linux环境下所有的事物皆是文件,它在文件系统中以文件名的形式存在,但它的行为却和我们已经见过的没有名字的管道类似我们可以在命令行上创建命名管道,也可以在程序中创建它从历史上看,命令行上用来创建命名管道的程序一直是mknod如下所示inknodfilenamep但mknod命令并未出现在X/Open规范的命令列表中,所以可能并不是所有的类UNIX系统都可以这样做推荐使用的命令行如下所示mkfifbfilename有些老版本的UNIX系统只有mknod命令X/Open规范的第四期第二版本中有mknod函数调用,但没有对应的命令行程序Linux系统非常友好,它同时支持mknod和mkfifoo在程序中,我们可以使用两个不同的函数调用,如下所示#includesys/types.h#includesys/stat.hintmkfifoconstchar*filenameinode_tmode;intniknodconstchar*filenameniode_tmode|S」FIFOdev_t0;与mknod命令一样,可以用mknod函数建立许多特殊类型的文件要想通过这个函数创建一个命名管道,唯一具有可移植性的方法是使用一个有dev」类型的值0并将文件访问模式与S」FIFO按位或我们在下面的例子中将使用较简单的mkfifo函数下面程序tifol.c是创建命名管道[epp]viewplaincopy#includeunistd.h#includcstdlib.h#includestdio.h#includesys/types.h#includesys/stat.hintmain{intres=0777;ifres==0H.{.printfFIFOcreated!/n;.}..returnEXIT_SUCCESS;.}.编译程序.gccfifol.c-ofifol.运行程序../fifol.FIFOcreated实验解析我们可以用卜面命令找到刚创建的管道Is-IFprwxrwxr-xImonkeymonkey010月1417:11my_fifo|注意,输出结果中的第一个字符为p表示这是一个管道最后的|符号是由1s命令的-F选项添加的它也表示这是一个管道这个程序用mkfifo函数创建一个特殊的文件虽然我们要求的文件模式是()777但它被用户掩码(□mask)设置(我所用的服务器为0002)给改变了,这与普通文件的创建是一样的,所以文件的最终模式是775如果你的掩码设置与这里不同,则你会看到一个不同的权限我们可以像删除一个普通文件那样用rm命令删除FIFO文件或者也可以在程序中用unlink系统调用来删除它
一、访问FIFO文件命名管道的一个非常有用的特点是由于它们出现在文件系统中,所以它们可以像平常的文件名一样在命令中使用在把创建的FIFO文件用在程序设计中之前,我们先通过普通的文件命令来观察FIFO文件的行为实验访问FIFO文件
(1)首先,我们来尝试读这个(空的)FIFO文件catmy_fifo
(2)现在,尝试向FIFO写数据你必须用另一个终端来执行下面命令,因为目前将挂起第一个命令,等等数据出现在FIFO中echo“MONKEY.D.MENG”myfifb你将看到cat命令产生输出如果不向FIFO发送任何数据,cat命令将一直挂起,直到你中断它,常用的中断方式是使用组合键”Ctrl+C”
(3)我们可以将第一个命令放在后台执行,这样即可一次执行两个命令catmy_fifoecho“MONKEY.D.MENG”myfifb实验解析一因为FIFO中没有数据,所以cat和echo程序都阻塞了,cat等待数据的到来,而echo等待其他进程读取数据在上面的第三步中,cat进程一开始就在后台被阻塞了,当echo向它提供一些数据之后,cat命令读取这些数据并把它们打印到标准输出上,然后cat程序退出,不再等待更多数据它没有阻塞是因为第二个命令将数据放入FIFO后,管道将被关闭,所以cal程序中的read调用返回0字节,表示已经到达文件尾现在已经看过用命令行程序访问FIFO的情况,接下来我们将仔细分析FIFO的编程接口,它可以让我们在访问FIFO文件时更多地控制其读写行为与通过pipe调用创建管道不同的是,FIFO是以命名文件的形式存在,而不是打开文件描述符,所以在对它进行读写操作之前必须先打开它FIFO也用open和close函数打开和关闭,这与我们前面看到的对文件的操作一样,但它多了一些额外的功能对FIFO来说,传递给pen调用的是FIFO的路径名,而不是一个正常的文件打开FIFO的一个主要限制是,程序不能以O_RDWR模式打开FIFO文件进行读写操作,这样做的后果并未明确定义但这个限制是有道理的,因为我们通常使用FIFO只是为了单向传递数据,所以没有必要使用O_RDWR模式如果一个管道以读/写方式打开,进程就会从这个管道读【可它自己的输出如果确实需要在程序之间双向传递数据,最好使用一对FIFO或管道,一个方向使用一个,或者但不常用采用先关闭再重新打开FIFO的方法来明确的改变数据流的方向打开FIFO文件和打开普通文件的另一点区别在于对open.flag的O_NONBLOCK选项的用法使用这个选项不仅改变open调用的处理方式,还会改变对这次open调用返回的文件描述符进行的读写请求的处理方式CLRDONLY、O_WRONLY和O_NONBLOCK标志共有4种合法的组合方式,我们将逐个介绍它们openconstchar*pathORDONLY;止匕时,open调用将阻塞,除非有一个进程以写方式打开同一个FIFO否则它不会返回这与前面第一个cat命令的例子类似openconslchar*pathORDONLY|O.NONBLOCK;即使没有其他进程以写方式打开FIFO这个将成功并立刻返【可openconstchar*pathO_WRONLY;此时,open调用将阻塞,直到有一个进程以读方式打开同一个FIFO为止opcnconstchar水pathO_WRONLY|O_NONBLOCK;这个函数调用总是立刻返回,但如果没有进程以读方式打开FIFO文件、open调用将返回一个错误-1并且FIFO也不会被打开如果确实有一个进程以读方式打开FIFO文件,那么我们就可以通过它返回的文件描述符对这个FIFO文件进行写操作请注意O_NONBLOCK分别搭配O_RDONLY和O.WRONLY在效果上的不同,如果没有进程以读方式打开管道,非阻塞写方式的pen调用将失败,但非阻塞读方式的open调用总是成功close启用的行为并不受O_NONBLOCK标志的影响下面我们来看,如何通过使用带O_NONBLOCK标志的open调用的行为来同步两个进程我们在这里并没有选择使用多个示例程序的做法,而是只使用一个测试程序fif2c通过给该程序传递不同的参数的方法来观察FIFO的行为[epp]viewplaincod、#includeunistd.h#includcstdlib.h#includestdio.h#includestring.h#inckidefcntl.h#includesys/types.h#includesys/stat.h#defineFIFO.NAME/tmp/my_fifo
10.
11.intniainintargcchar*argv[]12」intres=0;intopen_mode=0;ifargc2fprintfstderrUsage:%ssomecombinationofO_RDONLYO.WRONLYO_NONBLOCK/n*argv;returnEXIT.FAILURE;++argv;ifstmcmp*argvO_RDONLY8==0open_mode|=O_RDONLY;ifsirncmp*argvO.WRONLY8==0open.mode|=O_WRONLY;ifstrncmpC*argvO_NONBLOCK;8==0opcn_modc|=O_NONBLOCK;++argv;if*argvifstrncmp*argvO_RDONLY;8==0open_mode|=O_RDONLY;ifstrncmp*argvO.WRONLY8==0open.mode|=O.WRONLY;ifstrncmp*argvO-NONBLOCK8==0open_mode|=O_NONBLOCK;}ifaccessFIFO_NAMEF_OK==-l{res=mkfifoFIFO_NAME0777;ifres!=fprintfstderrCouldnotcreatefifo%s/nFIFO_NAME;returnEXIT_FAILURE;}}printfProcess%dopeningFIFO/ngetpidO;res=opcnFIFO_NAMEopcn_mode;printfProcess%dresult%d/ngetpidres;sleep5;ifres!=-1{closeres;}printfProcess%dfinished/ngepid;returnEXIT_SUCCESS;}编译程序geefifo
2.c-ofifo2运行程序:
61../fifo2O_RDONLY
62.2112326
[1]Exit
127./fifoO_RDONLYProcess12326openingFIFO
66../fifo2O_WRONLYProcess12327openingFIFOProcess12327result3Process12326result3Process12327finishedProcess12326finished实验解析这个程序允许我们在命令行上指定我们希望使用的O_RDONLY、O.WRONLY和O_NONBLOCK的组合方式它会把命令行参数与程序中的常量字符串进行比较,如果匹配,就(用!=操作符)设置相应的标志程序用access函数来检查FIFO文件是否存在,如果不存在就创建它在程序中,一直到最后都没有删除这个FIFO文件,因为我们没办法知道是否有其他程序正在使用它当一个Linux进程被阻塞时,它并不消耗CPU资源,所以这种进程的同步方式对CPU来说是非常有效率的有关FIFO的读写规则,可参考博文《Linux命名管道FIFO的读写规则》为了演示不相关的进程是如何使用命名管道进程通信的,我们需要用到两个独立的程序fifo
3.c和fifo
4.Co第一个程序是“生产者”程序它在需要时创建管道,然后尽可能快地向管道中写入数据注意,出于演示的目的,我们并不关心写入数据的内容,所以我们并未对缓冲区buffer进行初始化[epp]viewplaincopy#includeunistd.h#includcstdlib.h#includestdio.h#includestring.h#inckidefcntl.h#includelimits.h#includesys/typcs.h#inckidesys/stat.h#dcfincFIFO_NAME7tmp/my_fifo#defineBUFFER_SIZEPIPE_BUF#dcfineTEN.MEG1024*1024*10intmain{intpipe_fd;intres;intopcn_modc=O_WRONLY;intbytes_senl=0;
2.charbuffer]BUFFER_SIZE+1j;ifaccessFIFO_NAMEF_OK==-I{res=mkfifoFIFO_NAME0777;ifres!=0fprinlfsderr.Couldnotcreatefifo%s/nFIFO_NAME;returnEXIT.FAILURE;}}printfProcess%dopeningFIFOO_WRONLY/ngetpid;pipc_fd=opcnFIFO_NAMEopcn_mode;printfProcess%dresult%d/ngetpidpipe_fd;ifpipe_fd!=-1whilcbytes_sentTEN_MEG{res=writepipe_fdbufferBUFFER.SIZE;ifres!=-1{fprintfstderrWriteerroronpipe/n;returnEXIT.FAILURE;}bytcs_scnt+=res;}closepipe_fd;}elsereturnEXIT.FAILURE;returnEXIT.SUCCESS;}第二个程序是“消费者”程序,它的代码要简单的多,它从FIFO读取数据并丢弃它们#includeunistd.h#includcstdlib.h#inckidestdio.h.#includestring.h#includcfcntl.h#inchidelimits.h#includesys/types.h#inckidesys/stat.h#dcfincF1FO_NAME7tmp/my_fifo#defineBUFFER_SIZEPIPE_BUFintmain1intpipe_fd;intres=0;intopen_mode=O_RDONLY;charbuffer[BUFFER_SIZE+1J;intbytes_read=0;memsetbuffer
0.sizeofbuffer;printfProcess%dopeningFIFOO_RDONLY/ngetpid;pipe_fd=openFIFO_NAMEopen_mode;printfProcess%dresult%d/ngetpidpipe_fd;ifpipe_fd!=-1dores=rcadpipc_fdbufferBUFFER_SIZE;bytes_read+=res;}whileres0;closepipe_fd;}else{returnEXIT_FAILURE;}returnEXIT.SUCCESS;}编程程序gccfifo
3.c-ofifo3gccfifo
4.c-ofifo4运行程序:
14../fifo
3105.fi]13002Process13002openingFIFOO_WRONLYtime./fifo4Process13003openingFIFOO_RDONLYProcess13003result3Process13002result3Writeerroronpipe[I]+Exit
1./fifb3realOmO.OOlsuserOmO.OOOssysOniO.OO2s实验解析我们在运行这两个程序时,用time命令对读进程进行计时两个程序都是使用的都是阻塞模式的FIFOo我们首先启动fifo3写进程/生产者,它将阻塞以等待读进程打开这个FIFOfifo4读进程/消费者启动以后,写进程解除阻塞并开始向管道写数据同时,读进程也开始从管道中读取数据Linux会安排好这两个进程之间的调度,使它们在可以运行的时候运行,在不能运行的时候阻塞因此,写进程将在管道满时阻塞,读进程将在管道空时阻塞pclose调用的返回值通常是它所关闭的文件流所在的进程的退出码如果调用进程在调用pclose之前执行一个wait语句,被调用进程的退出状态就会丢失,pclose将返回-1并设置ermo为EHILD现在来看•个简单的popcn和pclose示例程序popenl.Co我们将在程序中用popcn访问unamc命令给出的信息命令uname-a的作用是打印系统信息,包括计算机型号、操作系统名字、版本和发行号,以及计算机的网络名完成程序的初始化工作后,打开一个连接到uname命令的管道,把管道设置为可读方式并让read_fp指向该命令的输出最后,关闭read,p指向的管道[cpplviewplaincopy#includeunistd.h#includestdlib.h#includestdio.h#includestring.hintmain1FILE»read_fp=NULL;charbufferfBUFSIZ+I];intchars_read=0;memsetbuffer
0.sizeofbuffer;read_fp=popenuname-ar;ifread_fp!=NULLchars_read=freadbuffersizeofcharBUFSIZread_fp;ifchars_read0printfOutput:/n%s/nbuffer;returnEXIT_SUCCESS;returnEXIT.FAILURE;}编译程序gccpopen
1.c-opopen1运行程序./popen1Output:Linuxlocalhost.localdomain
2.
6.25-
14.fc
9.i686#1SMPThuMay106:28:41EDT2008i686i686i386GNU/Linux实验解析这个程序用popen调用启动带有-a选项的uname命令然后用返回的文件流读取最多BUFSIZ这个常量是在stdio.h中用#define语句定义的个字符的数据并将它们打印出来显示在屏幕上因为我们是在程序内部捕获uname命令的输出,所以即可处理它了time命令的输出显示,在我们实验室的服务器上,读进程只运行了不到
0.01s的时间,却读取了IOM字节的数据这说明管道在程序之间传递数据是很有效率的
二、高级主题使用FIFO的客户/服务器应用程序我们来考虑如何通过使用命名管道来编写一个客户/服务器端应用嗜我们想只用一个服务器进程来接受请求,对它们进行处理,最后把结果数据返回给发送请求的一方客户我们想允许多个客户进程都可以向服务器发送数据为了使问题简单化,我们假设被处理的数据可以被拆分为一个个数据块,每个的长度都小于PIPE_BUF字节当然我们可以用很多方法实现这个系统,但在这里我们只考虑一种方法,即可以全责如何使用命名管道的方法因为服务器每次只能处理一个数据块,所以只使用一个FIFO应该是合乎逻辑的,服务器通过它读取数据,每个客户向它写数据只要将FIFO以阻塞模式打开,服务器和客户就会根据需要自动被阻塞将处理后的数据返回给客户稍微有些困难我们需要为每个客户安排第二个管道来接收返回的数据通过在传递给服务器的原先数据中加上客户进程的标识符PID双方就可以使用它来为返回数据的管道生成一个唯一的名字首先,我们需要一个头文件clienLh它定义了客户和服务器程序都会用到的数据为了方便使用,它还包含了必要的系统头文件[cpp]viewplaincopy〃头文件client.h#includeunistd.h#inckidestdlib.h#includestdio.h#includcstring.h#inckidesys/types.h#includesys/stat.h#inchidefcntl.h#includelimits.h#defineSERVER_FIFO_NAME^/tmp/serv.fifo#defineCLIENT_FIFO_NAME7tmp/cli_%d_fifo#defineBUFFER.SIZE20structmessagepid_tclicnt_pid;chardata[BUFFER_SIZE+1];};〃客户端程序clicnt.cincludeclient.hinimainintsener_fifo_fd;intclient_fifo_fd;structmessagemsg;charclient_fifo
[256];server_fifo_fd=openSERVER_FIFO_NAMEO.WRONLY;ifserver_fifo_fd==-lfprintfstderrSorrynoserver/n;returnEXIT.FAILURE;}msg.client_pid=getpid;sprintfclient_fifoCLIENT_FIFO_NAMEmsg.client_pid;ifmkfifdclient_fifb0777==-1{fprintfstderrSorrycannotmake%s/nclient_fifo;returnEXIT_FAILURE;}sprintfmsg.dataMONKEY.D.MENG;printf%dsent%s/nmsg.client_pidmsg.data;writcscrvcr_fifo_fdmsgsizcofmsg;client_fifo_fl=openclient_fifoO_RDONLY;ifclicnt_fifo_fd!=-Iifreadclienl_fifo_fdmsgsizeofmsg0prinlfreceived:%s/nmsg.data;}closeclient_fifo_fd;}closeserver_fifo_fd;unlinkclicnt_fifo;returnEXIT.SUCCESS;}//服务器端程序server.c#inckideclient.h.intniainintclient_fifo_fd;intserver_fifo_fd;structmessagemsg;intread_res=0;charclient_fifo
[256];mkfifoSERVER_FIFO_NAME0777;server_fifo_fd=openSERVER_FIFO_NAMEO_RDONLY;ifsei-ver_fifo_fd==-1fprintfstderrServerfifofailure/nu;returnEXIT.FAILURE;}sleeplO;do1rcad_res=rcadsen^cr_fifo_fdmsgsizcofstructmessage;ifread_res0{sprintfclient_fifoCLIENT_FIFO_NAMEmsg.client_pid;client_fifo_fd=openclient_fifoO_WRONLY;ifclient_fifo_fd!=-lwriteclient_fifo_Rimsgsizeofstructmessage;closcclicnt_fifo_fd;}}}whilcrcad_rcs0;closeserver_fifo_fd;unlinkSERVER_FIFO_NAME;returnEXIT.SUCCESS;}编译程序gccclicnt.c-oclientgccserver.c-oserxer运行程序./server
13641./client13642sentMONKEY.D.MENGreceived:MONKEY.D.MENG
[1]+Done./server实验解析服务器端是以只读模式创建它的FIFO并阻塞,直到第一个客户以写方式打开同一个FIFO来建立连接为止此时,服务器进程解除阻塞并执行sleep语句,这使得客户的数据排除等候,然而,在实际应用程序中,应该把sleep语句删除我们在这里使用它的目的只是为了演示当有多个客户的请求同时到达时程序的正确操作方法与此同时,客户打开了服务器FIFO后,它创建自己唯一的一个命名管道以读取服务器返回的数据完成这些工作后,客户发送数据给服务器,如果管道满或服务器仍在休眠中就阻塞,并阻塞在对自己的FIFO的read调用上,等待服务器的响应接收到来自客户的数据后,服务器处理它,然后以写的方式打开客户管道并将处理后的数据返回,这将解除客户的阻塞状态客户被解除阻塞后,它即可从自己的管道中读取服务器返回的数据整个处理过程不断重复,直到最后一个客户关闭服务器管道为止,这将使服务器的read调用失败(返同0)因为已经没有进程以写方式打开服务器管道了如果这是一个真正的服务器进程,它还需要继续等待客户的请求,我们就需要对它进行修改,有两种方法,如下所示
(1)对它自己的服务器管道打开一个文件描述符,这样read调用将阻塞而不是返回0
(2)当read调用返回0时・,关闭并重新打开服务器管道,使服务器进程阻塞在pen调用处以等待客户的到来,就像它最初启动时那样
3.将输出送往popen看过捕获外部程序输出的例子后,我们再来看一个将输出发送到外部程序示例程序popen
2.c它将数据通过管道送往另一个程序我们在这里使用的是1八进制输出命令[cpp]viewplaincopy#inchideunistd.h#includestdlib.h#includestdio.h#includestring.hintniain{FILE*write_fp=NULL;charbufferfBUFSIZ+1];sprintfbufferMONKEY.D.MENG;write_fp=popenodcw;ifwrite_fp!=NULLfwriiebuffersizeofcharstrlenbufferwrite_fp;pclosewrite_fp;returnEXIT_SUCCESS;}returnEXIT_FAILURE;}编译程序gccpopen
2.c-opopen2运行程序
26../popen
227.000000MONKEY.D.MENG280000015实验解析程序使用带有参数“w”的popen启动odY命令,这样就可以向该命令发送数据了然后它发送一个字符串给od-c命令,该命令接收并处理它,最后把处理结果打印到自己的标准输出上在命令行上,我们可以用下面的命令得到同样的输出结果echo“MONKEY.D.MENG”|od-c
一、传递更多的数据我们迄今为止所使用的机制是简单的将所有数据通过一次fread或fwrite调用来发送或接收有时,我们可能希望以块方式发送数据,或者我们根本就不知道输出数据的长度为了避免定义一个非常大的缓冲区,我们可以用多个fread或fwriie调用来按部分处理数据卜.面这个程序popcn
3.c通过管道读取大量数据在这个程序中,我们从被调用的进程“ps-ax”中读取数据该进程输出的数据有多少事先无法知道,所以我们必须对管道进行多次读取[cpp]viewplaincopy#includeunistd.h#inckidestdlib.h#includestdio.h#includestring.hintmainFILE*read_fp=NULL;charbuffer[BUFSIZ+IJ;intchars_read=0;mcmsctbuffcr0sizcofbuffcr;read_fp=popenps-axr;ifread_fp!=NULLchars_read=freadbuffersizeofcharBUFSIZread_fp;whilechars_read0buffcr[chars_rcad-1]=0;printfReading:/n%s/nbuffer;chars_read=freadbuffersizeofcharBUFSIZread_fp;pcloseread_fp;returnEXIT.SUCCESS;returnEXIT.FAILURE;}编译程序gccpopen
3.c-opopen3运行程序
34../popen3Reading:P1DTTYSTATTIMECOMMAND这个程序调用popen函数时使用了“i■”参数,这与popenl.c程序的做法一样这次,它连续地从文件流中读取数据,直到没有数据可读为止注意,虽然ps命令的执行要花费一些时间,但Linux会安排好进程间的调度,让两个程序在可以运行时继续运行如果读进程popen3没有数据可读,它将被挂起直到有数据到达如果写进程ps产生的输出超过了可用缓冲区的长度,它也会被挂起直到读进程读取了一些数据
二、如何实现popen请求popen调用运行一个程序时,它首先启动shell即系统中的sh命令,然后将command字符串作为一个参数传递给它这有两个效果,一个好,一个不太好在Linux中,所有的参数扩展都是由shell完成的所以在启动程序之前先启动shell来分析命令字符串,就可以使各种shell扩展在程序启动之前就全部完成这个功能非常有用,它允许我们通过popen启动非常复杂的shell命令而其他一些创建进程的函数如execl调用起来就复杂得多,因为调用进程必须自己去完成shell扩展使用shell的一个不太好的影响是,针对每个popen调用,不仅要启动一个被请求的程序,还要启动一个shell即每个popen调用将多启动两个进程,从节省系统资源的角度来看,popen函数的调用成本略高,而且对目标命令的调用比正常方式要慢一些我们用程序popcn
4.c来演示popen函数的行为这个程序对所有的popen示例程序的源文件的总行数进行统计,方法是用cal命令显示文件的内容并将输出通过管道传递给命令的wc-L由后者统计总行数如果是在命令行上完成这一任务,我们可以使用如下命令calpopen*.c|wc-I事实上,输入命令wc-1popen*.c非常简单而且更有效率,但我们是为了通过这个例子来演示popen函数的工作原理[epp]viewplaincopy#includeunistd.h#includcstdlib.h#includestdio.h#includestring.hintmain{FILE*read_fp=NULL;charbuffer!BUFSIZ+1]intchars_rcad=0;memsetbuffer0sizeofbuffer;read_fp=popencatpopen*.c|wc-1r;ifrcad_fp!=NULLchars_read=freadbuffersizeofcharBUFSIZread_fp;whilechars_rcad0buffer[chars_read-I]=0;printfReading:/n%s/nbuffer;chars_read=freadbuffersizeofcharBUFSIZread_fp;}pcloseread_fp;returnEXIT_SUCCESS;}returnEXIT_FAILURE;}编译程序gccpopen
4.c-opopen4运行程序./popcn4Reading:
36.108实验解析这个程序显示,shell在启动之后将popen*.c扩展为一个文件列表,列表中的文件名都是以popen开头以.c结尾,shell还处理了管道符|并将cat命令的输出传递给wc命令我们在一个单独的popen的调用中启动了shell、cat程序和wc程序,并进行了一次输出重定向而调用这些命令的程序只看到最终的输出结果
4.pipc调用在看过高级的pops函数之后,我们再来看看底层的pipe函数通过这个函数在两个程序之间传递数据不需要启动一个shell解释请求命令它同时还提供了对读写数据更多的控制pipe函数原型如下所示#includeunistd.hintpipeintfile_descriptor[2J;pipe函数的参数是一个由两个整数类型的文件描述符组成的数组指针该函数在数组中填上两个新的文件描述符后返回0如果失败则返回-1并设置em以表明失败原因在Linux使用手册中定义的错误有EMFILE进程使用的文件描述符过多ENFILE系统的文件表已满EFAULT文件描述符无效两个返回的文件描述符以一种特殊的方式连接起来写到file_descriptor[l]的所有数据都可以从file_descriptor
[0]读回来数据基于先进先出的原则FIFO进行处理,这意味着你把字节123写到filc_dcscriptorl1]从fHc_dcscriptor
[0]读取到的数据也会是
123.这与栈处理方式不同,栈采用后进先出的原则,通常简写为LIFO特别注意这里使用的是文件描述符,而不是文件流,我们必须用底层的read和write调用来访问数据,因为管道不是正规的文件,不能使用fread和fwrite下面的程序pipel.c用pipe函数创建一个管道[epp]viewplaincopy#includeunistd.h#includcstdio.h#inckidestdlib.h#includestring.hintniainintlength=0;intpipes
[2];constchardaa[]=MONKEY.D.MENG;charbuffer[BUFSIZ+1];nicmsetbuffcr0sizcofbuffcr;ifpipepipes==0length=writepipes
[1]datastrlendata;pnntfnWrote%dbytes/nlength;length=readpipes[O]bufferBUFSIZ;printfRcad%dbytcs/n\length;returnEXIT_SUCCESS;returnEXIT_FAILURE;}编译程序gccpipel.c-opipe1运行程序/pipeIWrote13bytesRead13bytes实验解析这个程序用两个文件描述符pipes[]创建管道然后它用文件描述符file_pipes[l]向管道中写数据,再从file_pipes⑼读回数据注意,管道有一些内置的缓存区,它在wriie和read调用之间保存数据如果你尝试用file_descriptor⑼写数据或用file_descriptor[l]读数据,其后果并未在文档中明确定义,所以其行为可能会非常奇怪,并且在不同的系统中可能会发生变化在我的系统中,当我这样修改后,调用的将失败,并返回-1至少能够说明这种错误比较容易发现Wrote-IbytesRead-1bytes乍看起来,这个使用管道的例子并无特别之处,它做的工作可以用一个简单的文件来完成管道的真正优势体现在,当你想在两个进程之间传递数据时,程序用fork创建新进程时,原先打开的文件描述符仍将保持打开的状态如果在原先的进程中创建一个管道,然后再调用fork创建新进程,我们即可通过管道在两个进程之间传递数据跨越fork调用的管道的程序pipe
2.c的开始部分直到fork调用为止和第一个例子非常相似[epp]viewplaincopy#includeunistd.h#inckidestdlib.h#includestdio.h#includcstring.hintmain{inlength=0;intpipes
[2];constchardata[|=MONKEY.D.MENG;charbufferlBUFSIZ+1];pid_tchild_pid;nicmsctbuffer0sizcofbuffcr;ifpipepipes==0child_pid=fork;ifchild_pid==-1{fprintfstderrForkfailure!;returnEXIT_FAILURE;}ifchild_pid==0{length=rcadpipcs[O]bufferBUFSIZ;printfRead%dbytes:%s/nlengthbuffer;returnEXIT.SUCCESS;}else{length=writepipes[l]datastrlendata;prinifWrote%dbytes/nlength;}}returnEXIT_SUCCESS;}编译程序gccpipe
2.c-opipe2运行程序
43../pipc2Wrote13bytesRead13bytes:MONKEY.D.MENG实验解析你可能发现在实际运行这个程序的时候,命令提示符在输出结果的最后一行之前出现了,为了便于阅读,我们在这里对输出结果进行了调整这个程序首先用pipe调用创建一个管道,接着用fork调用创建一个新进程如果fork调用成功,父进程就写数据到管道中,而子进程从管道中读取数据.父子进程都在只调用了一次wri【e或read之后就退IHo如果父进程在子进程之前退出,你就会在两部分输出内容之间看到shell命令提示符虽然从表面上看,这个程序和第一个使用管道的例子很相似,但实际上在这个例子中我们往前跨出了一大步,我们可以在不同的进程之间进行读写操作
5.父进程与子进程在接下来的对pipe调用的研究中,我们将学习如何在子进程中运行一个与其父进程完全不同的另外一个程序,而不仅仅运行一个相同的程序我们用exec调用来完成这一工作这里的一个难点是,通过exec进程需要知道应该访问哪个文件描述符在前面的例子中,因为子进程本身有pipes数据的•份拷贝所以这并不成为问题但经过exec调用之后,情况就不一样了,因为原先的进程已经被新的子进程所替换为解决这个问题,我们可以将文件描述符它实际上是一个整数作为一个参数传递给exec启动的程序为了演示它是如何工作,我们需要使用两个程序第一个程序是“数据生产者”,它负责创建管道和子进程,而后者是“数据消费者[cpp]viewplaincopy〃下面这个程序pipe
3.c是管道和exec函数的演示#includcunistd.h#includestdlib.h#includestdio.h#includestring.hintmainintlength=0;intpipcs
[2];constchardata口=MONKEY.D.MENG;charbuffer|BUFSIZ+IJ;pid_tchild_pid;mcmsctbuffcr0sizcofbuffcr;ifpipepipes==0child_pid=fork;ifchild_pid==-1{fprinfstderrForkfailure!;returnEXIT_FAILURE;ifchild_pid==0{sprintfbuffer%d;pipes
[0];execlpipe4pipe4bufferchar*0;returnEXIT_SUCCESS;elselength=writepipes[lLdatastrlendata;printf%d-wrote%dbytes/ngetpidlength;}}returnEXIT_SUCCESS;
37.1Ss0:03/sbin/ini
38.2S0:00[kthreadd]
39.3s0:00[migration/0]
40.4s0:00[ksoflirqd/O]
41.5s:00[watchdog/OJ
42.6s0:00[events/
0143....实验解析。