管道和FIFO

匿名管道(pipe)

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

  调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。

  向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。

  1. 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
  3. 父进程关闭管道读端,⼦子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。

一些限制: 

  1. 只支持单向数据流
  2. 只能用于具有亲缘关系的进程之间
  3. 没有名字
  4. 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小)
  5. 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等

特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):

  1. 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0,管道的写端不存在),而仍然有进程 从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2. 如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的 进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。 
  3. 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0,管道读端不存在),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。可以捕捉该信号。 
  4. 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回
  5. 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数 
  6. 向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞

用于shell

  管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入,当在某个shell程序(Bourne shell或C shell等)键入who │ wc -l后,相应shell程序将创建who以及wc两个进程和这两个进程间的管道。考虑下面的命令行

管道实现细节

  在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的

  有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作

//client.h
#include "unpipc.h"

void client(int readfd,int writefd)
{
    char buffer[MAXLINE];

    fgets(buffer,MAXLINE,stdin);
    size_t len=strlen(buffer);
    if(buffer[len-1]=='\n')//去除换行,fgets默认存入换行符,gets不存
        --len;
    
    //把路径写回管道
    write(writefd,buffer,len);

    size_t n;
    while((n=read(readfd,buffer,MAXLINE))>0)
            write(1,buffer,n);
}
//server.h
#include "unpipc.h"
#include "my_err.h"

void server(int readfd,int writefd)
{
    size_t n;
    char buffer[MAXLINE];
    
    //先从管道中读取数据---路径
    if((n=read(readfd,buffer,MAXLINE))==0)
        err_quit("end-of-file while reading pathname");

    buffer[n]='\0';
    
    //打开文件
    int fd;
    if((fd=open(buffer,O_RDONLY))<0)
    {
        snprintf(buffer+n,sizeof(buffer)-n,":can't open,%s\n",strerror(errno));
        n=strlen(buffer);
        write(writefd,buffer,n);
    }
    else
    {
        //把文件写回管道
        while((n=read(fd,buffer,MAXLINE))>0)
            write(writefd,buffer,n);
    }
    close(fd);
}
//pipe.c
#include "client.h"
#include "server.h"

int main()
{
    int pipe1[2],pipe2[2];
    
    pipe(pipe1);
    pipe(pipe2);

    pid_t cpid;
    if((cpid=fork())==0)
    {
        close(pipe1[1]);
        close(pipe2[0]);

        server(pipe1[0],pipe2[1]);
        exit(0);
    }

    close(pipe1[0]);
    close(pipe2[1]);

    client(pipe2[0],pipe1[1]);
    
    waitpid(cpid,NULL,0);
    return 0;
}
  1. 如果一个管道的写端一直在写,而读端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只写不读再次调用write会导致管道堵塞;
  2. 如果一个管道的读端一直在读,而写端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只读不写再次调用read会导致管道堵塞;
  3. 而当他们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只读不写会导致read返回0,就像读到⽂件末尾⼀样。

github:https://github.com/tianzengBlog/test/tree/master/ipc2/pipe

popen和pclose

  标准I/O提供一个函数,执行该函数后他创建一个管道并启动另一个进程,该进程要么从管道读标准输入要么往该管道写入标准输出。

#include <stdio.h>
FILE *popen(const char *cmd,const char *type);
int pclose (FILE* stream);
  1. popen:如果调用成功,则返回一个读或者打开文件的指针,如果失败,返回NULL,具体错误要根据errno判断
  2. pclose:如果调用失败,返回 -1

  创建一个管道,fork一个子进程,接着关闭管道的不使用端,子进程执行cmd指向的应用程序或者命令。执行完该函数后父进程和子进程之间生成一条管道,函数返回值为FILE结构指针该指针用于输入或输出这取决于type,该指针作为管道的一端,为父进程所拥有。子进程则拥有管道的另一端,该端口为子进程的stdin或者stdout,cmd是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令,他是由sh程序通常为bourne shell处理,因此PATH环境变量可用于定位cmd

  1. 如果type=r,那么该管道的方向为:子进程的stdout到父进程的FILE指针(文件指针连接到command的标准输出)
  2. 如果type=w,那么管道的方向为:父进程的FILE指针到子进程的stdin(type是"w"则文件指针连接到command的标准输入)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const int MAXLINE=4096;

