管道和FIFO概念以及相关函数(pipe、mkfifo)介绍

管道和FIFO

1.1管道
  • $ ls | wc -l 命令的运行过程:

    在这里插入图片描述

    1. 这里执行ls命令的进程把标准输出重定向到了管道的输入端,执行wc命令的进程把标准输入重定向到了管道的输出端,实际上这两个进程并不知道管道的存在,它只知道从标准输入和标准输出读取和写入数据,具体实现,后面说出来
    2. shell是父进程,它fork了两个同辈份的进程,分别执行ls和wc命令
  • 管道传输的是字节流:

    1. 字节流不存在消息边界
    2. 字节流是存在顺序的,读取的顺序和被写入的顺序是一致的
    3. 无法使用lseek来随机访问数据
  • 从管道中读取数据:

    1. 管道中不存在数据的时候,读取操作会阻塞直到存在一个字节的数据,如果写入端被关闭了,读取完数据之后,读取进程会收到一个文件结尾的标志
  • 管道是单向(半双工)的:写入端和读取端确定之后,通信方向也就确认了,不能在通信过程中修改

  • 多个进程写入同一个管道的时候,它们在一个时刻写入的数据量不超过PIPE_BUF字节,那么就可以确保写入的数据不会相互混合并且是原子操作(可能被信号打断),否则一个完整的数据可能会被分成几个片段,如果只存在一个写入进程,当然就没有这种PIPE_BUF的限制了,PIPE_BUF在不同的linux平台上大小不同,在linux上面是4096个字节

  • 管道的容量有限:管道其实是一个在内核中维护的缓冲器,一旦被填满,后续的写入操作就会被阻塞,其大小一般是65536字节

/*****************************
函数功能:创建一个管道,成功返回后,管道的写入端是pipefd[1],读取端是pipefd[0],管道一旦存在数据就可以读取,一般用write和read系统调用
返回值:On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.
*****************************/
#include <unistd.h>
int pipe(int pipefd[2]);

//ioctl(fd,FIONREAD,&cnt),调用返回文件描述符fd所指向的管道或FIFO中未读取的字节数
  • 管道一般用于具有血缘关系的进程之间通信,一般通信过程:

    1. 父进程中调用pipe(),然后fork,然后父子进程各自关闭一个端口,结果如图:

      在这里插入图片描述

    2. 为啥要关闭未使用的文件描述符:

      • 确保确保进程不会耗尽文件描述符的限制
      • 所有引用该管道的文件描述符都关闭之后,管道才会销毁
      • 当管道的所有写端关闭之后,读取端在尝试读取数据的时候,会收到一个文件结尾标志,如果所有其它进程引用的文件描述符都关闭了,但是读取进程本身,没有关闭写入端的文件描述符,那么进程本身会以为还有数据将要写入,read调用会一直阻塞
      • 当所有读取端已经关闭之后,对管道的写入操作,会使得进程收到SIGPIPE信号,默认是导致进程退出
  • 举例:

    /* pipe_ls_wc.c
    
       Demonstrate the use of a pipe to connect two filters. We use fork()
       to create two children. The first one execs ls(1), which writes to
       the pipe, the second execs wc(1) to read from the pipe.
    */
    #include <sys/wait.h>
    #include "tlpi_hdr.h"
    
    int
    main(int argc, char *argv[])
    {
        int pfd[2];                                     /* Pipe file descriptors */
    
        if (pipe(pfd) == -1)                            /* Create pipe */
            errExit("pipe");
    
        switch (fork()) {
        case -1:
            errExit("fork");
    
        case 0:             /* First child: exec 'ls' to write to pipe */
            if (close(pfd[0]) == -1)                    /* Read end is unused */
                errExit("close 1");
    
            /* Duplicate stdout on write end of pipe; close duplicated descriptor */
    
            if (pfd[1] != STDOUT_FILENO) {              /* Defensive check */
                if (dup2(pfd[1], STDOUT_FILENO) == -1)
                    errExit("dup2 1");
                if (close(pfd[1]) == -1)
                    errExit("close 2");
            }
    
            execlp("ls", "ls", (char *) NULL);          /* Writes to pipe */
            errExit("execlp ls");
    
        default:            /* Parent falls through to create next child */
            break;
        }
    
        switch (fork()) {
        case -1:
            errExit("fork");
    
        case 0:             /* Second child: exec 'wc' to read from pipe */
            if (close(pfd[1]) == -1)                    /* Write end is unused */
                errExit("close 3");
    
            /* Duplicate stdin on read end of pipe; close duplicated descriptor */
    
            if (pfd[0] != STDIN_FILENO) {               /* Defensive check */
                if (dup2(pfd[0], STDIN_FILENO) == -1)
                    errExit("dup2 2");
                if (close(pfd[0]) == -1)
                    errExit("close 4");
            }
    
            execlp("wc", "wc", "-l", (char *) NULL);
            errExit("execlp wc");
    
        default: /* Parent falls through */
            break;
        }
    
        /* Parent closes unused file descriptors for pipe, and waits for children */
    
        if (close(pfd[0]) == -1)
            errExit("close 5");
        if (close(pfd[1]) == -1)
            errExit("close 6");
        if (wait(NULL) == -1)
            errExit("wait 1");
        if (wait(NULL) == -1)
            errExit("wait 2");
    
        exit(EXIT_SUCCESS);
    }
    
1.2 FIFO(命名管道或者有名管道):
  • FIFO与管道类似,它们之间最大的区别是:FIFO在文件系统中拥有一个名称,打开方式和打开一个普通文件类似,这样就能够保证FIFO用于非相关进程间通信,其次和管道一样,所有引用FIFO的文件描述符关闭之后,所有未被读取的数据会被丢弃

  • 特点:

    1. 半双工
    2. 任意进程间通信
  • shell命令行中创建一个管道:

    //创建一个名为pathname的FIFO文件,-m用来指定其访问权限
    $ mkfifo [-m mode] pathname
    //创建一个名字为myfifo的fifo文件  
    root@ubuntu:mkfifo myfifo 
    //查看fifo文件,第一列用p表示,表示其是一个fifo文件
    root@ubuntu:/tmp# ls -l myfifo 
    prw-r--r-- 1 root root 0  51 00:51 myfifo
    
  • 在程序中创建一个fifo文件:

    /**************************
    函数功能:创建一个fifo文件
    返回值: On success mkfifo() returns 0.  In the case of an error, -1 is returned
           (in which case, errno is set appropriately).
    **************************/
    #include <sys/types.h>
    #include <sys/stat.h>
    
    int mkfifo(const char *pathname, mode_t mode);
    /*参数:
    pathname:表示fifo文件的路径,可以是相对路径,也可以是绝对路径
    mode:fifo文件的权限,最终fifo文件的权限是(mode & (~umask))
    */
    
  • 默认情况下,FIFO的两个端口都没有打开的话,第一个打开某个端口的open调用会阻塞到另外一个端口open调用成功,然后第一个open调用就会成功了

    1. 但是可以通open调用指定O_RDWR标志来绕过阻塞行为,但是这样破坏了FIFO的模型,因此一般情况下,开发人员不能这样做的,一般采用open()的O_NONBLOCK标记来完成非阻塞的任务
    2. 不能采用O_RDWR的其它原因:
      • FIFO的一端的进程从返回的文件描述符中读取数据永远不会看到文件结尾,因为该文件描述符既可以读取数据,也可以写入数据,这样导致永远至少存在一个文件描述符被打开以等待数据写入FIFO
  • 创建双重管道:

    /*1. 创建一个FIFO叫myfifo,执行wc -l命令从myfifo中读取数据,当然会先阻塞到有数据为止
    2. 执行ls -l命令将结果发送到tee命令,tee命令会把内容传递给sort命令,也会传递给myfifo文件中去
    */
    root@ubuntu:/tmp# wc -l < myfifo &
    root@ubuntu:/tmp# ls -l | tee myfifo | sort -k5n
    
    1. 图形表示:

      [外链图片转存失败(img-9NvZbqc5-1568113713282)(images\tee.png)]

1.3 非阻塞I/O
  • 在FIFO上调用open函数时候,加上O_NONBLOCK标志的影响:

在这里插入图片描述

  • 使用O_NONBLOCK标志的目的:

    1. 允许单个进程就打开FIFO的两端

    2. 防止两个FIFO进程之间产生死锁,产生死锁的情况:

      [外链图片转存失败(img-q8vJLVIX-1568113713283)(images\产生死锁.png)]

  • O_NONBLOCK标志不仅影响open调用,还会影响后续的write和read调用,因为在打开的文件描述符上面还继承着相应标识,如果需要修改,就需要使用fcntl函数

1.4 管道和FIFO上面的read()和write()语义
  • 执行读取操作时候的语义:
    在这里插入图片描述

  • 执行写入操作时候的语义:

    在这里插入图片描述

    1. 在启用O_NONBLOCK标志的时候:当n<PIPE_BUF的时候(这个时候保证数据必须一次性可以写进去),如果空间不足以写入数据,会立即返回失败(EAGAIN),而不是阻塞,因为如果继续部分写入,就破坏了数据小于PIPE_BUF时候原子操作的语义,举个例子如下:
      • PIPE_BUF = 4096bytes,n = 576bytes,每次写入必须是576bytes,如果剩余空间不足576bytes,就返回失败
    2. 当n>PIPE_BUF的时候,就不保证全部数据的原子性,只能保证小于PIPE_BUF部分的原子性,这个时候write返回实际传输的字节数,当一个字节都无法传输的时候,返回失败,举个例子:
      • PIPE_BUF = 4096bytes,n = 8000bytes,管道剩余空间是5000个字节,那么前4096个字节会原子的写入,然后剩余3904bytes没写,剩余空间还剩904bytes,这个时候CPU调度到其他进程占用写入端口,然后消耗了一部分剩余空间,假设消耗了204byts,剩余空间为700bytes,但是现在还有3904bytes没写入,所以就只能原子的写入700bytes,那么还剩余3204bytes没有写入,下次写的时候,因为没有剩余空间了,然后返回失败
1.5 字节流传输时候处理方法

在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值