您的位置:首页 > 运维架构 > Linux

UNIX/Linux进程间通信入门(1):管道

2010-10-15 15:26 309 查看
UNIX/Linux进程间通信入门(1):管道

重读了《Linux程序设计(第三版)》,对进程间通信做个总结。本文将介绍Unix/Linux环境下的使用管道进行的进程间通信,以主从以下几个方面进行介绍:
1. 管道的定义
2. 进程管道
3. 管道调用
4. 父进程和子进程
5. 命名管道:FIFO
6. 客户/服务器架构

1.什么是管道
当从一个进程连接数据流到另一个进程时,我们使用术语管道(pipe)。我们通常是把一个进程的输出通过管道连接到另一个进程的输入。大多数Linux用户都使用过shell,不可避免地会使用管道进行一些数据的分析和处理,命令格式如下:
cmd1 | cmd2
shell负责安排两个命令的标准输入和标准输出,这样:
1. cmd1的标准输入来自终端键盘
2. cmd1的标准输出传递给cmd2,作为它的标准输入
3. cmd2的标准输出连接到终端屏幕
shell所做的工作从效果上看是对标准输出和标准输出流进行了重新连接,使数据流从键盘输出通过两个命令最终输出到屏幕上。
当然这样的是利用shell的命令行形式,我们完成了Linux环境下的进程间管道通信,如何用C语言实现,我们接下来将会看到。

2.进程管道
最简单的两个程序之间的数据传递的方法就是使用popen和pclose函数。它们的原型如下:
#include <stdio.h>
FILE *popen(const char *command, const char *open_mode);

一、popen函数
popen函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command字符串要运行的程序名和相应的参数。open_mode必须是”r”或者”w”。
如果open_mode是”r”,被调用程序的输出就可以被调用程序使用,调用程序利用popen函数返回的FILE*文件流指针,就可以通过常用的stdio库函数(如fread)来读取被调用程序的输出。如果open_mode是”w”,调用程序就可以用fwrite调用向被调用命令发送数据,而被调用程序可以在自己的标准输入上读取这些数据。被调用的程序通常不会意识到自己正在从另一个进程读取数据,它只是简单在标准输入上读取数据,然后做出相应的操作。
每个popen调用都必须指定”r”或”w”,在popen函数的标准实现中不支持任何其他选项。这就意味着,我们不能调用另一个程序并同时对它进行读写操作。popen函数在失败时返回一个空指针。如果你想通过管道实现双向通信,最普通的解决方法是使用两个管道,每个管道负责一个方向的数据流。

二、pclose函数
用popen启动的进程结束时,我们可以用pclose函数关闭与之关联的文件流。pclose调用只在popen启动的进程结束后才返回。如果调用pclose时它仍在运行,pclose调用将等待该进程结束。
pclose调用的返回值通常是它所关闭的文件流所在的进程的退出码。如果调用进程在调用pclose之前执行一个wait语句,被调用进程的退出状态就会丢失,pclose将返回-1并设置errno为EHILD。
现在来看一个简单的popen和pclose示例程序popen1.c。我们将在程序中用popen访问uname命令给出的信息。命令uname –a的作用是打印系统信息,包括计算机型号、操作系统名字、版本和发行号,以及计算机的网络名。
完成程序的初始化工作后,打开一个连接到uname命令的管道,把管道设置为可读方式并让read_fp指向该命令的输出。最后,关闭read_fp指向的管道。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
FILE *read_fp = NULL;
char buffer[BUFSIZ + 1];
int chars_read = 0;

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:/n%s/n", buffer);
}

return EXIT_SUCCESS;
}

return EXIT_FAILURE;
}
编译程序:
gcc popen1.c –o popen1
运行程序:
./popen1
Output:
Linux localhost.localdomain 2.6.25-14.fc9.i686 #1 SMP Thu May 1 06:28:41 EDT 2008 i686 i686 i386 GNU/Linux


实验解析:

这个程序用popen调用启动带有-a选项的uname命令。然后用返回的文件流读取最多BUFSIZ(这个常量是在stdio.h中用#define语句定义的)个字符的数据并将它们打印出来显示在屏幕上。因为我们是在程序内部捕获uname命令的输出,所以即可处理它了。

3.将输出送往popen
看过捕获外部程序输出的例子后,我们再来看一个将输出发送到外部程序示例程序popen2.c,它将数据通过管道送往另一个程序。我们在这里使用的是od(八进制输出)命令。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
FILE *write_fp = NULL;
char buffer[BUFSIZ + 1];

sprintf(buffer, "MONKEY.D.MENG");
write_fp = popen("od -c", "w");
if (write_fp != NULL)
{
fwrite(buffer, sizeof(char), strlen(buffer), write_fp);
pclose(write_fp);

return EXIT_SUCCESS;
}

return EXIT_FAILURE;
}
编译程序:
gcc popen2.c –o popen2
运行程序:
./popen2
0000000   M   O   N   K   E   Y   .   D   .   M   E   N   G
0000015


