Linux进程间通信(三)——管道(非命名管道)【转】

第13章 Linux进程间通信——管道 
      在第11章,我们了解了使用信号在两个进程之间发送消息的一个简单方法。我们创建了可以用来引起响应的通知事件,但是所传递的信息限制于一个信号数量。
      在这一章,我们将会了解管道,这会允许在进程之间交换更为有用的数据。在本章的结尾,我们将会使用我们的新知识来重新将CD数据库程序实现为一个非常简单的客户/服务器程序。
      在本章,我们将会涉及下列主题:
      一、管道的定义
      二、处理管道
      三、管道调用
      四、父子进程
(以下部分本文忽略)
      五、有名管道:FIFO
      六、客户端/服务器方法
 
一、管道的定义 
      当我们由一个进程到另一个进程连接数据流时我们使用术语管道。通常我们将一个进程的输出连接到另一个进程的输入。
      大多数Linu用户已经很熟悉将shell命令连接到一起的思想,从而一个进程的输入可以直接连接到另一个进程的输入。对于shell命令,这是用下面的方式来输入的:
      cmd1 | cmd2
 
      shell安排标准输入与两个命令的输出,从而
      cmd1的标准输入来自由终端键盘
      cmd1的标准输出作为标准输入连接cmd2
      cmd2的标准输出连接到终端屏幕
 
      实际上,shell所完成的工作就是重新连接标准输入与输出流,从而由键盘输入的数据流可以通过两个命令,然后输出到屏幕。下图是这个过程的一个可视化表示。
      在这一章,我们将会了解如何在一个程序中实现这种效果,以及我们如何使用管道来连接多个进程从而使得我们实现一个简单的客户端/服务器系统。
 
二、处理管道 
      也许在两个程序之间传递数据最简单的方法就是使用popen与pclose函数了。这两个函数原型如下:
 
       #include <stdio.h>  //通常在这里==> /usr/include/stdio.h
 
FILE *popen(const char *command, const char *open_mode);
      popen函数允许一个程序调用另一个程序作为一个新的进程,并且或者向其发送数据或者由其接收数据。command字符串是要运行的程序名字及其参数。open_mode必须或者是"r"或者是"w"。
      如果open_mode是"r",由被调用程序的输出可以为调用程序所用,并且可以使用通常的stdio库函数中用于读取的函数由popen函数所返回的文件流 FILE *中进行读取。然而,如果open_mode是"w",程序可以使用fwrite调用向被调用的程序发送数据。被调用的程序可以由其标准输入读取数据。通常,正被调用的程序并不会知道他正在由另一个进程读取数据;他只是简单的读取其标准输入流。
      popen调用必须指明是"r"或者是"w";在popen的标准实现中并不支持其他的选项。这就意味着我们不能调用另一个程序,同时由其读取并向其写入。一旦失败,popen就会返回一个空指针。如果我们希望使用管道进行双向通信,通常的解决方法是使用两个管道,每个方向用于一个数据流。
 
int pclose(FILE *stream_to_close);
      当由popen所启动的进程已经完成时,我们可以使用pclose来关闭与其相关的文件流。pclose调用只会在由popen所启动的进程完成时才会返回。如果当调用pclose时,这个进程仍在运行,pclose调用就会等待这个进程结束。
      pclose调用通常返回他正关闭的进程的文件流的返回的代码。如果调用在调用pclose之前已经执行了一个wait语句,返回状态就会丢失,而pclose就会返回-1,并且将errno设置为ECHILD。
 
