进程间通信――管道

一种在两个进程间发送消息的非常简单的方法:使用信号。我们创建通知事件,通过它引起响应,但传送的信息只限于一个信号值。

下面将介绍管道,通过它进程之间可以交换更有用的数据。


下面主要介绍的内容:

管道的定义

进程管道

管道调用

父进程和子进程

命名管道:FIFO

客户/服务器架构


什么是管道?

当从一个进程连接数据流到另一个进程时,我们使用术语管道(pipe)。通常是把一个进程的输出通过管道连接到另一个进程的输入。

大多数linux用户应该早已对将shell命令连接在一起的概念很熟悉了,这实际上就是把一个进程的输出直接传递给另一个进程的输入。对于shell命令来说,命令的连接是通过管道字符来完成的,如下所示:

cmd1|cmd2

shell负责安排两个命令的标准输入和标准输出。

cmd1的标准输入来自终端键盘

cmd1的标准输出传递给cmd2,作为它的标准输入

cmd2的标准输出连接到终端屏幕

shell所做的工作实际上是对标准输入和标准输出流进行了重新连接,使数据流从键盘输入通过两个命令最终输出到屏幕上

如何在程序中获得这样的效果,怎样用管道将多个进程连接起来,从而实现一个简单的客户/服务器系统。


进程管道

可能最简单的在两个程序之间传递数据的方法就是使用popen和pclose函数了。它们的原型如下所示:

#include<stdio.h>

FILE*popen(constchar*command,constchar*open_mode);

intpclose(FILE*stream_to_close);


popen函数

popen函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者是通过它接收数据。command字符串是要运行的程序名和相应的参数。open_mode必须是“r”或者是“w”。

如果open_mode是r,被调用程序的输出就可以被调用程序使用,调用程序利用popen函数返回的FILE*文件流指针,就可以通过常用的stdio库函数(如fread)来读取被调用程序的输出。如果open_mode是w,调用程序就可以用fwrite调用向被调用程序发送数据,而被调用程序可以在自己的标准输入上读取这些数据。

每个popen调用都必须指定“r”或“w”,在popen函数的标准实现中不支持任何其他选项。我们不能调用另一个程序并同时对它进行读写操作。如果想实现双向通信,最普遍的解决方法是使用两个管道。


pclose函数

用popen启动的进程结束时,我们可以用pclose函数关闭与之关联的文件流。pclose调用只在popen启动的进程结束后才返回。如果调用pclose时它仍在运行,pclose调用将等待该进程的结束。

pclose调用的返回值通常是在它所关闭的文件流所在进程的退出码。如果调用进程在调用pclose之前执行了一个wait语句,被调用进程的退出状态就会丢失,因为被调用进程已结束。此时pclose将返回-1并设置errno为ECHILD。


实验:读取外部程序的输出

在程序中用popen访问uname命令给出的信息。命令uname-a的作用是打印系统信息,包括计算机型号、操作系统名称、版本和发行号,以及计算机的网络名。

完成程序的初始化工作后,打开一个连接到uname命令的管道,把管道设置为可读方式并让read_fp指向该命令的输出。最后,关闭read_fp指向的管道。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
FILE * read_fp;
char buffer[BUFSIZ+1];
int chars_read;
memset(buffer,'\0',sizeof(buffer));
read_fp=popen("uname -a","r");
if(read_fp!=NULL)
{
chars_read=fread(buffer,sizeof(char),BUFSIZ,read_fp);
if(chars_read>0)
{
printf("Output was:-\n%s\n",buffer);
}
pclose(read_fp);
exit(0);
}
exit(1);
}

BUFSIZ这个常量在stdio.h中用#define语句定义的,大小是8192,你可以打印出来看看。


编译运行:

[root@localhost C_test]# gcc -o popen1 popen1.c
[root@localhost C_test]# ./popen1
Output was:-
Linux localhost.localdomain 2.6.32-358.14.1.el6.i686 #1 SMP Tue Jul 16 21:12:30 UTC 2013 i686 i686 i386 GNU/Linux



