Linux 管道(匿名管道与命名管道)

管道 

管道是Unix中最古老古老的进程间通信手段, 人们把从一个进程连接到另一个进程的数据流称为“管道” . Linux中的管道从

Unix继承而来 .

管道分为匿名管道(pipe)和命名管道(named pipe / FIFO)

匿名管道(pipe)

匿名管道实际上是由内核管理内核中的一块缓冲区 , 是一种半双工通信手段, 通过让不同进程都能访问同一块缓冲区,

来实现进程间通讯 . 匿名管道仅限于本地父子进程之间通信, 结构简单, 相对于命名管道, 其占用小, 实现简单.

匿名管道是内核中的一块缓冲区, 其本质就是一个文件,  但这个文件没有名字, 所以称之为"匿名管道". 但一个文件没有名字的话,

不同进程又如何知道要访问哪一块内存来实现进程间通信呢 ? 实际上这是利用了父进程创建子进程时, 子进程的task_struct(PCB)

会获取到大部分父进程task_struct中的信息, 其中就包括父进程创建的匿名管道时返回的两个匿名管道的文件描述符, 这样,子进程

也就能通过这两个匿名管道的文件描述符访问匿名管道了.   也就是说匿名管道实现的进程间通信只能限制于有亲缘关系的进程间

才能通信, 也就是只能用于具有公共祖先的进程之间的通信.


基本特征

  • 半双工: 半双工通信其实就是一种可以选择方向的单向通信, 也就是在同一时刻, 数据只能单向传输. 就像现实中的水管
                
                 一样, 不能两端同时进水或出水一样.
     
  • 大小 :因为管道是内核中的一块缓冲区, 所以是有大小的,  在2.6.11之前的Linux版本中,管道的容量与系统页面大小相同. 从           
              Linux2.6.11开始,管道容量PIPE_SIZE为64K . 但原子操作的最大值是PIPE_BUF为4K .

              原子操作 :不可被打断的操作, 也就是说一旦进行某个原子操作, 就一定会一次性执行完 .
     
  • 生命周期 : 管道的生命周期时伴随进程的, 随着进程使用管道而创建缓冲区, 随进程的退出而释放销毁
     
  • 仅限于本地父子进程之间通信


管道自带同步和互斥

同步: 对临界资源访问的合理性, 通常时通过条件判断来实现的 .

同步体现在: 若管道没数据, 读取(read)时会阻塞,  若管道满了, 写入(write)时会阻塞

互斥: 在同一时间, 只有一个进程能对临界资源进行访问, 保证临界资源的安全性. 

互斥体现在: 对管道进行数据操作的大小不超过PIPE_BUF时, 则保证操作的原子性.


读写规则

  • 管道的读写使用read()和write()函数,采用字节流的方式,具有流动性, 读数据时, 每读一段数据, 则管道内会清除已读走的数据.
     
  • 读时,若管道为空,则被堵塞,直至管道写端将数据写入到管道为止, 若写端已关闭,则返回 0.
     
  • 写时,若管道已满,则被阻塞,直到管道读端将管道内数据取走为止.
     
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性. 当大于PIPE_BUF时, 将不再保证写入的原子性 .
     
  • 管道没有其他同步措施,所以为了不产生混乱, 它只能是半双工的, 即数据只能向一个方向流动. 如果需要双方互相传递数据,

    则需要建立两个管道.
     
  • 一个进程向管道中写的内容被管道另一端的进程读取, 写入的内容每次都添加在管道缓冲区的末尾, 读取时每次都是从缓冲区

    的头部读取.
     
  • 匿名管道的局限性:一是只能用于具有亲缘关系的进程之间通信, 这是其最大的局限性.  二是管道所传送的是无格式字节

    流,这就要求使用管道的双方实现必须对传输的数据格式进行约定 .
     
  • 在创建管道后, 哪个进程是写的, 则在这个进程开始就关闭读端. 同理, 哪个进程是读的, 就在这个进程开始就关闭写端. 但当

    进程结束, 还是需要关闭剩下的一个写端/读端.

匿名管道的创建

在Shell中 .

在Shell中 | 是管道符, 当Shell 解析命令时, 就会创建一个管道,  | 左面的程序输出流入管道作为 | 右边程序的输入 , 如下图所示:

那么Shell是如何实现匿名管道的创建的呢? Shell是调用pipe()来创建匿名管道的 .


函数:  int pipe( int fildes[2] )

头文件: unistd.h

参数 int fildes[2] :匿名管道的文件描述符数组,其中fildes[0]表示读端, fildes[1]表示写端 .

返回值: 成功返回0,失败返回错误代码

pipe()函数的功能就是创建一个管道文件,但与open()创建文件或打开文件不同,函数pipe()将在参数fildes中为进程返回匿名管

道的两个文件描述符fildes[0]和fildes[1]. 其中,fildes[0]是一个具有“只读”属性的文件描述符,fildes[1]是一个具有“只写”属性的文

件描述符,即进程通过fildes[0]只能进行文件的读操作,而通过fildes[1]只能进行文件的写操作。这样,就使得这个文件像一段只

能单向流通的管道一样,一头专门用来输入数据,另一头专门用来输出数据,所以称为管道 .


匿名管道基于fork的使用

如果匿名管道的出入口都在一个进程内, 这样的匿名管道是没有多大意义的, 就比如自己家里的水管的入口和出口都在自己家, 黄

无意义. 如下图所示 :

