pipe

管道(pipe)

管道是一种最基本的IPC机制, 作用于有血缘关系的进程之间通信, 完成数据传递.

本质:
  伪文件, 实质为内核缓冲区不占用磁盘空间

特点:
  两部分:
    读端, 写端, 对应两个文件描述符
    数据写端流入, 读端流出
  操作管道的进程被销毁之后, 管道自动占用的存储空间自动被释放
  管道默认阻塞, 读写都为阻塞

原理:
  内部实现方式: 环形队列
  缓冲区大小默认为4k, 大小会根据时间情况做适当调整

管道的局限性:
  自己先读数据时, 不能自己写, 阻塞
  自己先写数据时, 可以自己读
  数据自己读不能自己写? 有些疑问, 阻塞了?
  数据一旦被读走之后, 便不在管道中存在, 不可反复读取
  半双工的通信方式. 数据只能在一个方向上流动
  只能在公共祖先的进程间使用管道

管道的读写行为

使用管道需要注意以下4中特殊情况(假设都是阻塞I/O操作, 没有设置O_NONBLOCK标志):
(1)如果所有指向管道写端的文件描述符都关闭(管道写端的引用计数为0), 而仍然有进程从管道的读端读数据, 那么管道中剩余的数据都被读取后, 再次read会返回0, 就像读到文件末尾一样
(2)如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0), 而持有管道写端的进程也没有向管道中写数据, 这时有进程从管道读端读数据, 那么管道中剩余的数据都被读取后, 再次read会阻塞, 直到管道中有数据可读才读取数据并返回
(3)如果所有指向管道读端的文件描述符都关闭(管道读端引用计数为0), 而持有管道写端的进程也没有向管道中写数据, 这时有进程向管道的写端write, 那么该进程会收到信号SIGPIPE, 通常会导致进程异常终止. 当然可以对SIGPIPE信号进行捕捉, 不终止进程
(4)如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0), 而持有管道读端的进程也没有从管道中读数据, 这时有进程向管道写端写数据, 那么在管道被写满时再次write会阻塞, 直到管道中有空位置了才写入数据

总结
  读操作:
    有数据: read(fd), 正常读, 返回读出的字节数
    无数据: (1) 写端全部关闭, read解除阻塞, 返回0, 相当于读文件到尾部; (2) 没有全部关闭, read阻塞
  写操作
    读端全部关闭: 管道破裂, 进程被终止, 内核给当前进程发送SIGPIPE
    读端没有全部关闭: (1) 缓冲区写满, write阻塞; (2) write继续写

管道的优劣

优点: 简单, 相比信号和套接字实现进程间通信简单很多
缺点:

  1. 只能单向通信, 双向通信需要建立两个管道
  2. 只能用于父子, 兄弟(有共同祖先)间通信. 可使用命名管道fifo解决

基础API

pipe

头文件: #include <unistd.h>

int pipe(int pipefd[2]);
函数调用成功后返回r/w两个文件描述符. 无须open, 但需手动close. 规定fd[0]读, fd[1]写. 向文件读写数据其实是在读写内核缓冲区

管道创建成功以后, 创建该管道的进程(父进程)同时掌握着管道的读端和写端. 采用以下步骤实现父子进程间的通信:
(1)父进程调用pipe函数创建管道, 得到两个文件描述符fd[0]和fd[1]指向管道的读端和写端
(2)父进程调用fork创建子进程, 那么子进程也有两个文件描述符指向同一管道
(3)父进程关闭管道读端, 子进程关闭管道写端. 父进程可以像管道中写入数据, 子进程将管道中的数据读出. 由于管道是利用环形队列实现的, 数据从写端流入管道, 从读端流出, 这样就实现了进程间通信

父子进程使用管道通信, 思考:
  父子进程间通信是否需要sleep函数?
    父进程写的慢, 子进程读的快

如何设置非阻塞?
  默认读写两端都阻塞
  设置读端为非阻塞pipe(fd)
  fcntl-变参函数: (1) 赋值文件描述符; (2) 修改文件属性-对应open时的flag属性
  设置方法:

int flags = fcntl(fd[0], F_GETFL);
// 设置新的flags
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);

fpathconf与pathconf

头文件: #include <unistd.h>

