Linux系统编程——管道

管道的概述

  管道也叫无名管道,他是UNIX系统IPC的最古老形式,所有的UNIX系统都支持这种通信机制。作用与有血缘关系的进程之间,完成数据传递。

管道的特征

管道有如下特点

  1. 管道的本质是一个伪文件,不属于某个文件系统(实际为内核缓冲区)
  2. 写入管道的数据遵循先入先出的规则
  3. 管道所传送的数据是无格式的,这要求管道的读出写入方必须实现约定好数据的格式,如多少字节算一个消息等。

管道的原理管道实为内核使用环形队列机制,借助内核缓冲区(4K)实现

管道的局限性:

  1. 数据不能进程自己写,自己读
  2. 从管道读操作时一次性操作,数据一旦被读走,他就从管道中被抛弃 ,释放更多空间写更多的数据
  3. 管道为为双向半双工,数据在同一时刻只能在一个方向上流动
  4. 管道没有名字,只能在具有血缘关系的进程之间使用

无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。

常见的通信方式有:单工通信、半双工通信、全双工通信。

管道的基本用法

pipe函数

函数原型:int pipe(int pipefd[2]);
作用:创建匿名管道

头文件:

  • #include <unistd.h>

参数:

  • pipefd: 为 int 型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。

  当一个管道建立时,它会创建两个文件描述符 fd[0]fd[1]。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道。一般文件 I/O 的函数都可以用来操作管道( lseek() 除外)。

返回值:

  • 成功:0
  • 失败:-1

  使用pipe函数创建的管道默认都是打开的,无需open,但需要手动close。规定 fd[0] -> r
fd[1] -> w。向管道文件进行读写数据其实就是在读写内核缓冲区。

案例

下面我们写这个一个例子,子进程通过无名管道给父进程传递一个字符串数据:

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

int main(int argc, char *argv[]){
    pid_t pid;
    int fd[2];
    int ret = pipe(fd); //创建管道
    if(ret == -1){
        perror("pipe error");
        exit(1);
    }   
    pid = fork();
    if(pid == -1){
        perror("fork error");
        exit(1);
    } else if(pid == 0) { 
        close(fd[1]);//在子进程内关闭写端
        char buf[1024];
        int n = read(fd[0], buf, sizeof(buf));//从fd[0]中读取数据到 buf 缓冲区中
        write(STDOUT_FILENO, buf, n); //把读到的东西输出到屏幕上
        close(fd[0]);
    } else {
        close(fd[0]); //关闭读端
        char *str = "hello pipe\n";
        write(fd[1],str, strlen(str)); //向fd[1]里面写
        close(fd[1]);
    } 
    return 0;
}

程序执行结果如下
image.png

  在上述代码中有一个BUG,就是没有使用sleep 或者 wait 函数去保证 fork后是父进程先写,子进程在读,可是仍然完成可我们想要的形式,这就牵扯到了管道的读写行为

管道的读写行为

  每个管道只有一个页面作为缓冲区,该页面是按照环形缓冲区的方式来使用的。这种访问方式是典型的“生产者——消费者”模型。当“生产者”进程有大量的数据需要写时,而且每当写满一个页面就需要进行睡眠等待,等待“消费者”从管道中读走一些数据,为其腾出一些空间。相应的,如果管道中没有可读数据,“消费者” 进程就要睡眠等待,具体过程如下图所示:

默认的情况下,从管道中读写数据,最主要的特点就是阻塞问题,当管道里没有数据,另一个进程默认用 read() 函数从管道中读数据是阻塞的。

特点总结如下:

  • 读管道:
    • 管道中有数据,read返回实际读到的字节数
    • 管道中无数据:
        1. 管道写端全部被关闭,read 返回 0 (类似读到文件结尾)
        1. 管道写端没有全部被关闭,read 阻塞等待(此时会让出cpu)
  • 写管道:
    • 管道读端全部被关闭,进程异常终止(也可以使用SIGPIPE信号,使进程不终止)
    • 管道读端没有全部关闭
      • 1.管道以满,write 阻塞
        1. 管道未满, write 将数据写入,并返回实际写入的字节数

案例

使用管道实现父子进程间通信,完成 ls | wc - l 假定父进程实现 ls, 子进程实现 wc

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

int main(int argc, char *argv[]){
    pid_t pid;
    int fd[2];
    int ret = pipe(fd);
    if(ret == -1){
        perror("pipe error");
        exit(1);
    }   
    pid = fork();
    if(pid == -1){
        perror("fork error");
        exit(1);
    } else if(pid == 0) {
        close(fd[1]);
        dup2(fd[0], STDIN_FILENO); //将子进程的输入由屏幕数去重定向为从管道读取数据
        execlp("wc", "wc", "-l",NULL);//执行 wc -l 命令
        perror("execlp error");
    } else {
        close(fd[0]);
        dup2(fd[1], STDOUT_FILENO);//将输出由向屏幕输出重定向为向管道输出
        execlp("ls", "ls", NULL);
        perror("execlp error");
    }   

    return 0;
}

程序执行结果如下
image.png

  程序执行,发现程序执行结束 shell 还在阻塞等待用户输入,这是因为 shell -> fork -> ./ls_wc_l,程序 ls_wc_l 的子进程将 stdin 重定向给管道,父进程执行的 ls 会将结果集通过管道写给子进程。若父进程在子进程打印 wc 的结果到屏幕之前被 shell 调用 wait 回收,那么 shell 就会先输出 $ 提示符,也就是图片的情况。

  上述代码还有一个bug就是文件描述符没有被关闭,但是由于笔者目前水平不够,没有办法处理 在调用 exec族函数后关闭文件描述符的能力,后期学了信号在改吧。

兄弟进程间通信

使用管道实现兄弟进程间通信。兄执行:ls 命令,弟执行:wc -l 命令,父:等待回收子进程,要求使用循环创建多个子进程的方式创建兄弟进程。

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

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

    if(i == 0){ 
        close(fd[1]);
        dup2(fd[0], STDIN_FILENO);
        execlp("wc", "wc", "-l",NULL);
        perror("execlp error");
    } else if(i == 1) {
        close(fd[0]);
        dup2(fd[1], STDOUT_FILENO);
        execlp("ls", "ls", NULL);
        perror("execlp error");
    } else {
	close(fd[0]); //这里必须关闭,如果不关闭,不满足管道内的数据使单向流通的,具体看下图
	close(fd[1]);
        wait(NULL);
        wait(NULL);
    }   
    return 0;
}

image.png

image.png

管道缓冲区的大小

  可以使用 ulimit -a 命令查看当前系统中创建管道文件所对应的内核缓冲区大小,通常为:

pipe size (512 bytes, -p) 8

一些无聊的测试

只要保证管道内的数据是单向流动的,可以实现多个读端一个写端,也可以实现多个写端一个读端,或者多个读多个写,但是建议还是一对一,可以保障数据流动的稳定。

管道优劣

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

  • 只能单向通信,双向通信需要建立两个管道
  • 只能用于父子、兄弟(有公共祖先)进程间通信,后面可以使用FIFO有名管道解决。