实验解析:
程序使用带有参数”w”的popen启动od –c命令,这样就可以向该命令发送数据了。然后它发送一个字符串给od –c命令,该命令接收并处理它,最后把处理结果打印到自己的标准输出上。
在命令行上,我们可以用下面的命令得到同样的输出结果:
echo “MONKEY.D.MENG” | od –c

一、传递更多的数据
我们迄今为止所使用的机制是简单的将所有数据通过一次fread或fwrite调用来发送或接收。有时,我们可能希望以块方式发送数据,或者我们根本就不知道输出数据的长度。为了避免定义一个非常大的缓冲区,我们可以用多个fread或fwrite调用来按部分处理数据。
下面这个程序popen3.c通过管道读取大量数据。在这个程序中,我们从被调用的进程”ps -ax”中读取数据。该进程输出的数据有多少事先无法知道,所以我们必须对管道进行多次读取。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
FILE *read_fp = NULL;
char buffer[BUFSIZ + 1];
int chars_read = 0;

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)
{
buffer[chars_read - 1] = 0;
printf("Reading:/n%s/n", buffer);
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
}
pclose(read_fp);

return EXIT_SUCCESS;
}

return EXIT_FAILURE;
}
编译程序:
gcc popen3.c –o popen3
运行程序:
./popen3
Reading:
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:00 [ksoftirqd/0]
5 ?        S<     0:00 [watchdog/0]
6 ?        S<     0:00 [events/0]
…

实验解析:
这个程序调用popen函数时使用了”r”参数,这与popen1.c程序的做法一样。这次,它连续地从文件流中读取数据,直到没有数据可读为止。注意,虽然ps命令的执行要花费一些时间,但Linux会安排好进程间的调度,让两个程序在可以运行时继续运行。如果读进程popen3没有数据可读,它将被挂起直到有数据到达。如果写进程ps产生的输出超过了可用缓冲区的长度,它也会被挂起直到读进程读取了一些数据。

二、如何实现popen
请求popen调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它。这有两个效果,一个好,一个不太好。
在Linux中,所有的参数扩展都是由shell完成的。所以在启动程序之前先启动shell来分析命令字符串,就可以使各种shell扩展在程序启动之前就全部完成。这个功能非常有用,它允许我们通过popen启动非常复杂的shell命令。而其他一些创建进程的函数(如execl)调用起来就复杂得多,因为调用进程必须自己去完成shell扩展。
使用shell的一个不太好的影响是,针对每个popen调用,不仅要启动一个被请求的程序,还要启动一个shell,即每个popen调用将多启动两个进程,从节省系统资源的角度来看,popen函数的调用成本略高,而且对目标命令的调用比正常方式要慢一些。
我们用程序popen4.c来演示popen函数的行为。这个程序对所有的popen示例程序的源文件的总行数进行统计,方法是用cat命令显示文件的内容并将输出通过管道传递给命令的wc –l,由后者统计总行数。如果是在命令行上完成这一任务,我们可以使用如下命令:
cat popen*.c | wc –l
事实上,输入命令wc –l popen*.c非常简单而且更有效率,但我们是为了通过这个例子来演示popen函数的工作原理。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
FILE *read_fp = NULL;
char buffer[BUFSIZ + 1];
int chars_read = 0;

memset(buffer, 0, sizeof(buffer));
read_fp = popen("cat popen*.c | wc -l", "r");

if (read_fp != NULL)
{
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
while (chars_read > 0)
{
buffer[chars_read - 1] = 0;
printf("Reading:/n%s/n", buffer);
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
}
pclose(read_fp);

return EXIT_SUCCESS;
}

return EXIT_FAILURE;
}
编译程序:
gcc popen4.c –o popen4
运行程序:
./popen4
Reading:
108

实验解析:
这个程序显示,shell在启动之后将popen*.c扩展为一个文件列表,列表中的文件名都是以popen开头,以.c结尾,shell还处理了管道符 | 并将cat命令的输出传递给wc命令。我们在一个单独的popen的调用中启动了shell、cat程序和wc程序,并进行了一次输出重定向。而调用这些命令的程序只看到最终的输出结果。

4.pipe调用
在看过高级的popen函数之后,我们再来看看底层的pipe函数。通过这个函数在两个程序之间传递数据不需要启动一个shell解释请求命令。它同时还提供了对读写数据更多的控制。
pipe函数原型如下所示:
#include <unistd.h>
int pipe(int file_descriptor[2]);