(1) 试验--读取另一个程序作为输入
      下面我们来试验一个简单的popen与pclose的例子,popen1.c。我们会在一个程序中使用popen来访问uname所输出的信息。uname -a命令输出系统信息,包括机器类型,OS名字,版本号以及机器的网络名。
      在初始化程序之后,我们打开一个连接uname的管道,使其可读并且设置read_fp指向输出。最后,由read_fp指向的管道会被关闭。
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
int main() {
    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(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}
 
      当我们运行这个程序,我们就会得到下面的输出:
 
$ ./popen1
Output was:-
Linux gw1 2.4.20-8 #1 Thu Mar 13 17:54:28 EST 2003 i686 i686 i386 GNU/Linux
 
工作原理 
      这个程序使用popen调用来调用带有-a参数的uname命令。他然后使用所返回的文件流来读取直到BUFSIZ个字符然后输出到屏幕上。因为我们在一个程序内部捕获uname输出的,所以他可以用来处理。
      向popen发送输出 
      现在我们已经看到由一个外部程序捕获输出的例子,下面我们来看一下向另一个程序发送输出。这就是popen2.c,将数据导向另一个管道。在这里我们将使用od(octal dump)。
 
(2) 试验--向外部程序发送输出 
      看一下下面的代码,如果我们愿意可以将其输入文本编辑器。
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
int main() {
    FILE *write_fp;
    char buffer[BUFSIZ +1];
 
    sprintf(buffer,"Once upon a time, there was.../n");
    write_fp = popen("od -c","w");
    if(write_fp != NULL) {
        fwrite(buffer,sizeof(char),strlen(buffer),write_fp);
        pclose(write_fp);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}
 
      当我们运行这个程序时,我们会得到下面的输出:
 
$ ./popen2
0000000   O n c e   u p o n   a   t i  m e
0000020   ,   t h e r e   w a s . . . /n
0000037
 
工作原理 
      这个程序使用带有"w"参数的popen调用来启动od -c命令,从而他可以向这个命令发送数据。然后他发送一个字符串,od -c命令可以接收并处理;od -c命令然后在其标准输出上输出其处理结果。
      在命令行上,我们可以使用下面的命令来得到同样的结果
 
$ echo “Once upon a time, there was...” | od -c
 
(3) 传递更多的数据 
      到目前为止我们所用的机制只是简单的在一个fread或是fwrite中发送或是接收全部的数据。有时我们也许以更小的尺寸发送数据,或是也许我们并不知道输出的大小。为了避免声明一个大的缓冲区,我们可以使用多个fread或是fwrite调用并分别处理这些数据。
      下面是一个程序,popen3.c,由一个管道中读取所有的数据。
 
试验-- 大量读取另一个程序作为输入
      在这个程序中,我们由一个被调用的ps -alx进程读取数据。并没有办法预先知道会有多少输出,所以我们必须由管道中多次读取。
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
int main() {
    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) {
            buffer[chars_read - 1] = '\0';
            printf("Reading:-\n %s\n",buffer);
            chars_read = fread(buffer,sizeof(char),BUFSIZ,read_fp);
        }
        pclose(read_fp);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}
 
      我们得到的输出结果如下:
 
$ ./popen3
Reading:-
    PID TTY STAT  TIME COMMAND
     1 ? S       0:04 init
     2 ? SW      0:00 [kflushd]
     3 ? SW      0:00 [kpiod]
     4 ? SW      0:00 [kswapd]
     5 ? SW<     0:00 [mdrecoveryd]
...
  240 tty2 S     0:02 emacs draft1.txt
Reading:-
  368 tty1 S     0:00 ./popen3
  369 tty1 R     0:00 ps -ax
...
 
工作原理 
      这个程序以与popen1.c相类似的方法使用"r"参数调用popen。但是这一次,程序会继续由文件流中读取数据,直到没有更多的数据。注意,尽管ps命令的执行需要一些时间,Linux会安排进程调度,从而在可能时两个程序同时运行。如果读进程,popen3,没有输入数据,他会挂起,直到有输入数据。如果写进程,ps,产生了更多的可缓冲的数据,他就会挂起,直到读进程消化掉其中的一些数据。
 
 
(4) popen是如何实现的 
       popen调用通过首先调用shell,sh,将command字符串作为参数传递给他来运行我们所请求的程序。这有两个效果,一个是好的,而另一个并不是这样好。
      在Linux(同时在所有的类Unix系统)中,所有的参数扩展都是通过shell来完成的,所以在程序被调用之前调用shell来分析命令字符串会允许在程序启动之前完成任意的shell扩展,例如,确定'*.c'实际指向哪些文件。这通常是非常有用的,而且这允许使用popen来启动复杂的shell命令。其他的进程创建函数,例如execl,其调用可以更为复杂,因调用进程必须执行其自己的shell扩展。
      使用shell的另一个效果就是 对于每一个popen调用,必须为所请求的程序调用一个shell。从而每一个popen调用就会另外两个额外进程,这就会使得popen函数就系统资源而言更为昂贵,而且目标命令的调用速度更慢
 
      下面是程序popen4.c,我们可以模拟popen的行为。我们通过cat文件来计算所有popen例子源程序的行数,然后通过管道输出给wc -l,后者会计算这些行数。在命令行,等同的命令如下:
 
$ cat popen*.c | wc -l
      实际上,wc -l poen*.c命令输入更为简单而且更为高效,但是这个例子是为了演示这些原则。
 
试验--popen启动一个Shell 
      这个程序实际上使用前面的命令,但是通过popen可以读取结果:
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
int main() {
    FILE *read_fp;
    char buffer[BUFSIZ +1];
    int chars_read;
 
    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);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}
 
      当我们运行这个程序,我们会得到下面的输出:
 