将输出送往popen

看过捕获外部程序输出的例子后,我们再来看一个将输出发送到外部程序的示例程序popen2.c,它将数据通过管道送往另一个程序。


实验:将输出送往外部程序

和上面示例程序不同的是这个程序是将数据写入管道,而不是从管道中读取。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
FILE * write_fp;
char buffer[BUFSIZ];
sprintf(buffer,"Once upon a time,there was ...");
write_fp=popen("od -c","w");
if(write_fp!=NULL)
{
fwrite(buffer,sizeof(char),strlen(buffer),write_fp);
pclose(write_fp);
exit(0);
}
exit(1);
}

编译运行:

[root@localhost C_test]# gcc -o popen2 popen2.c
[root@localhost C_test]# ./popen2
0000000   O   n   c   e       u   p   o   n       a       t   i   m   e
0000020   ,   t   h   e   r   e       w   a   s       .   .   .
0000036

在命令行上我们可以用下面的命令得到同样的输出结果

[root@localhost C_test]# echo "Once upon a time,there was ..." | od -c



传递更多的数据

我们目前所使用的机制都只是将所有数据通过一次fread或fwrite调用发送或接收。有时,我们可能希望能以块方式发送数据,或者我们根本就不知道输出数据的长度。为了避免定义一个非常大的缓冲区,我们可以用多个fread或fwrite调用来将数据分为几部分处理。


实验:通过管道读取大量数据

这个程序中,我们从被调用的进程psax中读取数据。该进程输出的数据有多少事先无法知道,所以必须对管道进行多次读取。


#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
FILE * read_fp;
char buffer[BUFSIZ+1];
int chars_read;
memset(buffer,'\0',sizeof(buffer));
read_fp=popen("ps ax","r");
if(read_fp!=NULL)
{
chars_read=fread(buffer,sizeof(char),BUFSIZ,read_fp);
while(chars_read>0)
{
printf("Reading %d:-\n %s\n",BUFSIZ,buffer);
chars_read=fread(buffer,sizeof(char),BUFSIZ,read_fp);
}
pclose(read_fp);
exit(0);
}
return 1;
}


编译运行:

[root@localhost C_test]# gcc -o popen3 popen3.c
[root@localhost C_test]# ./popen3
Reading 8192:-
PID TTY      STAT   TIME COMMAND
1 ?        Ss     0:03 /sbin/init
2 ?        S      0:00 [kthreadd]
3 ?        S      0:00 [migration/0]
4 ?        S      0:02 [ksoftirqd/0]
5 ?        S      0:00 [migration/0]
6 ?        S      0:33 [watchdog/0]
7 ?        S      0:24 [events/0]
8 ?        S      0:00 [cgroup]
9 ?        S      0:00 [khelper]
。。。省略



实验解析:

在本例中,你可能不会看到Reading:-信息的第二次出现。如果BUFSIZ的值不超过ps命令输出的长度,这种情况就会发生。为了测试程序在读取多个输出数据块时能够正常工作,你可以尝试每次读取少于BUFSIZ个字符(比如BUFSIZE/20)。



如何实现popen

请求popen调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它。针对每个popen调用,不仅要启动一个被请求的程序,还要启动一个shell,即每个popen调用将多启动两个进程。


pipe调用

看过高级的popen函数之后,再来看看底层的pipe函数。通过这个函数在两个程序之间传递数据不需要启动一个shell来解释请求的命令。它同时还提供了对读写数据的更多控制。

pipe函数的原型如下所示:

#include<unistd.h>

intpipe(intfile_descriptor[2]);

pipe函数的参数是一个由两个整数类型的文件描述符组成的数组的指针。该函数在数组中填上两个新的文件描述符后返回0,如果失败则返回-1并设置errno来表明失败的原因。在linux手册页中定义了下面一些错误。

