什么是管道
当从一个进程连接数据流到另一个进程时,我们使用术语管道(pipe)。我们通常是把一个进程的输出通过管道连接到另一个进程的输入。 大多数Linux的用户应该早已对将shell命令连接在一起的概念很熟悉了,这实际上就是把一个进程的输出直接传递给另一个进程的输入。对于shell命令来说,命令的连接是通过管道链接的,如下所示
cmd1 | cmd2
shell负责安排两个命令的标准输入和标准输出。cmd1的标准输入来自终端键盘。 cmd1的标准输出传递给cmd2,作为它的标准输入。cmd2的标准输出连接到终端屏幕。
shell所做的工作实际上是对标准输入和标准输出流进行了重新连接,从而实个命令最终输出到屏幕上,见图。
在本篇中,我们将看到如何在程序中获得这样的效果,怎样用管道将多个进程连接起来,从而实现一个简单的客户/服务器系统。
进程管道
可能最简单的在两个程序之间传递数据的方法就是使用popen和pclose函数了。它们的原型如下所示:
#include <stdio.h>
FILE *popen(const char *command,const char *open_mode) ;
int pclose(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函数的标准实现中不支持任何其他选项。这意味着我们不能调用另一个程序并同时对它进行读写操作。popen函数在失败时返回一个空指针。如果想通过管道实现双向通信,最普通的解决方法是使用两个管道,每个管道负责一个方向的数据流。
pclose函数
用popen启动的进程结束时,我们可以用pclose函数关闭与之关联的文件流。pclose调用只在popen启动的进程结束后才返回。如果调用pclose时它仍在运行,pclose调用将等待该进程的结束。
pclose调用的返回值通常是它所关闭的文件流所在进程的退出码。如果调用进程在调用pclose之前执行了一个wait语句,被调用进程的退出状态就会丢失,因为被调用进程已结束。此时,pclose将返回-1并设置errno为ECHILD。
实例-读:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
FILE *read_fd;
char buff[128] = {0};
int read_count = 0;
read_fd = popen("uname -a","r");
if(read_fd != NULL)
{
read_count = fread(buff,sizeof(char),127,read_fd);
while(read_count != 0)
{
printf("read : %s",buff);
read_count = fread(buff,sizeof(char),127,read_fd);
}
pclose(read_fd);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
这个程序用popen调用启动带有-a选项的uname命令。然后用返回的文件流读取所有数据,并将它们打印出来显示在屏幕上。因为我们是在程序内部捕获uname命令的输出,所以可以处理它。
实例-写:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
FILE *write_fd;
char buff[128] = "12 23 34 45 56 67 78 89";
int write_count = 0;
write_fd = popen("cat","w");
if(write_fd != NULL)
{
fwrite(buff,sizeof(char),strlen(buff),write_fd);
pclose(write_fd);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
程序使用带有参数"w"的popen启动cat命令,这样就可以向该命令发送数据了。然后它给cat命令发送一个字符串,该命令接收并处理它,最后把处理结果打印到自己的标准输出上。
在命令行上,我们可以用下面的命令得到同样的输出结果:
pipe调用
在看过高级的popen函数之后,我们再来看看底层的pipe函数。通过这个函数在两个程序之间传递数据不需要启动一个shell来解释请求的命令。它同时还提供了对读写数据的更多控制。 pipe函数的原型如下所示:
#include <unistd.h>
int pipe(int fi1e_descriptor[2]);
pipe函数的参数是一个由两个整数类型的文件描述符组成的数组的指针。该函数在数组中填上两个新的文件描述符后返回0,如果失败则返回-1并设置errno来表明失败的原因。在Linux手册页(手册的第二部分)中定义了下面一些错误。 FEMFILE: 进程使用的文件描述符过多。 ENFILE: 系统的文件表己满。 EFAULT: 文件描述符无效。
两个返回的文件描述符以一种特殊的方式连接起来。写到file_descriptor[1]的所有数据都可以从file_descriptor[0]读回来。数据基于先进先出的原则(通常简写为FIFO)进行处理,这意味着如果你把字节1,2,3写到file_descriptor[1],从file_descriptor [0]读取到的数据也会是1,2,3。
特别要注意,这里使用的是文件描述符而不是文件流,所以我们必须用底层的read和write调用来访问数据,而不是用文件流库函数fread和fwrite。
pipe函数使用
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
int file_pipe[2] = {0};
const char send_data[] = "Hello , world \n";
char recv_buff[128] = {0};
if(pipe(file_pipe) == 0)
{
write(file_pipe[1],send_data,strlen(send_data));
int read_num = read(file_pipe[0],recv_buff,127);
write(STDOUT_FILENO,recv_buff,read_num);
close(file_pipe[0]);
close(file_pipe[1]);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
这个程序用数组file_pipes[]中的两个文件描述符创建一个管道。然后它用文件描述符file pipes[1]向管道中写数据,再从file_pipes[0]读回数据。注意,管道有一些内置的缓存区,它在write和read调用之间保存数据。
如果你尝试用file_descriptor[0]写数据或用file_descriptor[1]读数据,其后果并未在文档中明确定义,所以其行为可能会非常奇怪,并且随着系统的不同,其行为可能会发生变化。在我的系统上,这样的调用将失败并返回-1,这至少能够说明这种错误比较容易发现。
乍看起来,这个使用管道的例子并无特别之处,它做的工作也可以用一个简单的文件完成。管道的真正优势体现在,当你想在两个进程之间传递数据的时候。当程序用fork调用创建新进程时,原先打开的文件描述符仍将保持打开状态。如果在原先的进程中创建一个管道,然后再调用fork创建新进程,我们即可通过管道在两个进程之间传递数据。
pipe与fork
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
int file_pipe[2] = {0};
if(pipe(file_pipe) == 0)
{
pid_t pid = fork();
if(pid == 0)
{
const char send_data[] = "Hello , world \n";
write(file_pipe[1],send_data,strlen(send_data));
close(file_pipe[1]);
}
else
{
char recv_buff[128] = {0};
read(file_pipe[0],recv_buff,127);
printf("read : %s",recv_buff);
close(file_pipe[0]);
}
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
这个程序首先用pipe调用创建一个管道,接着用fork调用创建一个新进程。如果fork调用成功,子进程就写数据到管道中,而父进程从管道中读取数据。父子进程都在只调用了一次write或read之后就退出。
虽然从表面上看,这个程序和第一个使用管道的例子很相似,但实际上在这个例子中我们往前跨出了一大步,我们可以在不同的进程之间进行读写操作了,如图所示。
pipe与exec
在接下来的对pipe调用的研究中,我们将学习如何在子进程中运行一个与其父进程完全不同的另外一个程序,而不是仅仅运行一个相同程序。我们用exec调用来完成这一工作。这里的一个难点是,通过exec调用的进程需要知道应该访问哪个文件描述符。在前面的例子中,因为子进程本身有file_pipes数据的一份副本,所以这并不成为问题。但经过exec调用后,情况就不一样了,因为原先的进程已经被新的子进程替换了。为解决这个问题,我们可以将文件描述符(它实际上只是一个数字)作为一个参数传递给用exec启动的程序。
为了演示它是如何工作的,我们需要使用两个程序。第一个程序是数据生产者,它负责创建管道和启动子进程,而后者是数据消费者。 生产者:
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
int main()
{
int file_pipe[2] = {0};
if(pipe(file_pipe) == 0)
{
int pid = fork();
if(pid == 0)
{
char buff[5] = {0};
sprintf(buff,"%d",file_pipe[0]);
execl("consumer","consumer",buff,NULL);
}
else
{
char send_data[] = "12 23 45 56 78 \n";
write(file_pipe[1],send_data,strlen(send_data));
wait(NULL);
}
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
消费者:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(int argc,void *argv[])
{
int file_pipe = 0;
sscanf((char*)argv[1],"%d",&file_pipe);
char buff[128] = {0};
read(file_pipe,buff,127);
printf("read : %s",buff);
exit(EXIT_SUCCESS);
}
结果: 生产者程序的开始部分和前面的例子一样,用pipe调用创建一个管道,然后用fork调用创建一个新进程。接下来,它用sprintf把读取管道数据的文件描述符保存到一个缓存区中,该缓存区中的内容将构成消费者程序的一个参数。 我们通过execl调用来启动pipe4程序,execl的参数如下所示。 要启动的程序。 argv[0]:程序名。 argv[1]:包含我们想让被调用程序去读取的文件描述符。 NULL:这个参数的作用是终止被调用程序的参数列表。 消费者程序从参数字符串中提取出文件描述符数字,然后读取该文件描述符来获取数据。
管道关闭后的读操作
在继续学习之前,我们再来仔细研究一下打开的文件描述符。至此,我们一直采取的是让读进程读取一些数据然后直接退出的方式,并假设Linux会把清理文件当作是在进程结束时应该做的工作的一部分。
但大多数从标准输入读取数据的程序采用的却是与我们到目前为止见到的例子非常不同的另外一种做法。通常它们并不知道有多少数据需要读取,所以往往采用循环的方法,读取数据——处理数据——读取更多的数据,直到没有数据可读为止。
当没有数据可读时,read调用通常会阻塞,即它将暂停进程来等待直到有数据到达为止。如果管道的另一端已被关闭,也就是说,没有进程打开这个管道并向它写数据了,这时read调用就会阻塞。但这样的阻塞不是很有用,因此对一个已关闭写数据的管道做read调用将返回0而不是阻塞。这就使读进程能够像检测文件结束一样,对管道进行检测并作出相应的动作。注意,这与读取一个无效的文件描述符不同,read把无效的文件描述符看作一个错误并返回-1。
如果跨越fork调用使用管道,就会有两个不同的文件描述符可以用于向管道写数据,一个在父进程中,一个在子进程中。只有把父子进程中的针对管道的写文件描述符都关闭,管道才会被认为是关闭了,对管道的read调用才会失败。
命名管道:FIFO
至此,我们还只能在相关的程序之间传递数据,即这些程序是由一个共同的祖先进程启动的。但如果我们想在不相关的进程之间交换数据,这还不是很方便。
我们可以用FIFO文件来完成这项工作,它通常也被称为命名管道(named pipe)。命名管道是一种特殊类型的文件(别忘了Linux中的所有事物都是文件),它在文件系统中以文件名的形式存在,但它的行为却和我们已经见过的没有名字的管道类似。 我们可以在命令行上创建命名管道,也可以在程序中创建它。命令行上用来创建命名管道的程序是mkfifo,如下所示:
mkfifo filename
在程序中,我们可以使用两个不同的函数调用,如下所示;
#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);
在下面的例子里面,我们使用较为简单的mkfifo
创建命名管道
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
int res = mkfifo("my_fifo",0600);
if( res == 0 ) printf("FIFO Created \n");
exit(EXIT_SUCCESS);
}
访问FIFO文件
接下来我们将仔细分析FIFO的编程接口,它可以让我们在访问FIFO文件时更多地控制其读写行为。
与通过pipe调用创建管道不同,FIFO是以命名文件的形式存在,而不是打开的文件描述符,所以在对它进行读写操作之前必须先打开它。 FIFO也用open和close函数打开和关闭,这与我们前面看到的对文件的操作一样,但它多了一些其他的功能.对FIFO来说,传递给open调用的是FIFO的路径名,而不是一个正常的文件。
使用open打开FIFO文件
打开FIFO的一个主要限制是,程序不能以O_RDWR模式打开FIFO文件进行读写操作,这样做的后果并未明确定义。但这个限制是有道理的,因为我们通常使用FIFO只是为了单向传递数据,所以没有必要使用O_RDWR模式。如果一个管道以读/写方式打开,进程就会从这个管道读回它自己的输出。
如果确实需要在程序之间双向传递数据,最好使用一对FIFO或管道,一个方向使用一个,或者(但并不常用)采用先关闭再重新打开FIFO的方法来明确地改变数据流的方向。
打开FIFO文件和打开普通文件的另一点区别是,对open_flag (open函数的第二个参数)的O_NONBLOCK选项的用法。使用这个选项不仅改变open调用的处理方式,还会改变对这次open调用返回的文件描述符进行的读写请求的处理方式。 O_RDONLY、O_WRONLY和O_NONBLOCK标志共有4种合法的组合方式,我们将逐个介绍它们。
open(const char *path,O_RDONLY);
在这种情况下,open调用将阻塞,除非有一个进程以写方式打开同一个FIFO,否则它不会返回。
open(const char *path, O__RDONLY | O_NONBLOCK);
即使没有其他进程以写方式打开FIFO,这个open调用也将成功并立刻返回。
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标志的影响。
对FIFO进行读写操作
使用O_NONBLOCK模式会影响到对FIFO的read和write调用。
对一个空的、阻塞的FIFO(即没有用O_NONBLOCK标志打开)的read调用将等待,直到有数据可以读时才继续执行。与此相反,对一个空的、非阻塞的FIFO的read调用将立刻返回0字节。
对一个完全阻塞FIFO的write调用将等待,直到数据可以被写入时才继续执行。如果FIFO不能接收所有写入的数据,它将按下面的规则执行。
如果请求写入的数据的长度小于等于PIPE_BUF字节,调用失败,数据不能写入。 如果请求写入的数据的长度大于PIPE_BUF字节,将写入部分数据,返回实际写入的字节数,返回值也可能是0。
FIFO的长度是需要考虑的一个很重要的因素。系统对任一时刻在一个FIFO中可以存在的数据长度是有限制的。它由#define PIPE_BUF语句定义,通常可以在头文件limits.h中找到它。在Linux和许多其他类UNIX系统中,它的值通常是4 096字节,但在某些系统中它可能会小到512字节。系统规定:在一个以O_WRONLY方式(即阻塞方式)打开的FIFO中,如果写入的数据长度小于等于PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。
虽然,对只有一个FIFO写进程和一个FIFO读进程的简单情况来说,这个限制并不是非常重要,但只使用一个FIFO并允许多个不同的程序向一个FIFO读进程发送请求的情况是很常见的,如果几个不同的程序尝试同时向FIFO写数据,能否保证来自不同程序的数据块不相互交错就非常关键了。也就是说,每个写操作都必须是“原子化”的。怎样才能做到这一点呢?
如果你能保证所有的写请求是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据决不会交错在一起。通常将每次通过FIFO传递的数据长度限制为PIPE_BUF字节是个好方法,除非你只使用一个写进程和一个读进程。
使用FIFO实现进程间通信
第一个是发送程序: 注意,出于演示的目的,我们并不关心写入数据的内客,所以我们并未对缓冲区进行初始化。(发10M的数据)
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
int fd = open("./fifo",O_WRONLY);
char buff[128];
int send_num = 1024*1024*10;
while(send_num > 0)
{
write(fd,buff,128);
send_num-=128;
}
close(fd);
exit(EXIT_SUCCESS);
}
第二个是接收程序:接收–>丢弃
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
int fd = open("./fifo",O_RDONLY);
char buff[128] = {0};
int recv_num = 0;
while(read(fd,buff,128))
{
recv_num+=128;
}
printf("I recv %d byte\n",recv_num);
close(fd);
exit(EXIT_SUCCESS);
}
运行这两个程序之时,可以用time命令记录其运行的时间,输出结果如下 两个程序使用的都是阻塞模式的FIFO。我们首先启动写进程,它将阻塞以等待读进程打开这个FIFO。消费者启动以后,写进程解除阻塞并开始向管道写数据。同时,读进程也开始从管道中读取数据。
Linux会安排好这两个进程之间的调度,使它们在可以运行的时候运行,在不能运行的时候阻塞。因此,写进程将在管道满时阻塞,读进程将在管道空时阻塞。 管道在程序之间传递数据是很有效率的。
|