pipe函数的参数是一个由两个整数类型的文件描述符组成的数组指针。该函数在数组中填上两个新的文件描述符后返回0,如果失败则返回-1并设置errno以表明失败原因。在Linux使用手册中定义的错误有:
EMFILE:进程使用的文件描述符过多。
ENFILE:系统的文件表已满。
EFAULT:文件描述符无效。
两个返回的文件描述符以一种特殊的方式连接起来。写到file_descriptor[1]的所有数据都可以从file_descriptor[0]读回来。数据基于先进先出的原则(FIFO)进行处理,这意味着你把字节1,2,3写到file_descriptor[1],从file_descriptor[0]读取到的数据也会是1,2,3.这与栈处理方式不同,栈采用后进先出的原则,通常简写为LIFO。
特别注意:这里使用的是文件描述符,而不是文件流,我们必须用底层的read和write调用来访问数据,因为管道不是正规的文件,不能使用fread和fwrite。

下面的程序pipe1.c用pipe函数创建一个管道。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
int length = 0;
int pipes[2];
const char data[] = "MONKEY.D.MENG";
char buffer[BUFSIZ + 1];

memset(buffer, 0, sizeof(buffer));

if (pipe(pipes) == 0)
{
length = write(pipes[1], data, strlen(data));
printf("Wrote %d bytes/n", length);

length = read(pipes[0], buffer, BUFSIZ);
printf("Read %d bytes/n", length);

return EXIT_SUCCESS;
}

return EXIT_FAILURE;
}
编译程序:
gcc pipe1.c –o pipe1
运行程序:
./pipe1
Wrote 13 bytes
Read 13 bytes

实验解析:
这个程序用两个文件描述符pipes[]创建管道。然后它用文件描述符file_pipes[1]向管道中写数据,再从file_pipes[0]读回数据。注意,管道有一些内置的缓存区,它在write和read调用之间保存数据。
如果你尝试用file_descriptor[0]写数据或用file_descriptor[1]读数据,其后果并未在文档中明确定义,所以其行为可能会非常奇怪,并且在不同的系统中可能会发生变化。在我的系统中,当我这样修改后,调用的将失败,并返回-1,至少能够说明这种错误比较容易发现。
Wrote -1 bytes
Read -1 bytes

乍看起来,这个使用管道的例子并无特别之处,它做的工作可以用一个简单的文件来完成。管道的真正优势体现在,当你想在两个进程之间传递数据时,程序用fork创建新进程时,原先打开的文件描述符仍将保持打开的状态。如果在原先的进程中创建一个管道,然后再调用fork创建新进程,我们即可通过管道在两个进程之间传递数据。

跨越fork调用的管道的程序pipe2.c的开始部分(直到fork调用为止)和第一个例子非常相似。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
int length = 0;
int pipes[2];
const char data[] = "MONKEY.D.MENG";
char buffer[BUFSIZ + 1];
pid_t child_pid;

memset(buffer, 0, sizeof(buffer));

if (pipe(pipes) == 0)
{
child_pid = fork();
if (child_pid == -1)
{
fprintf(stderr, "Fork failure!");
return EXIT_FAILURE;
}

if (child_pid == 0)
{
length = read(pipes[0], buffer, BUFSIZ);
printf("Read %d bytes:%s/n", length, buffer);
return EXIT_SUCCESS;
}
else
{
length = write(pipes[1], data, strlen(data));
printf("Wrote %d bytes/n", length);
}
}

return EXIT_SUCCESS;
}
编译程序:
gcc pipe2.c –o pipe2
运行程序:
./pipe2
Wrote 13 bytes
Read 13 bytes:MONKEY.D.MENG

实验解析:
你可能发现在实际运行这个程序的时候,命令提示符在输出结果的最后一行之前出现了,为了便于阅读,我们在这里对输出结果进行了调整。
这个程序首先用pipe调用创建一个管道,接着用fork调用创建一个新进程。如果fork调用成功,父进程就写数据到管道中,而子进程从管道中读取数据。父子进程都在只调用了一次write或read之后就退出。如果父进程在子进程之前退出,你就会在两部分输出内容之间看到shell命令提示符。
虽然从表面上看,这个程序和第一个使用管道的例子很相似,但实际上在这个例子中我们往前跨出了一大步,我们可以在不同的进程之间进行读写操作。