long fpathconf(int fd, int name);
long pathconf(char *path, int name);

查看管道缓冲区大小:
  命令: ulimit -a
  函数: fpathconf

示例程序

自己读写阻塞情况

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>

int main(void) {
    int fd[2];
    char buf[10];
    char p[] = "haha";

    int ret = pipe(fd);
    if (ret == -1) {
        perror("pipe error");
        exit(1);
    }

    printf("pipe[0] = %d\n", fd[0]);
    printf("pipe[1] = %d\n", fd[1]);

    // 先读, 阻塞
    //ret = read(fd[0], buf, sizeof(buf));

    write(fd[1], p, sizeof(p));

    // 后写, 没有被阻塞
    ret = read(fd[0], buf, sizeof(buf));

    printf("ret = %d, %s, sizeof(buf)=%d\n", ret, buf, sizeof(buf));

    close(fd[0]);
    close(fd[1]);

    return 0;
}

/*
pipe[0] = 3
pipe[1] = 4
ret = 5, haha, sizeof(buf)=10
*/

管道执行ps和grep命令

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>

int main(int argc, const char* argv[]) {
    int fd[2];
    int ret = pipe(fd);
    if (ret == -1) {
        perror("pipe error");
        exit(1);
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork error");
        exit(1);
    }

    // 父进程: ps -aux
    if (pid > 0) {
        // 写管道操作, 关闭读端
        close(fd[0]);
        // 文件描述符重定向, STDOUT_FILENO, 
        dup2(fd[1], STDOUT_FILENO);
        // 执行ps aux
        execlp("ps", "ps",  "aux", NULL);
        perror("excelp");
        exit(1);
    }
    // 子进程: grep "bash"
    else if (pid == 0) {
        close(fd[1]);
        dup2(fd[0], STDIN_FILENO);
        execlp("grep", "grep", "bash", "--color=auto", NULL);
    }

    printf("pipe[0] = %d \n", fd[0]);
    printf("pipe[1] = %d \n", fd[1]);

    close(fd[0]);
    close(fd[1]);

    return 0;
}

进程通信

兄弟进程间通信, 父进程用于回收子进程

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc, const char* argv[]) {
    int fd[2];
    int ret = pipe(fd);
    if (ret == -1) {
        perror("pipe error");
        exit(1);
    }
    
    int i = 0;
    for (i = 0; i < 2; i++) {
        pid_t pid = fork();
        if (pid == 0)
            break;
        if (pid == -1) {
            perror("fork error");
            exit(1);
        }
    }

    // 子进程1: ps -aux
    if (i == 0) {
        // 写管道操作, 关闭读端
        close(fd[0]);
        // 文件描述符重定向, STDOUT_FILENO, 
        dup2(fd[1], STDOUT_FILENO);
        // 执行ps aux
        execlp("ps", "ps",  "aux", NULL);
        perror("excelp");
        exit(1);
    }
    // 子进程2: grep "bash"
    else if (i == 1) {
        close(fd[1]);
        dup2(fd[0], STDIN_FILENO);
        execlp("grep", "grep", "bash", "--color=auto", NULL);
    }
    // 父进程回收子进程
    else if (i == 2) {
        close(fd[0]);
        close(fd[1]);
        wait(NULL);  
        // 回收子进程
        pid_t wpid;
        while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) {
            if (wpid == 0) 
                continue;
            printf("child die pid = %d\n", wpid);
        }
    }

    printf("pipe[0] = %d \n", fd[0]);
    printf("pipe[1] = %d \n", fd[1]);

    return 0;
}

查看管道缓冲区

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>

int main(int argc, const char* argv[]) {
    int fd[2];
    int ret = pipe(fd);
    if (ret == -1) {
        perror("pipe error");
        exit(1);
    }

    // 测试管道缓冲区大小
    long size = fpathconf(fd[0], _PC_PIPE_BUF);
    printf("size = %ld\n", size); 
        
    printf("pipe[0] = %d \n", fd[0]);
    printf("pipe[1] = %d \n", fd[1]);

    close(fd[0]);
    close(fd[1]);

    return 0;
}
posted @ 2019-04-19 21:18  张飘扬  阅读(1442)  评论(0编辑  收藏  举报