EMFILE:进程使用的文件描述符过多。

ENFILE:系统的文件表已满。

EFAULT:文件描述符无效。

两个返回的文件描述符以一种特殊的方式连接起来。写到file_descriptor[1]的所有数据都可以从file_descriptor[0]读回来。数据基于先进先出的原则进行处理,这意味着如果你把字节1,2,3写到file_descriptor[1],从file_descriptor[0]读取到的数据也会是1,2,3。

下面的程序pipe1.c用pipe函数来创建一个管道。

实验:pipe函数

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
int data_processed;
int file_pipes[2];
char some_data[]="123";
char buffer[BUFSIZ+1];
memset(buffer,'\0',sizeof(buffer));
if(pipe(file_pipes)==0)
{
data_processed=write(file_pipes[1],some_data,strlen(some_data));
printf("write %d bytes\n",data_processed);
data_processed=read(file_pipes[0],buffer,BUFSIZ);
printf("read %d bytes : %s\n",data_processed,buffer);
exit(0);
}
exit(1);
}


编译运行:

[root@localhost C_test]# gcc -o pipe1 pipe1.c
[root@localhost C_test]# ./pipe1
write 3 bytes
read 3 bytes : 123

管道的真正优势体现在,当你想在两个进程之间传递数据的时候。当程序用fork调用创建新进程时,原先打开的文件描述符仍将保持打开状态。如果在原先的进程中创建一个管道,然后再调用fork创建新进程,我们即可通过管道在两个进程之间传递数据。


实验:跨越fork调用的管道

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
int data_processed;
int file_pipe[2];
char some_data[]="yao";
char buffer[BUFSIZ];
pid_t pid;
memset(buffer,'\0',BUFSIZ);
if(pipe(file_pipe)==0)
{
int status;
pid=fork();
if(pid==-1)
{
perror("fork failed");
exit(1);
}
if(pid==0)
{
data_processed=read(file_pipe[0],buffer,BUFSIZ);
printf("Read %d bytes:%s\n",data_processed,buffer);
exit(0);
}
else {
data_processed=write(file_pipe[1],some_data,strlen(some_data));
printf("write %d bytes\n",data_processed);
wait(&status);
if(WIFEXITED(status))
{
printf("child process is finished with exit code:%d",WEXITSTATUS(status));
}
}
}
exit(0);
}


编译运行:

[root@localhost C_test]# gcc -o pipe2 pipe2.c
[root@localhost C_test]# ./pipe2
write 3 bytes
Read 3 bytes:yao
child process is finished with exit code:0


实验解析:

这个程序首先用pipe调用创建一个管道,接着用fork调用创建一个新进程。如果fork调用成功,父进程就写数据到管道中,而子进程从管道中读取数据。父子进程都在只调用了一次write或read之后就退出。



父进程和子进程

如何在子进程中运行一个与其父进程完全不同的另外一个程序,而不是仅仅运行一个相同程序。我们用exec调用来完成这一工作。这里的一个难点是通过exec调用的进程需要知道应该访问哪个文件描述符。在前面的例子中,因为子进程本身有file_pipes数据的一份副本,所以不成问题。但经过exec调用后,情况就不一样了,因为原先的进程已经被新的子进程替换了。为了解决这个问题,我们可以将文件描述符作为一个参数传递给用exec启动的程序。

我们需要使用两个程序。第一个程序是数据生产者,它负责创建管道和启动子进程,而后者是数据消费者。


实验:管道和exec函数

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
int data_processed;
int file_pipe[2];
char some_data[]="yao";
char buffer[BUFSIZ];
pid_t pid;
memset(buffer,'\0',BUFSIZ);
if(pipe(file_pipe)==0)
{
int status;
pid=fork();
if(pid==-1)
{
perror("fork failed");
exit(1);
}
if(pid==0)
{
sprintf(buffer,"%d",file_pipe[0]);
execl("./pipe4","pipe4",buffer,NULL);
exit(1);
}
else {
int status;
data_processed=write(file_pipe[1],some_data,strlen(some_data));
printf("%d - write %d bytes\n",getpid(),data_processed);
wait(&status);
if(WIFEXITED(status))
{
printf("child thread is finished.\n");
}
}
}
exit(0);
}