5.父进程与子进程
在接下来的对pipe调用的研究中,我们将学习如何在子进程中运行一个与其父进程完全不同的另外一个程序,而不仅仅运行一个相同的程序。我们用exec调用来完成这一工作。这里的一个难点是,通过exec进程需要知道应该访问哪个文件描述符。在前面的例子中,因为子进程本身有pipes数据的一份拷贝,所以这并不成为问题。但经过exec调用之后,情况就不一样了,因为原先的进程已经被新的子进程所替换。为解决这个问题,我们可以将文件描述符(它实际上是一个整数)作为一个参数传递给exec启动的程序。
为了演示它是如何工作,我们需要使用两个程序。第一个程序是“数据生产者”,它负责创建管道和子进程,而后者是“数据消费者”。
//下面这个程序pipe3.c是管道和exec函数的演示。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
int length = 0;
int pipes[2];
const char data[] = "MONKEY.D.MENG";
char buffer[BUFSIZ + 1];
pid_t child_pid;

memset(buffer, 0, sizeof(buffer));

if (pipe(pipes) == 0)
{
child_pid = fork();
if (child_pid == -1)
{
fprintf(stderr, "Fork failure!");
return EXIT_FAILURE;
}

if (child_pid == 0)
{
sprintf(buffer, "%d", pipes[0]);
execl("pipe4", "pipe4", buffer, (char *)0);

return EXIT_SUCCESS;
}
else
{
length = write(pipes[1], data, strlen(data));
printf("%d - wrote %d bytes/n", getpid(), length);
}
}
return EXIT_SUCCESS;
}

//“数据消费者”程序pipe4.c负责读取数据,它的代码要简单的多,如下所示:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
int length = 0;
int file_descriptor;
char buffer[BUFSIZ + 1];

memset(buffer, 0, sizeof(buffer));
sscanf(argv[1], "%d", &file_descriptor);
length = read(file_descriptor, buffer, BUFSIZ);

printf("%d -read %d bytes: %s/n", getpid(), length, buffer);
return EXIT_SUCCESS;
}
编译程序:
gcc pipe4.c –o pipe4
gcc pipe3.c –o pipe3
运行程序:
./pipe3
10733 - wrote 13 bytes
10734 -read 13 bytes: MONKEY.D.MENG

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

一、管道关闭后的读操作
在继续学习之前,我们再来仔细研究一下打开的文件描述符。到此,我们一直采取的是让读进程简单的读取一些数据然后直接退出的方式,并假设Linux会把清理文件当作是在进程结束时应该做的工作的一部分。
但大多数从标准输入读取的数据的程序采用的却是与我们迄今为止见到的例子非常不同的另外一种做法。通常它们并不知道有多少数据需要读取,所以往往采用循环的方法,读取数据----处理数据----读取更多的数据,直到没有数据可读为止。
当没有数据可读时,read调用通常会阻塞,即它将暂停进程以等待直到有数据到达为止。如果管道的另一端已经被关闭,也就是说没有进程打开这个管道并向它写数据了,这时read调用就会被阻塞。但这样的阻塞不是很有用,因此对一个已经关闭写数据的管道做read调用将返回0,而不是阻塞。这就使读进程能够像检测文件结束一样,对管道进行检测并作出相应的动作。注意,这与读取一个无效的文件描述符不同,read把无效的文件描述符看作一个错误并返回-1。
如果我们跨越fork调用使用管道,就会有两个不同的文件描述符可以用于向管道写数据,一个在父进程中,一个在子进程。只有把父子进程中的针对管道的写文件描述符都关闭,管道才会被认为是关闭了,对管道的read调用才会失败。我们还将对这一问题做更深入的讨论,在学习到O_NONBLOCK标志和FIFO时,我们将看到一个这样的例子。

二、把管道用作标准输入和标准输出
现在,知道了如何使得一个空管道的读操作失败,下面我们来看一种用管道连接两个进程的更简洁的方法。我们把其中一个管道文件描述符设置为一个已知值,一般是标准输入0或标准输出1。在父进程中做这个设置稍微有点复杂,但它使得子程序的编写变得非常简单。
这样做的最大好处是我们可以调用标准程序,即那些不需要以文件描述符为参数的程序。为了完成这个工作,我们需要使用dup函数。dup函数有两个紧密关联的版本,它们的原型如下所示:
#include <unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);

dup调用的目的是打开一个新的文件描述符,这与open调用有点类似。不同之处在于,dup调用创建的新文件描述符与作为它的参数的那个已有文件描述符指向同一个文件(或管道)。对于dup函数来说,新的文件描述符总是取最小的可用值,而对于dup2函数来说,它所创建的新文件描述符或者与参数file_descriptor_two相同,或者是第一个大于该参数的可用值。
我们可以使用更通用的fcntl调用(command参数设置为F_DUPFD)来达到与调用dup和dup2相同的效果。虽然如此,但dup调用更易于使用,因为它是专门用于复制文件描述符的。而且它的使用非常普遍,你可以发现,在已有程序中,它的使用比fcntl和F_DUPFD更频繁。
那么,dup是如何帮助我们在进程间传递数据的呢?诀窍在于,标准输入的文件描述符总是0,而dup返回的新文件描述符又总是使用最小可用的数字。因此,我们首先关闭文件描述符0,然后调用dup,那么新的文件描述符就是数字0。因为新的文件描述符是复制一个已有的文件描述符,所以标准输入就会改为指向一个我们传递给dup函数的文件描述符所对应的文件或管道。我们创建了两个文件描述符,它们指向同一个文件或管道,而且其中之一是标准输入。
这次,我们把子程序的stdin文件描述符替换为我们创建的管道的读取端。我们还将对文件描述符做一些整理,使得子程序可以正确地检测到管道中数据的结束。与往常一样,为了简洁起见,我们省略了一些错误检查。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
int length = 0;
int pipes[2];
const char data[] = "MONKEY.D.MENG";
pid_t child_pid;