但当父进程创建子进程后, 由于子进程也拿到了匿名管道的文件描述符, 情况就不同了, 如下图: 

实际使用中在确定管道的传输方向之后,比如父进程写, 子进程读, 此时就可以在父进程中关闭读端(close(fildes[0])), 在子进

程中关闭写端( close(fildes[0]) ), 于是管道的连接情况就变成如下情况的单向传输管道:

来看个例子 :

模拟父子进程间通信:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main(){
    int pfd[2];
    if(pipe(pfd) < 0){
        perror("make pipe");
        exit(1);
    }
    pid_t pid = fork();
    if(pid < 0){
        perror("fork");
    }
    else if(pid == 0){
        char buf[100];
        int len, n = 3;
        close(pfd[0]);
        while(n-- && fgets(buf, 100, stdin)){
            len = strlen(buf);
            printf("子进程将所输字符串写入管道\n");
            if(write(pfd[1], buf, len) != len){
                perror("write to pipe");
            } 
            usleep(100);
        }
        close(pfd[1]);
    }
    else{
        char buf[100];
        int len, n = 3;
        close(pfd[1]);
        while(n--){
            usleep(100);
            if((len = read(pfd[0], buf, 100)) == -1){
               perror("read from pipe");
            }  
            write(1, buf, len);
            printf("父进程从管道读出字符串, 打印到屏幕\n");
        }
        close(pfd[0]);
    }
    exit(0);
}


命名管道(named pipe / FIFO)

由于匿名管道值限制于本地具有亲缘关系的进程之间通信, 极大地限制了匿名管道的使用. 但命名管道就解决了这两个问题.

Linux提供了FIFO方式进行进行间通信, FIFO又叫做命名管道(named pipe).

FIFO (First in, First out)为一种特殊的文件类型, 不同与普通文件, 它在文件系统中有对应的路径. 当一个进程以读(r)的方式

打开该文件,而另一个进程以写(w)的方式打开该文件, 那么内核就会在这两个进程之间建立管道. 所以FIFO实际上也

由内核管理,不与硬盘打交道. 之所以叫FIFO,是因为这个管道本质上是一个先进先出的队列数据结构,最早放入的数据

被最先读出来, 从而保证信息交流的顺序. FIFO只是借用了文件系统来为管道命名, 有了文件名, 就好让不具有亲缘关系的

进程也可以访问到同一块缓冲区. 当删除FIFO文件时,管道连接也随之消失 .


基本特征

  • 半双工
     
  • 可跨网络在不同计算机上实现不同进程之间的通信
     
  • 生命周期 : 随内核 (直到用户删除)

打开/读写规则

  • 适用于open(),close(), write(), read()系统调用.
     
  • 若open以只读的方式打开, 则会阻塞, 直到某个进程以写的方式打开
     
  • 若open以只写的方式打开, 则会阻塞, 直到某个进程以读的方式打开
     
  • 读时,若管道为空,则被堵塞,直至管道写端将数据写入到管道为止, 若写端已关闭,则返回 0.
     
  • 写时,若管道已满,则被阻塞,直到管道读端将管道内数据取走为止.
     
  • 通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到SIGPIPE信号)退出.
     
  • 管道的读写使用read()和write()函数,采用字节流的方式,具有流动性, 读数据时, 每读一段数据, 则管道内会清除已读走的数据.

命名管道的创建

在Shell中

mkfifo命令

可以看到, ls -l 可以看到fifo的类型为p, 是管道文件.

那么Shell是在解析到mkfifo时, 是如何创意命名管道的 ? 其实Shell最终还是调用了mkfifo()函数.

int mkfifo(const char *pathname, mode_t mode);

头文件 : sys/stat.h

函数功能 : 生成名为pathname的FIFO特殊文件, mode指定FIFO文件的权限. 预设权限 = mode&(~umask), 若已存在pathname

同名文件, 则失败, 置errno为EEXIST.

返回值 : 成功返回0. 错误,返回-1(并设置errno)

举个命名的栗子:

命名管道实现无亲缘关系的进程间通信

named_pipe1.c

#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
int main(){
    umask(0);
    if(mkfifo("my_named_pipe", 0664) < 0){
        perror("mkfifo");
    }
    int read_fd = open("my_named_pipe", O_RDONLY);
    if(read_fd < 0){
        perror("open");
    }
    char buf[1024] = {0};
    ssize_t s;
    for(int i = 0; i < 5; ++i){
        buf[0] = 0;
        printf("Please wait ... \n");
        s = read(read_fd, buf, 1023);
        if(s > 0){
            buf[s - 1] = '\0';
            printf("client say# %s\n", buf);
        }
        else if(s == 0){
            printf("client quit, exit now!\n");
            exit(EXIT_SUCCESS);
        }
        else{
            perror("read");
        }
    }
        close(read_fd);
    return 0;
}

 named_pipe2.c

#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
int main(){
    int write_fd = open("my_named_pipe", O_WRONLY);
    if(write_fd < 0){
        perror("open");
    }
    char buf[1024] = {0};
    ssize_t s;
    for(int i = 0; i < 5; ++i){
        printf("Please Enter# ");
        fflush(stdout);
        s = read(0, buf, 1023);
        if(s > 0){
            buf[s] = '\0';
            write(write_fd, buf, strlen(buf));
        }
        else{
            perror("read");
        }
    }
        close(write_fd);
    return 0;
}

运行如下:

发布了223 篇原创文章 · 获赞 639 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/103541761