数据消费者程序pipe4.c负责读取数据

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
int data_processed;
char buffer[BUFSIZ];
int file_descriptor;
memset(buffer,'\0',BUFSIZ);
sscanf(argv[1],"%d",&file_descriptor);
data_processed=read(file_descriptor,buffer,BUFSIZ);
printf("%d -read %d bytes:%s\n",getpid(),data_processed,buffer);
return 0;
}


编译运行:

[root@localhost C_test]# gcc -o pipe3 pipe3.c
[root@localhost C_test]# ./pipe3
27623 - write 3 bytes
27624 -read 3 bytes:yao
child thread is finished.



实验解析:

pipe3程序的开始部分和前面的例子一样,用pipe调用创建一个管道,然后用fork调用创建一个新进程。接下来,它用sprintf把读取管道数据的文件描述符保存到一个缓冲区,该缓冲区中的内容将构成pipe4程序的一个参数。


管道关闭后的读操作

我们一直采取的是让读进程读取一些数据然后直接退出的方式,并假设linux会把清理文件当作是在进程结束时应该做的一部分。

但大多数从标准输入读取数据的程序采用的却是与我们到目前为止见到的例子非常不同的另外一种做法。通常它们并不知道有多少数据需要读取,所以往往采用循环的方法,读取数据――处理数据――读取更多的数据,直到没有数据可读为止。

当没有数据可读时,read调用通常会阻塞,即它将暂停进程来等待直到有数据到达为止。如果管道的另一端已被关闭,没有进程打开这个管道并向它写数据了,这时read调用就会阻塞。但这样的阻塞不是很有用,因此对一个已关闭写数据的管道做read调用将返回0而不是阻塞。这就使读进程能够像检测文件结束一样,对管道进行检测并作出相应的动作。注意,这与读取一个无效的文件描述符不同,read把无效的文件描述符看作一个错误并返回-1。

如果跨越fork调用使用管道,就会有两个不同的文件描述符可以用于向管道写数据,一个在父进程中,一个在子进程中。只有把父进程中的针对管道的写文件描述符都关闭,管道才会被认为是关闭了,对管道的read调用才会失败。


把管道用作标准输入和标准输出

用管道连接两个进程的更简洁的方法。我们把其中一个管道文件描述符设置为一个已知值,一般是标准输入0或标准输出1。在父进程中做这个设置有点复杂,但它使得子进程的编写变得非常简单。

这样做的最大好处是我们可以调用标准程序,即那些不需要以文件描述符为参数的程序。为了完成这个工作,我们需要使用dup函数。dup函数有两个紧密关联的版本,它们的原型如下所示:

#include<unistd.h>

intdup(intfile_descriptor);

intdup2(intfile_descriptor_one,intfile_descriptor_two);

dup调用的目的是打开一个新的文件描述符,这与open调用有点类似。不同的是,dup调用创建的新文件描述符与作为参数的那个已有文件描述符指向同一个文件(或管道)。对于dup函数来说,新的文件描述符总是取最小的可用值。而对于dup2函数来说,它所创建的新文件描述符或者与参数file_descriptor_two相同,或者是第一个大于该参数的可用值。

标准输入的文件描述符总是0,而dup返回的新文件描述符又总是使用最小可用的数字。因此,如果我们首先关闭文件描述符0然后调用dup,那么新的文件描述符就将是数字0。因为新的文件描述符是复制一个已有的文件描述符,所以标准输入就会改为指向一个我们传递dup函数的文件描述符所对应的文件或管道。我们创建了两个文件描述符,它们指向同一个文件或管道,而且其中之一是标准输入。


用close和dup函数对文件描述符进行处理