if (pipe(pipes) == 0)
{
child_pid = fork();
if (child_pid == -1)
{
fprintf(stderr, "Fork failure!");
return EXIT_FAILURE;
}

if (child_pid == 0)
{
close(0);
dup(pipes[0]);
close(pipes[0]);

// 子进程的写文件描述符关闭,管道不会关闭
close(pipes[1]);

execlp("od", "od", "-c", (char *)0);

return EXIT_SUCCESS;
}
else
{
close(pipes[0]);
length = write(pipes[1], data, strlen(data));

// 父进程的写文件描述符关闭,管道才会关闭
close(pipes[1]);
printf("%d - wrote %d bytes/n", getpid(), length);
}
}

return EXIT_SUCCESS;
}
编程程序:
gcc pipe5.c –o pipe5
运行程序:
./pipe5
10874 - wrote 13 bytes
0000000   M   O   N   K   E   Y   .   D   .   M   E   N   G
0000015

实验解析:
与往常一样,这个程序创建一个管道,然后通过fork创建一个子进程。此时,父子进程都有可以访问管道的文件描述符,一个用于读数据,一个用于写数据,所以总共有四个打开的文件描述符。
我们首先看子进程。子进程先用close(0)关闭它的标准输入,然后调用dup(pipe[0])把与管道的读取端关联的文件描述符复制为文件描述符0,即标准输入。接下来,子进程关闭原先的用来从管道读取数据的文件描述符pipes[0]。因为子进程不会向管道写数据,所以它把与管道关联的写操作文件描述符pipes[1]也关闭了。现在,它只有一个与管道关联的文件描述符,即文件描述符0,它的标准输入。
接下来,子进程就可以用exec函数启动任何从标准输入读取数据的程序了。在本例中我们使用的是od命令。od命令将等待数据的到来,就好像它在等待来自用户终端的输入一样。事实上,如果没有明确使用检测这两者之间不同的特殊代码,它并不知道输入是来自一个管道,而不是来自一个终端。
父进程首先关闭管道的读取端pipes[0],因为它不会从管道读取数据。接着它向管道写入数据。当所有数据都写完后,父进程关闭管道的写入端并退出。因为现在已经没有打开的文件描述符可以向管道定数据了,od程序读取写到管道中的三个字节数据后,后续的读操作将返回0字节,表示已经到达文件尾。当读操作返回0时,od程序就退出运行。这类似于在终端上运行od命令,然后按下Ctrl + D组合键发送文件尾标志。

6.命名管道:FIFO
到此,我们还只能在相关的程序之间传递数据,即这些程序是由一个共同的祖先进程启动的。但如果我们想在不相关的进程之间交换数据,这还不是很方便的。
我们可以用FIFO文件来完成这项工作,它通常也被称为命名管道。命名管道是一种特殊类型的文件,不要忘记了Linux环境下所有的事物皆是文件,它在文件系统中以文件名的形式存在,但它的行为却和我们已经见过的没有名字的管道类似。
我们可以在命令行上创建命名管道,也可以在程序中创建它。从历史上看,命令行上用来创建命名管道的程序一直是mknod,如下所示:
mknod filename p
但mknod命令并未出现在X/Open规范的命令列表中,所以可能并不是所有的类UNIX系统都可以这样做。推荐使用的命令行如下所示:
mkfifo filename
有些老版本的UNIX系统只有mknod命令。X/Open规范的第四期第二版本中有mknod函数调用,但没有对应的命令行程序。Linux系统非常友好,它同时支持mknod和mkfifo。
在程序中,我们可以使用两个不同的函数调用,如下所示:
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *filename, mode_t mode);
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
与mknod命令一样,可以用mknod函数建立许多特殊类型的文件。要想通过这个函数创建一个命名管道,唯一具有可移植性的方法是使用一个有dev_t类型的值0,并将文件访问模式与S_IFIFO按位或。我们在下面的例子中将使用较简单的mkfifo函数。
下面程序fifo1.c是创建命名管道。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
int res = mkfifo("my_fifo", 0777);
if (res == 0)
{
printf("FIFO created!/n");
}

