linux的IPC进程通讯方式-匿名管道(一)

linux的IPC进程通讯-匿名管道

什么是管道

若是你使用过Linux的命令,那么对于管道这个名词你必定不会感受到陌生,由于咱们一般经过符号"|"来使用管道,可是管道的真正定义是什么呢?管道是一个进程链接数据流到另外一个进程的通道,它一般是用做把一个进程的输出经过管道链接到另外一个进程的输入。
举个例子,在shell中输入命令:ls -l | grep string,咱们知道ls命令(其实也是一个进程)会把当前目录中的文件都列出来,可是它不会直接输出,而是把原本要输出到屏幕上的数据经过管道输出到grep这个进程中,做为grep这个进程的输入,而后这个进程对输入的信息进行筛选,把存在string的信息的字符串(以行为单位)打印在屏幕上。php

使用popen函数

1.popen函数和pclose函数介绍,函数原型以下(C语言)linux

#include <stdio.h>  
FILE* popen (const char *command, const char *open_mode);  
int pclose(FILE *stream_to_close);

poen函数容许一个程序将另外一个程序做为新进程来启动,并能够传递数据给它或者经过它接收数据。command是要运行的程序名和相应的参数。open_mode只能是"r(只读)"和"w(只写)"的其中之一。popen的函数返回值是FILE类型的指针,而Linux把一切都视为文件,也就是说咱们可使用stdio I/O库中的文件处理函数来对其进行操做。shell

若是open_mode是"r",主调用程序就可使用被调用程序的输出,经过函数返回的FILE指针,就能够能过stdio函数(如fread)来读取程序的输出;若是open_mode是"w",主调用程序就能够向被调用程序发送数据,即经过stdio函数(如fwrite)向被调用程序写数据,而被调用程序就能够在本身的标准输入中读取这些数据。数组

pclose函数用于关闭由popen建立出的关联文件流。pclose只在popen启动的进程结束后才返回,若是调用pclose时被调用进程仍在运行,pclose调用将等待该进程结束。它返回关闭的文件流所在进程的退出码。swoole

二、例子
不少时候,咱们根本就不知道输出数据的长度,为了不定义一个很是大的数组做为缓冲区,咱们能够以块的方式来发送数据,一次读取一个块的数据并发送一个块的数据,直到把全部的数据都发送完。下面的例子就是采用这种方式的数据读取和发送方式。源文件名为popen.c,代码以下:并发

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

int main()
{
    FILE *read_fp = NULL;
    FILE *write_fp = NULL;
    char buffer[BUFSIZ + 1];
    int chars_read = 0;
    
    //初始化缓冲区
    memset(buffer, '\0', sizeof(buffer));
    //打开ls和grep进程
    read_fp = popen("ls -l", "r");
    write_fp = popen("grep rwxrwxr-x", "w");
    //两个进程都打开成功
    if(read_fp && write_fp)
    {
        //读取一个数据块
        chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        while(chars_read > 0)
        {
            buffer[chars_read] = '\0';
            //把数据写入grep进程
            fwrite(buffer, sizeof(char), chars_read, write_fp);
            //还有数据可读,循环读取数据,直到读完全部数据
            chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        }
        //关闭文件流
        pclose(read_fp);
        pclose(write_fp);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}

执行编译composer

gcc -o popen popen.c

运行结果以下函数

[root@test test]# ./popen              
-rw-r--r--  1 admin admin    73231 Mar 24 15:52 composer.lock
-rw-r--r--  1 root  root       745 Mar 29 09:59 c_read.c
-rw-r--r--  1 root  root       773 Mar 29 09:58 c_write.c
-rw-r--r--  1 admin admin      294 Mar 24 18:46 fork2.php
-rw-r--r--  1 admin admin      225 Mar 24 17:48 fork3.php
-rw-r--r--  1 root  root      1028 Mar 29 11:39 popen.c
-rw-r--r--  1 root  root    566294 Mar 16 15:41 swoole-1.9.8.tgz

从运行结果来看,达到了信息筛选的目的。程序在进程ls中读取数据,再把数据发送到进程grep中进行筛选处理,至关于在shell中直接输入命令:ls -l | grep rwxrwxr-x。学习

三、popen的实现方式及优缺点
当请求popen调用运行一个程序时,它首先启动shell,即系统中的sh命令,而后将command字符串做为一个参数传递给它。ui

这样就带来了一个优势和一个缺点。优势是:在Linux中全部的参数扩展都是由shell来完成的。因此在启动程序(command中的命令程序)以前先启动shell来分析命令字符串,也就可使各类shell扩展(如通配符)在程序启动以前就所有完成,这样咱们就能够经过popen启动很是复杂的shell命令。

而它的缺点就是:对于每一个popen调用,不只要启动一个被请求的程序,还要启动一个shell,即每个popen调用将启动两个进程,从效率和资源的角度看,popen函数的调用比正常方式要慢一些。

pipe调用

若是说popen是一个高级的函数,pipe则是一个底层的调用。与popen函数不一样的是,它在两个进程之间传递数据不须要启动一个shell来解释请求命令,同时它还提供对读写数据的更多的控制。

pipe函数的原型以下:

#include <unistd.h>  
int pipe(int file_descriptor[2]);

咱们能够看到pipe函数的定义很是特别,该函数在数组中墙上两个新的文件描述符后返回0,若是返回返回-1,并设置errno来讲明失败缘由。

数组中的两个文件描述符以一种特殊的方式链接起来,数据基于先进先出的原则,写到file_descriptor[1]的全部数据均可以从file_descriptor[0]读回来。因为数据基于先进先出的原则,因此读取的数据和写入的数据是一致的。

特别提醒:
一、从函数的原型咱们能够看到,它跟popen函数的一个重大区别是,popen函数是基于文件流(FILE)工做的,而pipe是基于文件描述符工做的,因此在使用pipe后,数据必需要用底层的read和write调用来读取和发送。

二、不要用file_descriptor[0]写数据,也不要用file_descriptor[1]读数据,其行为未定义的,但在有些系统上可能会返回-1表示调用失败。数据只能从file_descriptor[0]中读取,数据也只能写入到file_descriptor[1],不能倒过来。

例子:
首先,咱们在原先的进程中建立一个管道,而后再调用fork建立一个新的进程,最后经过管道在两个进程之间传递数据。源文件名为pipe.c,代码以下:

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

int main()
{
    int data_processed = 0;
    int filedes[2];
    const char data[] = "Hello pipe!";
    char buffer[BUFSIZ + 1];
    pid_t pid;
    //清空缓冲区
    memset(buffer, '\0', sizeof(buffer));

    if(pipe(filedes) == 0)
    {
        //建立管道成功
        //经过调用fork建立子进程
        pid = fork();
        if(pid == -1)
        {
            fprintf(stderr, "Fork failure");
            exit(EXIT_FAILURE);
        }
        if(pid == 0)
        {
            //子进程中
            //读取数据
            data_processed = read(filedes[0], buffer, BUFSIZ);
            printf("Read %d bytes: %s\n", data_processed, buffer);
            exit(EXIT_SUCCESS);
        }
        else
        {
            //父进程中
            //写数据
            data_processed = write(filedes[1], data, strlen(data));
            printf("Wrote %d bytes: %s\n", data_processed, data);
            //休眠2秒,主要是为了等子进程先结束,这样作也只是纯粹为了输出好看而已
            //父进程其实没有必要等等子进程结束
            sleep(2);
            exit(EXIT_SUCCESS);
        }
    }
    exit(EXIT_FAILURE);
}

运行结果

[root@test test]# gcc -o pipe pipe.c   
[root@test test]# ./pipe                 
Wrote 11 bytes: Hello pipe!
Read 11 bytes: Hello pipe!

可见,子进程读取了父进程写到filedes[1]中的数据,若是在父进程中没有sleep语句,父进程可能在子进程结束前结束,这样你可能将看到两个输入之间有一个命令提示符分隔。

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

下面来介绍一种用管道来链接两个进程的更简洁方法,咱们能够把文件描述符设置为一个已知值,通常是标准输入0或标准输出1。这样作最大的好处是能够调用标准程序,即那些不须要以文件描述符为参数的程序。
为了完成这个工做,咱们还须要两个函数的辅助,它们分别是dup函数或dup2函数,它们的原型以下

#include <unistd.h>  
int dup(int file_descriptor);  
int dup2(int file_descriptor_one, int file_descriptor_two);

dup调用建立一个新的文件描述符与做为它的参数的那个已有文件描述符指向同一个文件或管道。对于dup函数而言,新的文件描述老是取最小的可用值。而dup2所建立的新文件描述符或者与int file_descriptor_two相同,或者是第一个大于该参数的可用值。因此当咱们首先关闭文件描述符0后调用dup,那么新的文件描述符将是数字0.

例子
在下面的例子中,首先打开管道,而后fork一个子进程,而后在子进程中,使标准输入指向读管道,而后关闭子进程中的读管道和写管道,只留下标准输入,最后调用execlp函数来启动一个新的进程od,可是od并不知道它的数据来源是管道仍是终端。父进程则相对简单,它首先关闭读管道,而后在写管道中写入数据,再关闭写管道就完成了它的任务。源文件为pipe2.c,代码以下:

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

int main()
{
    int data_processed = 0;
    int pipes[2];
    const char data[] = "123";
    pid_t pid;

    if(pipe(pipes) == 0)
    {
        pid = fork();
        if(pid == -1)
        {
            fprintf(stderr, "Fork failure!\n");
            exit(EXIT_FAILURE);
        }
        if(pid == 0)
        {
            //子进程中
            //使标准输入指向fildes[0]
            close(0);
            dup(pipes[0]);
            //关闭pipes[0]和pipes[1],只剩下标准输入
            close(pipes[0]);
            close(pipes[1]);
            //启动新进程od
            execlp("od", "od", "-c", 0);
            exit(EXIT_FAILURE);
        }
        else
        {
            //关闭pipes[0],由于父进程不用读取数据
            close(pipes[0]);
            data_processed = write(pipes[1], data, strlen(data));
            //写完数据后,关闭pipes[1]
            close(pipes[1]);
            printf("%d - Wrote %d bytes\n", getpid(), data_processed);
        }
    }
    exit(EXIT_SUCCESS);
}
[root@cp01-mawmd-rd03 test]# ./pipe2 
16546 - Wrote 3 bytes
[root@cp01-mawmd-rd03 test]# 0000000   1   2   3
0000003

从运行结果中能够看出od进程正确地完成了它的任务,与在shell中直接输入od -c和123的效果同样。

关于管道关闭后的读操做的讨论

如今有这样一个问题,假如父进程向管道file_pipe[1]写数据,而子进程在管道file_pipe[0]中读取数据,当父进程没有向file_pipe[1]写数据时,子进程则没有数据可读,则子进程会发生什么呢?再者父进程把file_pipe[1]关闭了,子进程又会有什么反应呢?

当写数据的管道没有关闭,而又没有数据可读时,read调用一般会阻塞,可是当写数据的管道关闭时,read调用将会返回0而不是阻塞。注意,这与读取一个无效的文件描述符不一样,read一个无效的文件描述符返回-1。

6、匿名管道的缺陷

看了这么多相信你们也知道它的一个缺点,就是通讯的进程,它们的关系必定是父子进程的关系,这就使得它的使用受到了一点的限制,可是咱们可使用命名管道来解决这个问题。命名管道将在下一篇文章:Linux进程间通讯——使用命名管道中介绍。

引用: 本文转载自http://blog.csdn.net/ljianhui/article/details/10168031 仅供学习使用记录

相关文章
相关标签/搜索