Reading:-
 98
 
工作原理 
      这个程序证明了被调用的shell将popen*.c扩展为所有以popen开头并以.c结束的文件列表,同时处理管道符号,并将cat的输入传递给wc。我们在一个popen调用中调用shell,cat程序以及wc程序,并将结果重定向。调用命令的程序只看到最终的结果。
 
三、管道调用
 
      我们已经了解了高层的popen函数,现在我们继续来了解低层的pipe函数。这个函数提供了一个在两个函数之间传递数据的方法,而不必调用shell来解释所请求的命令的。同时他也为我们提供了更多的数据读写控制。pipe函数的原型如下:
 
      #include <unistd.h>
      int pipe(int file_descriptor[2]);
 
      pipe函数接受一个两个整数文件描述符的数组作为参数。他会使用两个新的文件描述符来填充这个数据并且返回一个零值。如果失败则会返回-1并且设置errno来指明失败原因。Linux手册页中所定义的错误如下:
 
      EMFILE:进程正在使用的文件描述符过多
      ENFILE:系统文件表已满
      EFAULT:文件描述符不可用
 
      返回的两个文件描述符以一个特殊的方式相连接。任何写入file_descriptor[1]的数据可以由file_descriptor[0]中读取。数据以先进先出的方式进行处理,通常简记为FIFO。这就意味着如果我们向file_descriptor[1]中写入字节1,2,3,那么由file_descriptor[0]中的读取就会产生1,2,3。这与堆栈不同,后者以后进先出的方式进行操作,通常简记为LIFO。
      注意:在这里我们要清楚,这些是文件描述符,而不是文件流,所以我们必须使用低层的read与write调用来访问数据,而不是fread与fwrite。
 
      下面的程序,pipe1.c,使用pipe来创建一个管道。
 
试验--pipe函数
 
#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];
 
    memset(buffer,'/0',sizeof(buffer));
 
    if(pipe(file_pipes) == 0) {
        data_processed = write(file_pipes[1],some_data,strlen(some_data));
        printf("Wrote %d bytes/n",data_processed);
        data_processed = read(file_pipes[0],buffer,BUFSIZ);
        printf("Read %d bytes: %s/n",data_processed,buffer);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}
 
      当我们运行这个程序,我们会得到下面的输出:
 
$ ./pipe1
Wrote 3 bytes
Read 3 bytes: 123
 
