父子进程
我们的pipe调用探索的下一步就是使得子进程是与父进程不同的一个程序,而不是运行相同程序的另一个进程。我们可以使用exec调用来完成这个任务。这样做的一个困难就是通过exec执行的新进程需要知道访问哪一个文件描述符。在exec调用之后,就不再是这样的情况了,因为老进程已经被新的子进程所替代。我们可以通过向exec所执行的新进程传递文件描述符作为参数就可以解决这个问题。
要显示这是如何工作的,我们需要两个程序。第一个就是数据生产者,他创建管道并且调用子进程,数据消费者。
试验--管道与exec
1 作为第一个程序,我们将pipe2.c修改为pipe3.c。对比这两个文件我们可以看到修改的行:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
int data_processed;
int file_pipes[2];
const char some_data[] = "123";
char buffer[BUFSIZ +1];
pid_t fork_result;
memset(buffer,'/0',sizeof(buffer));
if(pipe(file_pipes) == 0)
{
fork_result = fork();
if(fork_result == (pid_t) -1)
{
fprintf(stderr,"Fork failure");
exit(EXIT_FAILURE);
}
if(fork_result == 0)
{
sprintf(buffer,"%d",file_pipes[0]);
(void)execl("pipe4","pipe4",buffer,(char *)0);
exit(EXIT_FAILURE);
}
else
{
data_processed = write(file_pipes[1],some_data,strlen(some_data));
printf("%d - wrote %d bytes/n",getpid(),data_processed);
}
}
exit(EXIT_SUCCESS);
}
2 读取数据的消费者程序,pipe4.c,与此相类似:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc,char **argv)
{
int data_processed;
char buffer[BUFSIZ +1];
int file_descriptor;
memset(buffer,'/0',sizeof(buffer));
sscanf(argv[1],"%d",&file_descriptor);
data_processed = read(file_descriptor,buffer,BUFSIZ);
printf("%d - read %d bytes: %s/n",getpid(),data_processed,buffer);
exit(EXIT_SUCCESS);
}
记住,当我们运行pipe3程序时,pipe3会为我们调用pipe4,我们会得到下面的结果:
$ ./pipe3
980 - wrote 3 bytes
981 - read 3 bytes: 123
工作原理
pipe3程序的开始部分与前面的例子相似,使用pipe调用来创建一个管道然后使用fork调用来创建一个新的进程。然后他使用sprintf将管道的"读"文件描述号存入一个缓冲区并构成pipe4程序的一个参数。
使用execl调用来调用pipe4程序。execl的参数如下:
要调用的程序
argv[0]为程序名
argv[1]包含我们希望程序由其中进行读取的文件描述号
(char *)0结束参数
pipe4程序由参数字符串获取文件描述符号并且由这个文件描述符中进行读取以获取数据。
读取关闭的管道
在我们继续之前,我们需要更为仔细的了解一下打开的文件描述符。直到此时,我们只是使得读取进程简单的读取一些数据然后退出,假定Linux会作为进程结束的一部分进行文件清理。
大多数由标准输入读取数据的程序的行为方式与我们到目前为止所见到的例子有些不同。他们通常并不知道他们要读取多少数据,所以他们通常进行循环,读取,处理,然后读取更多的数据,直到没有更多的数据可以读取。
一个read调用通常是阻塞的,也就是说,他会使得进程等待直到有数据变为可用。如果管道的另一端已经被关闭,那么就没有进程使得管道打开用于写入,而read就会阻塞。因为这并不是非常有益的,在并没有打开进行写入的管道上调用read会返回零而不是返回阻塞。这使得读取进程可以象检测文件结尾一样检测管道并且进行正确的动作。
注意,这与读取一个不可用的文件描述符时并不一样,此时read会认为这是一个错误并且返回-1来表示错误。
如果我们通过一个fork调用来使用管道,那么就会有两个不同的文件描述符可以供我们使用来写入管道:一个是父进程中的而另一个是子进程中的。我们必须在管道被认为是关闭的之前关闭父子进程中的write文件描述符,否则管道上的read调用就会失败。当我们返回到这个主题来更为详细的了解O_NONBLOCK标记与FIFO时,我们会看到这样的一个例子。
作为标准输入输出使用的管道
现在我们知道如何使在一个空管道上的read调用失败,我们可以看到通过一个管道连接两个进程的更为简洁的方法。我们为管道文件描述中的一个指定一个已知的值,通常是标准输入0,或是标准输出1。在父进程中进行设置会更为复杂,但是他会使得子程序更为简单。
这样做的一个好处就是我们可以调用那些不需要文件描述符作为参数的标准程序。为了这样做,我们需要使用dup函数,这个函数我们在第3章已经了解过了。有两个紧密相关的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,或者是大于参数file_descriptor_two的第一个可用的文件描述符。
注意:我们可以通过使用fcntl调用与F_DUPFD命令来达到与dup和dup2相同的效果。也就是说,dup调用更容易使用,因为他更适合于创建复制文件描述符的需要。他也是非常通用的,所以我们会发现在已存在的程序中,他比fcntl与F_DUPFD更为常见。
那么dup是如何帮助我们在两个进程之间传递数据的呢?这个小技巧就在于标准输入描述符总是0,而dup总是使用最小可用的数字返回新的描述符。通过首先关闭文件描述符0,然后调用dup,新的文件描述符就是数字0。因为新的描述符是已存在文件描述符的一个复制,标准输入已经发生改变来访问具有我们传递给dup的文件描述符的文件或是管道。我们将会创建指向同一个文件或是管道的两个文件描述符,而其中的一个将是标准输入。
通守close与dup操作文件描述
理解当我们关闭文件描述符0并且调用dup之后发生了什么的最简单的方法就是查看在这一过程中前四个文件描述符的状态生了哪些变化。如下表所示:
文件描述符号 初始值 关闭文件描述0之后 dup调用之后
0 标准输入 关闭 管道文件描述符
1 标准输出 标准输出 标准输出
2 标准错误输出 标准错误输出 标准错误输出
3 管道文件描述符 管道文件描述符 管道文件描述符
试验--管道与dup
让我们回到我们前面的例子中,但是这一次我们会使得子程序使用我们所创建的管道的读端来替换其stdin文件描述符。我们也会做一些清理工作,从而子程序可以正确的检测管道中数据的结束。如平时一样,为了代码的简洁,我们忽略了一些错误检测。
修改pipe3.c为pipe5.c,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
int data_processed;
int file_pipes[2];
const char some_data[] = "123";
pid_t fork_result;
if(pipe(file_pipes) == 0)
{
fork_result = fork();
if(fork_result == (pid_t) -1)
{
fprintf(stderr,"Fork failure");
exit(EXIT_FAILURE);
}
if(fork_result == (pid_t) 0)
{
close(0);
dup(file_pipes[0]);
close(file_pipes[0]);
close(file_pipes[1]);
execlp("od","od","-c",(char *)0);
exit(EXIT_FAILURE);
}
else
{
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",(int)getpid(),data_processed);
}
}
exit(EXIT_SUCCESS);
}
如果我们运行这个程序,我们会得到下面的结果:
$ ./pipe5
1239 - wrote 3 bytes
0000000 1 2 3
0000003
工作原理
与前面的例子一样,程序创建一个管道然后进行fork调用生成一个子进程。此时,父子进程都具有访问管道的文件描述符,用于读写的各一个,所以共有四个打开的文件描述符。
让我们首先来看一下子进程。子进程通过close(0)来关闭其标准输入,然后调用dup(file_pipes[0])。这会复制与管道读端相关联的文件描述符作为文件描述符0,标准输入。子进程然后关闭由pipe调用所获得的用于读操作的原始文件描述符,file_pipes[0]。因为子进程绝不会向管道写入,他同时也关闭了与管道相关联的写文件描述符,file_pipes[1]。现在他只有与管道相关联的一个文件描述符:文件描述符0,其标准输入。
子进程然后使用exec来调用读取标准输入的任何程序。在这个例子中,我们使用od命令。od命令会等待可用的数据,如同他正等待用户终端的输入一样。事实上,如果没有一些特殊的代码显示的检测这些区别,他并不会知道其输入是来自一个管道,而不是一个终端。
父进程通过关闭管道的读端file_pipes[0]开始,因为他绝不会由管道中读取。他然后向管道中写入数据。当写入所有的数据以后,父进程会关闭管道的写端并且退出。因为没有可以向管道中写入的打开的文件描述符,od程序可以读取写入管道的三个字节,但是后续的读取会返回0字节,表明文件的结束。当读取返回0时,od程序退出。这与在一个终端上运行od命令相类似,然而后者是按下Ctrl+D来向od命令表明文件结束。
图13-3表明了pipe调用之后的操作序列,图13-4表明了fork调用之后的操作序列,而图13-5表示程序已准备好来传输数据。