实验:管道和dup函数

我们将把子程序的stdin文件描述符替换为我们创建的管道的读取端。还将对文件描述符做一些清理,使得子程序可以正确检测到管道中数据的结束。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
int data_processed;
int file_pipes[2];
char some_data[] = "yao";
pid_t pid;
if (pipe(file_pipes) == 0) {
pid = fork();
if (pid == -1) {
perror("fork failed");
exit(1);
}
if (pid == 0) {
close(0);
dup(file_pipes[0]);
close(file_pipes[0]);
close(file_pipes[1]);
execlp("od", "od", "-c", NULL);
exit(1);
}
else {
int status;
close(file_pipes[0]);
data_processed = write(file_pipes[1], some_data, strlen(some_data));
close(file_pipes[1]);
printf("%d - wrote %d bytes \n", getpid(), data_processed);
wait(&status);
printf("child process is finished\n");
}
}
exit(0);
}

编译运行:

[root@localhost C_test]# gcc -o pipe5 pipe5.c
[root@localhost C_test]# ./pipe5
29412 - wrote 3 bytes
0000000   y   a   o
0000003
child process is finished


实验解析:

这个程序创建一个管道,然后通过fork创建一个子进程。此时,父子进程都有可以访问管道的文件描述符,一个用于读数据,一个用于写数据,所以共有4个打开的文件描述符。

我们首先看子进程。子进程先用close(0)关闭它的标准输入,然后调用dup(file_pipes[0])把与管道的读取端关联的文件描述符复制为文件描述符0,即标准输入。接下来,子进程关闭原先的用来从管道读取数据的文件描述符file_pipes[0]。因为子进程不会向管道写数据,所以它把与管道关联的写操作文件描述符file_pipes[1]也关闭了。现在,它只有一个与管道关联的文件描述符,即文件描述符0,它的标准输入。

接下来,子进程就可以用exec来启动任何从标准输入读取数据的程序了。在本例中,我们使用的是od命令。od命令等待数据的到来,就好像在等待来自用户终端的输入一样。事实上,如果没有明确使用检测这两者之间不同的特殊代码,它并不知道是来自一个管道,而不是来自一个终端。

父进程首先关闭管道的读取端file_pipes[0],因为它不会从管道读取数据。接着它向管道写入数据。当所有数据都写完后,父进程关闭管道的写入端并退出。因为现在已没有打开的文件描述符可以向管道写数据了,od程序读取写到管道中的3个字节数据后,后续的读操作将返回0字节,表示已到文件尾。当读操作返回0时,od程序就退出运行。



命名管道:FIFO

我们还只能在相关的程序之间传递数据,即这些程序是由一个共同的祖先进程启动的。但如果我们想在不相关的进程之间交换数据,还不是很方便。

我们可以用FIFO文件来完成这项工作,它通常也被称为命名管道(namedpipe)。命名管道是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但它的行为却和我们已经见过的没有名字的管道类似。

我们可以在命令行上创建命名管道,也可以在程序中创建它。过去,命令行上用来创建命名管道的程序是mknod。linux系统非常友好,它同时支持mknod和mkfifo。

在程序中,我们可以使用两个不同的函数调用,如下所示:

#include<sys/types.h>

#include<sys/stat.h>

intmkfifo(constchar*filename,mode_tmode);

intmknod(constchar*filename,mode_tmode|S_IFIFO,(dev_t)0);

与mknod命令一样,我们可以用mknod函数建立许多特殊类型的文件。要想通过这个函数创建一个命名管道,唯一具有可移植性的方法是使用一个dev_t类型的值0,并将文件访问模式与S_IFIFO按位或。


实验:创建命名管道

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char **argv) {
int res=mkfifo("/tmp/mymkfifo",0777);
if(res==0)
{
printf("FIFO created\n");
}
exit(0);
}


编译运行:

[root@localhost C_test]# gcc -o fifo1 fifo1.c
[root@localhost C_test]# ./fifo1
[root@localhost C_test]# ll /tmp/myfifo
prwxr-xr-x. 1 root root 0 Aug  5 18:08 /tmp/myfifo

输出结果中的第一个字符为p,表示这是一个管道。


实验解析:

这个程序用mkfifo函数创建一个特殊的文件。虽然我们要求的文件模式是0777,但它被用户掩码umask=022给改变了,这与普通文件的创建是一样的,所以文件的最终模式是755.

我们可以像删除一个普通文件那样用rm命令删除FIFO文件,或者也可以在程序中用unlink系统调用来删除它。


访问FIFO文件

命名管道的一个非常有用的特点是:由于它们出现在文件系统中,所以它们可以像平常的文件名一样在命令中使用。在把创建的FIFO文件用在程序设计中之前,我们先通过普通的文件命令来观察FIFO文件的行为。


实验:访问FIFO文件

1首先,我们来尝试读这个空的FIFO文件

$cat</tmp/myfifo

2现在,尝试向FIFO写数据。你必须用另一个终端来执行下面的命令,因为第一个命令现在被挂起以等待数据出现在FIFO中。

$echo"HelloWorld">/tmp/myfifo

你将看到cat命令产生输出。如果不向FIFO发送任何数据,cat命令将一直挂起,直到你中断它。

3我们可以将第一个命令放在后台执行,这样就可以一次执行两个命令。

[root@localhost C_test]# cat < /tmp/myfifo &
[3] 1618
[root@localhost C_test]# echo "Hello World" > /tmp/myfifo
[root@localhost C_test]# Hello World


实验解析:

因为FIFO中没有数据,所以cat和echo程序都阻塞了,cat等待数据的到来,而echo等待其他进程读取数据。

在上面实验中,cat进程一开是就在后台被阻塞了,当echo向它提供了一些数据后,cat命令读取这些数据把它们打印到标准输出上,然后cat程序退出,不再等待更多的数据。它没有阻塞诶是因为第二个命令将数据放入FIFO后,管道将被关闭,所以cat程序中的read调用返回0字节,表示已经到达文件尾。

与通过pipe调用创建管道不同,FIFO是以命名文件的形式存在,而不是打开的文件描述符,所以在对它进行读写操作之前要先打开它。FIFO也用open和close函数打开和关闭。

1使用open打开FIFO文件

打开FIFO的一个主要限制是,程序不能以O_RDWR模式打开FIFO文件进行读写操作,这样做的后果并未明确定义。但这个限制是有道理的,因为我们通常使用FIFO只是为了单向传递数据,所以没必要使用O_RDWR模式。如果一个管道以读/写方式打开,进程就会从这个管道读回它自己的输出。

如果确实需要在程序之间双向传递数据,最好使用一对FIFO或管道,一个方向使用一个,或者采用先关闭再重新打开FIFO的方法来明确地改变数据流的方向。

打开FIFO文件和打开普通文件的另一点区别是,对open_flag的O_NONBLOCK选项的用法。使用这个选项不仅改变open调用的处理方式,还会改变对这次open调用返回的文件描述符进行的读写请求的处理方式。

O_RDONLYO_WRONLYO_RDWRO_NONBLOCK标志共有4钟合法的组合方式


open(constchar*path,O_RDONLY);

在这种情况下,open调用将阻塞,除非有一个进程以写方式打开同一个FIFO,否则它不会返回。这与前面第一个cat命令的例子类似。


open(constchar*path,O_RDONLY|O_NONBLOCK);

即使没有其他进程以写方式打开FIFO,这个open调用也将成功并立刻返回。


open(constchar*path,O_WRONLY);

在这种情况下,open调用将阻塞,直到有一个进程以读方式打开同一个FIFO为止。


open(constchar*path,O_WRONLY|O_NONBLOCK);

这个函数调用总是立刻返回,但如果没有进程以读方式打开FIFO文件,open调用将返回一个错误-1并且FIFO也不会被打开。如果确实有一个进程以读方式打开FIFO文件,那么我们就可以通过它返回的文件描述符对这个FIFO文件进行写操作。


注意:O_NONBLOCK分别搭配O_RDONLY和O_WRONLY在效果上的不同,如果没有进程以读方式打开管道,非阻塞写方式的open调用将失败,但非阻塞读方式的open调用总是成功。close调用的行为并不受O_NONBLOCK标志的影响。


实验:打开FIFO文件

通过使用带O_NONBLOCK标志的open调用的行为来同步两个进程。我们在这里并没有选择使用多个示例程序的做法,而是只使用一个测试程序fifo2.c,通过给该程序传递不同的参数的方法来观察FIFO的行为。


#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FIFO_NAME "/tmp/myfifo"
int main(int argc,char **argv)
{
int res;
int open_mode=0;
int i;
if(argc<2)
{
perror("Usage Error!");
exit(1);
}
for(i=1;i<argc;i++)
{
if(strncasecmp(*++argv,"O_RDONLY",8)==0)
{
open_mode |=O_RDONLY;
}
if(strncasecmp(*argv,"O_WRONLY",8)==0)
{
open_mode |=O_WRONLY;
}
if(strncasecmp(*argv,"O_NONBLOCK",10)==0)
{
open_mode |=O_NONBLOCK;
}
}
if(access(FIFO_NAME,F_OK)==-1)
{
res=mkfifo(FIFO_NAME,0777);
if(res!=0)
{
perror("mkfifo failed");
exit(1);
}
}
printf("Process %d opens FIFO\n",getpid());
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(0);
}


不带O_NONBLOCK标志的O_RDONLY和O_WRONLY

[root@localhost C_test]# ./fifo2 O_RDONLY &
[3] 3662
[root@localhost C_test]# Process 3662 opens FIFO
[root@localhost C_test]# ./fifo2 O_WRONLY
Process 3668 opens FIFO
Process 3662 result 3
Process 3668 result 3
Process 3662 finished
Process 3668 finished
[3]+  Done                    ./fifo2 O_RDONLY

这可能是命名管道最常见的用法了。它允许先启动读进程,并在open调用中等待,当第二个程序打开FIFO文件时,两个程序继续运行。注意:读进程和写进程在open调用处取得同步。

当一个linux进程被阻塞时,它并不消耗CPU资源,所以这种进程的同步方式对CPU来说是非常有效率的。


带O_NONBLOCK标志的O_RDONLY和不带标志的O_WRONLY

这次,读进程执行open调用并立刻继续执行,即使没有写进程的存在。随后写进程开始执行,它也在执行open调用后立刻继续执行,但这次是因为FIFO已被读进程打开。


[root@localhostC_test]#./fifo2O_RDONLYO_NONBLOCK&
[3]4326
[root@localhostC_test]#Process4326opensFIFO
Process4326result3
Process4326finished


[root@localhostC_test]#./fifo2O_WRONLY
Process4328opensFIFO
Process4328result3
Process4328finished

这里开两个终端,不然看不出效果!


对FIFO进行读写操作

使用O_NONBLOCK模式会影响到FIFO的read和write调用。

对一个空的、阻塞的FIFO(即没有用O_NONBLOCK标志打开)的read调用将等待,直到有数据可以读时才继续执行。与此相反,对一个空的、非阻塞的FIFO的read调用将立刻返回0字节。

对一个完全阻塞FIFO的write调用将等待,直到数据可以被写入时才继续执行。如果FIFO不能接收所有写入的数据,它将按下面的规则执行。

1如果请求写入的数据的长度小于等于PIPE_BUF字节,调用失败,数据不能写入。

2如果请求写入的数据的长度大于PIPE_BUF字节,将写入部分数据,返回实际写入的字节数,返回值也可能是0。

FIFO的长度是需要考虑一个很重要的因素。系统对任一时刻在一个FIFO中可以存在的数据长度是有限制的。它由#definePIPE_BUF语句定义,通常可以在头文件limit.h中找到它。在linux和许多其他类unix系统中,它的值通常是4096字节,但在某些系统中它可能会小到512字节。系统规定:在一个以O_WRONLY方式(即阻塞方式)打开的FIFO中,如果写入的数据长度小于PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。

虽然对只有一个FIFO写进程和一个FIFO读进程的简单情况来说,这个限制并不是非常重要,但只使用一个FIFO并允许多个不同的程序向一个FIFO读进程发送请求的情况是很常见的。如果几个不同的程序尝试同时向FIFO写数据,能否保证来自不同程序的数据块不相互交错就非常关键了。也就是说每个写操作都必须是原子化的。

如果你能保证所有的写请求是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据块不会交错在一起。通常每次通过FIFO传递的数据块长度限制为PIPE_BUF字节是个好方法,除非你只使用一个写进程和一个读进程。


实验:使用FIFO实现进程间通信


生产者程序fifo3.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#define FIFO_NAME "/tmp/myfifo"
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024*1024*10)
int main(int argc, char **argv) {
int pipe_fd;
int res;
int open_mode = O_WRONLY;
int bytes_sent = 0;
char buffer[BUFFER_SIZE];
if (access(FIFO_NAME, F_OK)== -1) {
res = mkfifo(FIFO_NAME, open_mode);
if (res != 0) {
perror("mkfifo failed");
exit(1);
}
}
printf("Process %d opens FIFO O_WRONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d result %d\n", getpid(), pipe_fd);
if (pipe_fd != -1) {
while (bytes_sent < TEN_MEG) {
res = write(pipe_fd, buffer, BUFFER_SIZE);
if (res == -1) {
perror("Write Error");
exit(1);
}
bytes_sent += res;
}
close(pipe_fd);
} else {
exit(1);
}
printf("Process %d finished \n", getpid());
exit(0);
}


消费者程序fifo4.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <string.h>
#define FIFO_NAME "/tmp/myfifo"
#define BUFFER_SIZE PIPE_BUF
int main(int argc, char **argv) {
int pipe_fd;
int res;
int open_mode=O_RDONLY;
char buffer[BUFFER_SIZE];
int bytes_read=0;
memset(buffer,'\0',BUFFER_SIZE);
printf("Process %d opens FIFO O_RDONLY\n",getpid());
pipe_fd=open(FIFO_NAME,open_mode);
printf("Process %d result %d\n",getpid(),pipe_fd);
if(pipe_fd!=-1)
{
do {
res=read(pipe_fd,buffer,BUFFER_SIZE);
bytes_read+=res;
} while (res>0);
close(pipe_fd);
}
else {
exit(1);
}
printf("Process %d finished,%d bytes read\n",getpid(),bytes_read);
exit(0);
}

我们在运行这两个程序的同时,用time命令对读进程进行计时。输出结果如下所示

[root@localhost C_test]# ./fifo3 &
[4] 6432
[root@localhost C_test]# Process 6432 opens FIFO O_WRONLY
time ./fifo4
Process 6434 opens FIFO O_RDONLY
Process 6432 result 3
Process 6434 result 3
Process 6432 finished
Process 6434 finished,10485760 bytes read
[4]+  Done                    ./fifo3
real    0m0.048s
user    0m0.004s
sys 0m0.026s


实验解析:

两个程序使用的都是阻塞模式的FIFO。我们首先启动fifo3,它将阻塞以等待读进程打开这个FIFO。fifo4启动以后,写进程解除阻塞并开始向管道写数据。同时,读进程也开始从管道中读取数据。

linux会安排好这两个进程之间的调度,使他们在可以运行的时候运行,在不能运行的时候阻塞。因此,写进程将在管道满时阻塞,读进程将在管道空时阻塞。说明管道在程序之间传递数据是很有效率的。





你可能感兴趣的:(process)