return EXIT_SUCCESS;
}
编译程序:
gcc fifo1.c –o fifo1
运行程序:
./fifo1
FIFO created


实验解析:

我们可以用下面命令找到刚创建的管道:
ls –lF
prwxrwxr-x 1 monkey monkey 0 10月 14 17:11 my_fifo|
注意,输出结果中的第一个字符为p,表示这是一个管道。最后的 | 符号是由ls命令的-F选项添加的,它也表示这是一个管道。
这个程序用mkfifo函数创建一个特殊的文件。虽然我们要求的文件模式是0777,但它被用户掩码(umask)设置(我所用的服务器为0002)给改变了,这与普通文件的创建是一样的,所以文件的最终模式是775。如果你的掩码设置与这里不同,则你会看到一个不同的权限。
我们可以像删除一个普通文件那样用rm命令删除FIFO文件,或者也可以在程序中用unlink系统调用来删除它。

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

实验:访问FIFO文件
(1)首先,我们来尝试读这个(空的)FIFO文件:
cat < my_fifo
(2)现在,尝试向FIFO写数据。你必须用另一个终端来执行下面命令,因为目前将挂起第一个命令,等等数据出现在FIFO中。
echo “MONKEY.D.MENG” > my_fifo
你将看到cat命令产生输出。如果不向FIFO发送任何数据,cat命令将一直挂起,直到你中断它,常用的中断方式是使用组合键”Ctrl + C”。
(3)我们可以将第一个命令放在后台执行,这样即可一次执行两个命令:
cat < my_fifo &
echo “MONKEY.D.MENG” > my_fifo
实验解析:
因为FIFO中没有数据,所以cat和echo程序都阻塞了,cat等待数据的到来,而echo等待其他进程读取数据。
在上面的第三步中,cat进程一开始就在后台被阻塞了,当echo向它提供一些数据之后,cat命令读取这些数据并把它们打印到标准输出上,然后cat程序退出,不再等待更多数据。它没有阻塞是因为第二个命令将数据放入FIFO后,管道将被关闭,所以cat程序中的read调用返回0字节,表示已经到达文件尾。
现在已经看过用命令行程序访问FIFO的情况,接下来我们将仔细分析FIFO的编程接口,它可以让我们在访问FIFO文件时更多地控制其读写行为。
与通过pipe调用创建管道不同的是,FIFO是以命名文件的形式存在,而不是打开文件描述符,所以在对它进行读写操作之前必须先打开它。FIFO也用open和close函数打开和关闭,这与我们前面看到的对文件的操作一样,但它多了一些额外的功能。对FIFO来说,传递给open调用的是FIFO的路径名,而不是一个正常的文件。
打开FIFO的一个主要限制是,程序不能以O_RDWR模式打开FIFO文件进行读写操作,这样做的后果并未明确定义。但这个限制是有道理的,因为我们通常使用FIFO只是为了单向传递数据,所以没有必要使用O_RDWR模式。如果一个管道以读/写方式打开,进程就会从这个管道读回它自己的输出。
如果确实需要在程序之间双向传递数据,最好使用一对FIFO或管道,一个方向使用一个,或者(但不常用)采用先关闭再重新打开FIFO的方法来明确的改变数据流的方向。
打开FIFO文件和打开普通文件的另一点区别在于:对open_flag的O_NONBLOCK选项的用法。使用这个选项不仅改变open调用的处理方式,还会改变对这次open调用返回的文件描述符进行的读写请求的处理方式
O_RDONLY、O_WRONLY和O_NONBLOCK标志共有4种合法的组合方式,我们将逐个介绍它们。
open(const char *path, ORDONLY);
此时,open调用将阻塞,除非有一个进程以写方式打开同一个FIFO,否则它不会返回。这与前面第一个cat命令的例子类似。
open(const char *path, ORDONLY | O_NONBLOCK);
即使没有其他进程以写方式打开FIFO,这个将成功并立刻返回。
open(const char *path, O_WRONLY);
此时,open调用将阻塞,直到有一个进程以读方式打开同一个FIFO为止。
open(const char *path, O_WRONLY | O_NONBLOCK);
这个函数调用总是立刻返回,但如果没有进程以读方式打开FIFO文件,open调用将返回一个错误-1,并且FIFO也不会被打开。如果确实有一个进程以读方式打开FIFO文件,那么我们就可以通过它返回的文件描述符对这个FIFO文件进行写操作。
请注意O_NONBLOCK分别搭配O_RDONLY和O_WRONLY在效果上的不同,如果没有进程以读方式打开管道,非阻塞写方式的open调用将失败,但非阻塞读方式的open调用总是成功。close启用的行为并不受O_NONBLOCK标志的影响。
下面我们来看,如何通过使用带O_NONBLOCK标志的open调用的行为来同步两个进程。我们在这里并没有选择使用多个示例程序的做法,而是只使用一个测试程序fifo2.c,通过给该程序传递不同的参数的方法来观察FIFO的行为。
#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 = 0;
int open_mode = 0;