工作原理 
      这个程序使用两个文件描述符file_pipes[]来创建一个管道。然后他使用文件描述符file_pipes[1]向管道中写入数据,并且由file_pipes[0]中读取。注意管道有内部缓冲,从而可以在两个调用write与read之间存储数据。
      我们应该清楚尝试使用file_pipes[0]写入数据,或是使用file_pipes[1]读取数据的效果是未定义的,所以这样的行为结果会非常奇怪,并且也许会毫无警告的发生改变。在作者的系统上,这样的调用会失败并且返回-1,这至少保证了是很容易捕获这个错误的。
      粗看起来这个管道的例子似乎并没有为我们提供任何我们使用一个简单的文件不能完成的任务。管道的真正优点在我们希望在两个进程之间传递数据时才会体现出来。正如我们在第12章所看到的,当一个程序使用fork调用创建一个新进程时,以前打开的文件描述符会保持打开。通过在原始进程中创建一个管道,然后通过fork创建一个新进程,我们可以由一个进程向另一个进程传递数据。
 
试验--使用fork的管道 
 
// 1 这是pipe2.c。这个程序的开始部分与第一个例子类似,直到我们调用fork。
#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 == -1) {
                fprintf(stderr,"Fork failure");
                exit(EXIT_FAILURE);
            }
 
            //2 我们已经保证fork正常工作,所以如果fork_result等于0,我们是在子进程中:
            if(fork_result == 0) {
                data_processed = read(file_pipes[0],buffer,BUFSIZ);
                printf("Read %d bytes: %s/n",data_processed,buffer);
                exit(EXIT_SUCCESS);
            }
 
            // 3 否则,我们一定在父进程中:
            else {
                data_processed = write(file_pipes[1],some_data,strlen(some_data));
                printf("Wrote %d bytes/n",data_processed);
            }
        }
        exit(EXIT_SUCCESS);
}
 
      当我们运行这个程序,我们得到与前面一样的输出:
 
$ ./pipe2
Wrote 3 bytes
Read 3 bytes: 123
 
      我们也许会发现实际上命令提示符会在输出的最后部分之前出现,但是我们在这里已经对输出进行整理从而使其更易于阅读。
 
工作原理 
      首先这个程序使用pipe调用创建一个管道。然后他使用fork调用来创建一个新的进程。如果fork调用成功,父进程就会向管道写入数据,而子进程由管道中读取数据。父子进程都会在一个简单的write与read之后退出。如果父进程在子进程之前退出,我们就会看到shell提示符出现在两个输出之间。
      尽管这个程序与前面的管道例子十分相似,但是我们已经通过使用单独的进程用于读写而向前进了一大步.
 
四、父子进程
      我们的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命令表明文件结束。
*************************************************************************************************************************************

总结本文:

1. 使用信号(回忆另一篇文章):
       使用信号(signal),只能传递系统定义(/usr/include/signal.h)的有限的“信号(signal)”;

2. 使用管道(本文内容。注:本文并没介绍命名管道):
popen() & pclose(): 
       会启动新的shell进程作为管道;
pipe(int file_descriptor[2]):
        不必创建新的进程,父进程通过传参的方式向子进程传递数据 —— (void) execl("pipe4", "pipe4", buffer, (char *)0);
        注:写入file_descriptor[1],从file_descriptor[0]读取
dup(int file_desciptor):
        同pipe()的方式,使用execl()启动子进程,但是子进程通过接收标准输入(文件描述符0)的方式接收父进程传递的数据

 

总结本文:

1. 使用信号(回忆另一篇文章):
       使用信号(signal),只能传递系统定义(/usr/include/signal.h)的有限的“信号(signal)”;

2. 使用管道(本文内容。注:本文并没介绍命名管道):
popen() & pclose(): 
       会启动新的shell进程作为管道;
pipe(int file_descriptor[2]):
        不必创建新的进程,父进程通过传参的方式向子进程传递数据 —— (void) execl("pipe4", "pipe4", buffer, (char *)0);
        注:写入file_descriptor[1],从file_descriptor[0]读取
dup(int file_desciptor):
        同pipe()的方式,使用execl()启动子进程,但是子进程通过接收标准输入(文件描述符0)的方式接收父进程传递的数据

猜你喜欢

转载自chuanwang66.iteye.com/blog/1997277