int main()
{
    char buffer[MAXLINE],command[MAXLINE];
    fgets(buffer,MAXLINE,stdin);//输入文件路径
    size_t len=strlen(buffer);
    if(buffer[len-1]=='\n')//去除fgets尾的换行
        --len;
    
    //把cat与路径名相连 shell命令
    snprintf(command,sizeof(command),"ls %s",buffer);
    //创建一个子进程,子进程执行command命令,子进程的输出输入到父进程的输入
    FILE *fp=popen(command,"r");
    
    //printf("pid is %d\n",getpid());
    //fgets每次读取一行,不能用if
    while(fgets(buffer,MAXLINE,fp)!=NULL)//读取成功
        fputs(buffer,stdout);//输出到屏幕

    pclose(fp);
    exit(0);
}

 github:https://github.com/tianzengBlog/test/tree/master/ipc2/popen

 FIFO

  每个FIFO都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过FIFO进行通信。,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文件)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失

  • 和管道一样,FIFO仅提供半双工的数据通信,即只支持单向的数据流。
  • 和管道不同的是,FIFO可以支持任意两个进程间的通信。
#include <sys/types.h>
#include <sys/stat.h>
 
int mkfifo(const char *pathname, mode_t mode);
  1. pathname:一个Linux路径名,它是FIFO的名字。即每个FIFO与一个路径名相对应。
  2. mode:指定的文件权限位,类似于open函数的第三个参数。即创建该FIFO时,指定用户的访问权限,有以下值:S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH,S_IWOTH。
  3. mkfifo函数默认指定O_CREAT | O_EXECL方式创建FIFO,如果创建成功,直接返回0。如果FIFO已经存在,则创建失败,会返回-1并且errno置为EEXIST。对于其他错误,则置响应的errno值;
  4. 当创建一个FIFO后,它必须以只读方式打开或者只写方式打开,所以可以用open函数,当然也可以使用标准的文件I/O打开函数,例如fopen来打开。由于FIFO是半双工的,所以不能够同时打开来读和写。
  5. 其实一般的文件I/O函数,如read,write,close,unlink都可用于FIFO。对于管道和FIFO的write操作总是会向末尾添加数据,而对他们的read则总是会从开头数据,所以不能对管道和FIFO中间的数据进行操作,因此对管道和FIFO使用lseek函数,是错误的,会返回ESPIPE错误。
  6. mkfifo的一般使用方式是:通过mkfifo创建FIFO,然后调用open,以读或者写的方式之一打开FIFO,然后进行数据通信。
  7. 管道在所有进程最终关闭之后自动消失,FIFO的名字则只有通过调用unlink才能从文件系统删除
  8. 当前如果没有任何进程打开某个FIFO来写,那么打开该FIFO来读的进程将阻塞
  9. 最后删除FIFO的是客户而不是服务器,因为这些FIFO执行最终的操作时客户。内核为管道和FIFO维护一个访问计数器,他的值时访问同一个管道或FIFO的打开着的描述符的个数,有了访问计数器后,客户或服务器就能成功的调用unlink,尽管该函数从文件系统中删除了指定的路径名,先前已打开的路径名,目前仍打开的描述符却不受影响,然而对于其他形式的IPC来说(如system v消息队列),这样的计数器不存在,因此要是向服务器在某个消息队列写入自己的最终消息后删除了该队列,那么当客户尝试读出这个消息队列时,该消息可能已经消失。

FIFO和管道的额外属性

  1.mkfifo对于shell命令的支持

:~/work/ipc2/popen$ mkfifo aa
:~/work/ipc2/popen$ echo "hello word" >aa&
[1] 6396
:~/work/ipc2/popen$ cat <aa
hello word
[1]+  已完成               echo "hello word" > aa

  后加上‘&’使进程转到后台运行,是因为FIFO以只写方式打开需要阻塞到FIFO以只读方式打开为止,所以必须要作为后台程序运行,否则进程会阻塞在前端,无法再进行相关输入

  2.设置FIFO或管道为非阻塞

int flag;
flag = fcntl(fd, F_GETFL, 0);
 
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);

/**
以下方法是错误的,这样会在设置所需标志的时候清除其他可能存在的文件标志
if(fcntl(fd,F_SETFD,)_NOBLOCK)<0)
  err...  
*/

  3.管道和FIFO的各种操作在阻塞和非阻塞状态下的不同

  1. 如果请求write的数据的字节数小于等于PIPE_BUF(POSIX关于管道和FIFO大小的限制值),那么write操作可以保证是原子的(两个进程差不多同时向一个管道或FIFO写,那么或者先写入来自第一个进程的所有数据,在写入来自第二个进程的所有数据,或者二者颠倒,但不会混杂来自两个进程的数据),如果大于PIPE_BUF,那么就不能保证了。
  2. 如果请求读出的数据量多于管道或FIFO中的可用数据量,那么只返回这些可用的数据,需准备好read小于请求所需数目的返回值。
  3. 如果没有一个为读打开的管道或FIFO写入,内核将产生一个SIGPIPE信号(这是个同步信号,是由特定线程——调用write的线程引起的信号,然而处理这个信号最容易的办法时忽略(把信号处理办法设置成SIG_IGN),让write返回个EPIPE错误,应用程序无遗漏的检测write返回的错误,检测某个进程被SIGPIPE终止时却很困难,如果该信号为捕获,就从shell中检查被终止的进程状态,确定该进程是否被某个信号杀死及具体是被哪个信号杀死)
  4. 如果调用进程没有捕获或处理该信号,默认进程终止
  5. 如果调用进程忽略了SIGPIPE信号,或捕获该信号或从信号处理程序中返回,那么write返回个EPIPE错误
  6. 当一个管道或FIFO最终close时,里面残余的数据都被丢弃

  write的原子性是由写入数据的字节数是否小于等于PIPE_BUF(定义在<limits.h>)决定的,和是不是O_NONBLOCK没有关系,当设置车非阻塞后,来自write的返回值取决于待写入的字节数以及管道或FIFO中当前可用的空间大小

在阻塞的情况下:

  1. 如果write的字节数小于等于PIPE_BUF,那么write会阻塞到写入所有数据,并且写入操作是原子的。
  2. 如果write的字节数大于PIPE_BUF,那么write会阻塞到写入所有数据,但写入操作不是原子的,即write会根据当前缓冲区剩余的大小,写入相应的字节数,然后等待下一次有空余的缓冲区,这中间可能会有其他进程进行write操作。

在非阻塞的情况下:

  1. 如果write的字节数小于等于PIPE_BUF,且管道或FIFO有足以存放要写入数据大小的空间,那么就写入所有数据;
  2. 如果write的字节数小于等于PIPE_BUF,且管道或FIFO没有足够存放要写入数据大小的空间,那么就会立即返回EAGAIN错误。
  3. 如果write的字节数大于PIPE_BUF,且管道或FIFO有至少1B的空间,那么就内核就会写入相应的字节数,然后返回已写入的字节数;
  4. 如果write的字节数大于PIPE_BUF,且管道或FIFO无任何的空间,那么就会立即返回EAGAIN错误。
//fifo.h
#include "unpipc.h"
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"

//client.c
#include <sys/types.h>
#include <sys/stat.h>
#include "fifo.h"
#include "client.h"

int main()
{
    int readfd,writefd;
    writefd=open(FIFO1,O_WRONLY,0);
    readfd=open(FIFO2,O_RDONLY,0);

    client(readfd,writefd);

    close(readfd);
    close(writefd);

    unlink(FIFO1);
    unlink(FIFO2);
    exit(0);
}

//server.c
#include <sys/types.h>
#include <sys/stat.h>
#include "fifo.h"
#include "unpipc.h"
#include "server.h"

int main()
{
    int readfd,writefd;
    
    if((mkfifo(FIFO1,FILE_MODE)<0)&&(errno!=EEXIST))
      err_sys("can't create %s",FIFO1);
    if((mkfifo(FIFO2,FILE_MODE)<0)&&(errno!=EEXIST))
    {
        unlink(FIFO1);
        err_sys("can't create %s",FIFO2);
    }

    readfd=open(FIFO1,O_RDONLY,0);
    writefd=open(FIFO2,O_WRONLY,0);

    server(readfd,writefd);
    exit(0);
}

github:https://github.com/tianzengBlog/test/tree/master/ipc2/fifo1

管道和FIFO的限制

系统内核对于管道和FIFO的唯一限制为

  1. OPEN_MAX:一个进程在任意时刻可以打开的最大描述符数为16
  2. PIPE_BUF标识一个管道可以原子写入管道和FIFO的最大字节数,并不是管道或FIFO的容量。为512

  关于POSIX的每个不变最小值都有一个具体的系统的实现值,这些是实现值由具体的系统决定,通过调用以下函数在运行时确定这个实现值:

#include <unistd.h>
 
long sysconf(int name);
long fpathconf(int filedes, int name);
long pathconf(char *path, int name);
//成功返回具体的值,失败返回-1

  其中sysconf是用于返回系统限制值,这些值是以_SC_开头的常量,pathconf和fpathconf是用于返回与文件和目录相关的运行时的限制值,这些值都是以_PC_开头的常量

  当然上面两个系统限制值的具体实现值也可以通过ulimit(Bourne shell或KornShell)(或limit (c shell))命令来查看

# ulimit -a
...
open files                    (-n) 1024
pipe size            (512 bytes, -p) 8

管道和FIFO区别

  对文件系统来说,匿名管道是不可见的,它的作用仅限于在父进程和子进程两个进程间进行通信。而命名管道是一个可见的文件。

字节流与消息

  所使用的管道和FIFO的例子都是字节流I/O模型,这是Unix原生的I/O模型模型,这种模型无记录边界,也就是读写操作不检查数据。如:从某个FIFO读100个字节的进程无法判定该FIFO写入的这100个字节的进程执行了单个100个字节的写操作,5个20字节的写操作等,或写入35个字节后在写入65个字节,这种数据是字节流,系统不对他作解释,他如果需要某种解释,读进程和写进程就得同意这种解释,并亲自去做。

  传送的消息加上某种结构,当数据由长度可变消息构成时,读者必须知道这些消息的边界以判定何时读出单个消息

  1. 带内特殊终止序列:许多Unix用换行符来分割每个消息,写进程给每个消息加个换行符,读进程每次读出一行,这种方法在数据中任何出现分隔符都做转义处理也即是以某种方式把他们标志成数据而不是作为分隔符。(http,ftp后跟双字节序列(CR/LF)分割文本记录
  2. 每个记录前冠以长度,不需通过转义字符来分割数据,不需要扫描整个数据来寻找每个记录结束的位置
  3. 每次连接一个记录:应用程序关闭与对端的连接(网络应用为TCP连接,IPC为IPC连接)来指示一个记录的长度,为每个记录创建一个新的连接,HTTP1.0使用这一技术

  用标准I/O函数fdopen将标准I/O与pipe返回的某个已打开的文件描述符关联。

posted on 2018-07-23 11:38  tianzeng  阅读(1538)  评论(0编辑  收藏  举报

导航