if (argc < 2)
{
fprintf(stderr, "Usage: %s <some combination of O_RDONLY O_WRONLY O_NONBLOCK>/n", *argv);
return 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", 8) == 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", 8) == 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);
return EXIT_FAILURE;
}
}

printf("Process %d opening 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());

return EXIT_SUCCESS;
}
编译程序:
gcc fifo2.c –o fifo2
运行程序:
./fifo2 O_RDONLY &
[2] 12326
[1]   Exit 127                ./fifo O_RDONLY
Process 12326 opening FIFO

./fifo2 O_WRONLY
Process 12327 opening FIFO
Process 12327 result 3
Process 12326 result 3
Process 12327 finished
Process 12326 finished


实验解析:
这个程序允许我们在命令行上指定我们希望使用的O_RDONLY、O_WRONLY和O_NONBLOCK的组合方式。它会把命令行参数与程序中的常量字符串进行比较,如果匹配,就(用!=操作符)设置相应的标志。程序用access函数来检查FIFO文件是否存在,如果不存在就创建它。
在程序中,一直到最后都没有删除这个FIFO文件,因为我们没办法知道是否有其他程序正在使用它。
当一个Linux进程被阻塞时,它并不消耗CPU资源,所以这种进程的同步方式对CPU来说是非常有效率的。
有关FIFO的读写规则,可参考博文《Linux命名管道FIFO的读写规则》
为了演示不相关的进程是如何使用命名管道进程通信的,我们需要用到两个独立的程序fifo3.c和fifo4.c。
第一个程序是“生产者”程序。它在需要时创建管道,然后尽可能快地向管道中写入数据。注意,出于演示的目的,我们并不关心写入数据的内容,所以我们并未对缓冲区buffer进行初始化。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 *10)

int main()
{
int pipe_fd;
int res;
int open_mode = O_WRONLY;
int bytes_sent = 0;
char buffer[BUFFER_SIZE + 1];

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);
return EXIT_FAILURE;
}
}

printf("Process %d opening 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)
{
fprintf(stderr, "Write error on pipe/n");
return EXIT_FAILURE;
}
bytes_sent += res;
}
close(pipe_fd);
}
else
{
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}
第二个程序是“消费者”程序,它的代码要简单的多,它从FIFO读取数据并丢弃它们。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF

int main()
{
int pipe_fd;
int res = 0;
int open_mode = O_RDONLY;
char buffer[BUFFER_SIZE + 1];
int bytes_read = 0;

memset(buffer, 0, sizeof(buffer));

printf("Process %d opening 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
{
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}
编程程序:
gcc fifo3.c –o fifo3
gcc fifo4.c –o fifo4
运行程序:
./fifo3 &
[1] 13002
Process 13002 opening FIFO O_WRONLY

time ./fifo4
Process 13003 opening FIFO O_RDONLY
Process 13003 result 3
Process 13002 result 3
Write error on pipe
[1]+  Exit 1                  ./fifo3

real    0m0.001s
user    0m0.000s
sys     0m0.002s

实验解析:
我们在运行这两个程序时,用time命令对读进程进行计时。两个程序都是使用的都是阻塞模式的FIFO。我们首先启动fifo3(写进程/生产者),它将阻塞以等待读进程打开这个FIFO。fifo4(读进程/消费者)启动以后,写进程解除阻塞并开始向管道写数据。同时,读进程也开始从管道中读取数据。
Linux会安排好这两个进程之间的调度,使它们在可以运行的时候运行,在不能运行的时候阻塞。因此,写进程将在管道满时阻塞,读进程将在管道空时阻塞。
time命令的输出显示,在我们实验室的服务器上,读进程只运行了不到0.01s的时间,却读取了10M字节的数据。这说明管道在程序之间传递数据是很有效率的。

二、高级主题:使用FIFO的客户/服务器应用程序
我们来考虑如何通过使用命名管道来编写一个客户/服务器端应用唷。我们想只用一个服务器进程来接受请求,对它们进行处理,最后把结果数据返回给发送请求的一方:客户。
我们想允许多个客户进程都可以向服务器发送数据。为了使问题简单化,我们假设被处理的数据可以被拆分为一个个数据块,每个的长度都小于PIPE_BUF字节。当然我们可以用很多方法实现这个系统,但在这里我们只考虑一种方法,即可以全责如何使用命名管道的方法。
因为服务器每次只能处理一个数据块,所以只使用一个FIFO应该是合乎逻辑的,服务器通过它读取数据,每个客户向它写数据。只要将FIFO以阻塞模式打开,服务器和客户就会根据需要自动被阻塞。
将处理后的数据返回给客户稍微有些困难。我们需要为每个客户安排第二个管道来接收返回的数据。通过在传递给服务器的原先数据中加上客户进程的标识符(PID),双方就可以使用它来为返回数据的管道生成一个唯一的名字。
首先,我们需要一个头文件client.h,它定义了客户和服务器程序都会用到的数据。为了方便使用,它还包含了必要的系统头文件。
//头文件client.h
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>

#define SERVER_FIFO_NAME "/tmp/serv_fifo"
#define CLIENT_FIFO_NAME "/tmp/cli_%d_fifo"

#define BUFFER_SIZE 20

struct message
{
pid_t client_pid;
char data[BUFFER_SIZE + 1];
};

//客户端程序client.c
#include "client.h"

int main()
{
int server_fifo_fd;
int client_fifo_fd;
struct message msg;
char client_fifo[256];

server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY);
if (server_fifo_fd == -1)
{
fprintf(stderr, "Sorry, no server/n");
return EXIT_FAILURE;
}

msg.client_pid = getpid();
sprintf(client_fifo, CLIENT_FIFO_NAME, msg.client_pid);

if (mkfifo(client_fifo, 0777) == -1)
{
fprintf(stderr, "Sorry, can not make %s/n", client_fifo);
return EXIT_FAILURE;
}

sprintf(msg.data, "MONKEY.D.MENG");
printf("%d sent %s/n", msg.client_pid, msg.data);
write(server_fifo_fd, &msg, sizeof(msg));

client_fifo_fd = open(client_fifo, O_RDONLY);
if (client_fifo_fd != -1)
{
if (read(client_fifo_fd, &msg, sizeof(msg)) > 0)
{
printf("received : %s/n", msg.data);
}
close(client_fifo_fd);
}

close(server_fifo_fd);
unlink(client_fifo);

return EXIT_SUCCESS;
}

//服务器端程序server.c
#include "client.h"

int main()
{
int client_fifo_fd;
int server_fifo_fd;
struct message msg;
int read_res = 0;
char client_fifo[256];

mkfifo(SERVER_FIFO_NAME, 0777);
server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);

if (server_fifo_fd == -1)
{
fprintf(stderr, "Server fifo failure/n");
return EXIT_FAILURE;
}

sleep(10);

do
{
read_res = read(server_fifo_fd, &msg, sizeof(struct message));
if (read_res > 0)
{
sprintf(client_fifo, CLIENT_FIFO_NAME, msg.client_pid);
client_fifo_fd = open(client_fifo, O_WRONLY);
if (client_fifo_fd != -1)
{
write(client_fifo_fd, &msg, sizeof(struct message));
close(client_fifo_fd);
}
}
}while(read_res > 0);

close(server_fifo_fd);
unlink(SERVER_FIFO_NAME);

return EXIT_SUCCESS;
}
编译程序:
gcc client.c –o client
gcc server.c –o server
运行程序:
./server &
[1] 13641
./client
13642 sent MONKEY.D.MENG
received : MONKEY.D.MENG
[1]+  Done                    ./server

实验解析:
服务器端是以只读模式创建它的FIFO并阻塞,直到第一个客户以写方式打开同一个FIFO来建立连接为止。此时,服务器进程解除阻塞并执行sleep语句,这使得客户的数据排除等候,然而,在实际应用程序中,应该把sleep语句删除。我们在这里使用它的目的只是为了演示当有多个客户的请求同时到达时,程序的正确操作方法。
与此同时,客户打开了服务器FIFO后,它创建自己唯一的一个命名管道以读取服务器返回的数据。完成这些工作后,客户发送数据给服务器,如果管道满或服务器仍在休眠中就阻塞,并阻塞在对自己的FIFO的read调用上,等待服务器的响应。
接收到来自客户的数据后,服务器处理它,然后以写的方式打开客户管道并将处理后的数据返回,这将解除客户的阻塞状态。客户被解除阻塞后,它即可从自己的管道中读取服务器返回的数据。
整个处理过程不断重复,直到最后一个客户关闭服务器管道为止,这将使服务器的read调用失败(返回0),因为已经没有进程以写方式打开服务器管道了。如果这是一个真正的服务器进程,它还需要继续等待客户的请求,我们就需要对它进行修改,有两种方法,如下所示:
(1)对它自己的服务器管道打开一个文件描述符,这样read调用将阻塞而不是返回0。
(2)当read调用返回0时,关闭并重新打开服务器管道,使服务器进程阻塞在open调用处以等待客户的到来,就像它最初启动时那样。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: