进程间通信——管道(PIPE)

版权声明:本文为博主原创文章,转载请务必注明作者与原文链接。 https://blog.csdn.net/jingerppp/article/details/89380683

前言

管道是UNIX系统IPC 的最古老形式,所有UNIX系统都提供此种通信机制。通常说的管道为匿名管道(pipe),下一篇讲述命名管道(fifo)。

管道有以下两种局限性:

  1. 历史上,它们是半双工的。现在,某些系统提供全双工管道,但是为了最佳的移植性,我们决不应预先假定系统支持全双工管道。
  2. 管道只能在具有公共祖先的两个进程(父子进程或兄弟进程)之间使用。

它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

原型

#include <unistd.h>
int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

经由fd返回两个文件描述符:fd[0] 为读而打开,fd[1]为写而打开。fd[1] 输出是 fd[0]的输入。

说明

下图描绘了半双工管道的方法。强调数据需要通过内核在管道中流动。

单个进程中的管道几乎没有任何用处,通常进程会先调用pipe创建管道,接着调用fork,从而创建从父进程到子进程的IPC 通道,反之亦然。如下图:

fork之后做什么取决于想要的数据流向。对于从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1]),如下图:

扫描二维码关注公众号,回复: 5945137 查看本文章

反之,对于从子进程到父进程的管道,父进程关闭管道的写端(fd[1]),子进程关闭读端(fd[0])。

当管道的一端关闭后,下列两条规则起作用:

  • 当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。
  • 如果写(write)一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE。

实例

#include <stdio.h>
#include <string.h>
#include <unistd.h> //include this header for pipe

int main()
{
    printf("this is the main...\n");

    int fd[2]; //the pipe needs 2 fds.

    if (pipe(fd) < 0) {
        printf("create pipe failed.\n");
        goto exit;
    }
    char buff[20] = {0};

    pid_t pid = fork();

    if (pid < 0) {
        printf("fork error\n");
    } else if (pid == 0) {
        printf("this is child process.\n");

        close(fd[0]); //close read

        write(fd[1], "hello world", 12);
    } else {
        printf("this is parent process. pid = %d\n", pid);

        close(fd[1]); //close write
        read(fd[0], buff, 20);

        printf("read from child: %s, length: %d\n", buff, strlen(buff));
    }

exit:
    return 0;
}

首先通过pipe创建一个管道,在子进程中关闭了读端,在父进程中关闭了写端。描绘出子进程向父进程传输数据的流程。

运行结果:

./test 
this is the main...
this is parent process. pid = 5958
this is child process.
read from child: hello world, length: 11

popen 和 pclose

常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据,为此,标准I/O库提供了两个函数popen 和pclose。这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell 运行命令,然后等到命令终止。

#include <stdio.h>
FILE* popen(const char *command, const char *open_mode);
                                              返回值: 若成功,返回文件指针;若出错,返回NULL 

int pclose(FILE *stream_to_close);
                                      返回值:若成功,返回cmdstring的终止状态;若出错,返回-1

popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准的I/O文件指针。如果type是“r”,则文件指针连接到cmdstring的标准输出。如果type 是“w”,则文件指针连接到cmdstring的标准输入。

pclose()函数用于关闭由popen创建出的文件流,等待命令终止,然后返回shell的终止状态。如果shell 不能被执行,则pclose返回的终止状态与shell已执行exit(127)一样。pclose()只在popen启动的进程结束后才返回,如果调用pclose()时被调用进程仍在运行,pclose()调用将等待该进程结束。它返回关闭的文件流所在进程的退出码。

cmdstring 由Bourne shell以下列方式执行:

sh -c cmdstring

这表示shell将扩展cmdstring的任何特殊字符。例如,可以使用:

fp = popen("ls *.c", "r");

或者

fp = popen("cmd 2>&1", "r");

实例:

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

代码是查找权限是-rwxrwxr-x 的文件,并列出。结果如下:

-rwxrwxr-x 1 justinwei justinwei 9016 4月  18 16:34 test
-rwxrwxr-x 1 justinwei justinwei 8864 4月  18 16:51 test1

popen 优缺点:

当请求popen()调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它。

这样就带来了一个优点和一个缺点。

优点是:在Linux中所有的参数扩展都是由shell来完成的。所以在启动程序(command中的命令程序)之前先启动shell来分析命令字符串,也就可以使各种shell扩展(如通配符)在程序启动之前就全部完成,这样我们就可以通过popen()启动非常复杂的shell命令。

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

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

猜你喜欢

转载自blog.csdn.net/jingerppp/article/details/89380683