【Linux】Linux的管道

管道是Linux由Unix那里继承过来的进程间的通讯机制,它是Unix早期的一个重要通讯机制。其思想是,在内存中建立一个共享文件,从而使通讯双方利用这个共享文件来传递信息。因为这种方式具备单向传递数据的特色,因此这个做为传递消息的共享文件就叫作“管道”。java

在管道的具体实现中,根据通讯所使用的的文件是否具备名称,有“匿名管道”和“命名管道”。node

 

管道与共享内存的区别
乍一看,感受管道和共享内存并非区别很大,这里介绍一下二者之间的区别:linux

管道须要在内核和用户空间进行四次的数据拷贝:由用户空间的buf中将数据拷贝到内核中 -> 内核将数据拷贝到内存中 -> 内存到内核 -> 内核到用户空间的buf。而共享内存则只拷贝两次数据:用户空间到内存 -> 内存到用户空间。
管道用循环队列实现,连续传送数据能够不限大小。共享内存每次传递数据大小是固定的;
共享内存能够随机访问被映射文件的任意位置,管道只能顺序读写;
管道能够独立完成数据的传递和通知机制,共享内存须要借助其余通信方式进行消息传递。
也就是说,二者之间最大的区别就是: 共享内存区是最快的可用IPC形式,一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递,就再也不经过执行任何进入内核的系统调用来传递彼此的数据,节省了时间。c++

 

匿名管道
匿名管道是在具备公共祖先的进程之间进行通讯的一种方式。面试

前面在介绍进程的建立时讲到,由父进程建立的子进程将会赋值父进程包括文件在内的一些资源。若是父进程建立子进程以前建立了一个文件,那么这个文件的描述符就会被父进程在随后所建立的子进程所共享。也就是说,父、子进程能够经过这个文件进行通讯。若是通讯的双方一方只能进行读操做,而另外一方只能进行写操做,那么这个文件就是一个只能单方向传送消息的管道,以下图所示:数组

进程能够经过调用函数pipe()建立一个管道。函数pipe()的原型以下:服务器

int pipe(int fildes[2]);
与该函数pipe()相对应的系统调用sys_pipe()的原型以下:数据结构

asmlinkage int sys_pipe(unsigned long __user * fildes);
从本质上来讲,pipe()函数的功能就是建立一个内存文件,但与建立普通文件的函数不一样,函数pipe()将在参数fildes中为进程返回这个文件的两个文件描述符fildes[0]和fildes[1]。其中,fildes[0]是一个具备“只读”属性的文件描述符,fildes[1]是一个具备“只写”属性的文件描述符,即进程经过fildes[0]只能进行文件的读操做,而经过fildes[1]只能进行文件的写操做。架构

这样,就使得这个文件像一段只能单向流通的管道同样,一头专门用来输入数据,另外一头专门用来输出数据,因此称为管道。因为这种文件没有文件名,不能被非亲进程所打开,只能用于亲属进程间的通讯,因此这种没有名称的文件造成的通讯管道叫作“匿名管道”。框架

显然,若是父进程建立的这种文件只是用来通讯,那么它感兴趣的只是该文件所占用的内存空间,因此也就没有必要建立一个正式文件,只需建立一个只存在于内存的临时文件。从这一点来看,匿名管道与共享内存具备共同点,只不过匿名管道时单向通讯,并且这个通讯只能在亲属进程间进行。

为支持匿名管道,内核初始化时由内核函数kernel_mount()安装了一种特殊的文件系统,在该系统中所建立的都是临时文件。

因为匿名管道是一个文件,因此它也有i节点,其结构以下:

struct inode
{
        ...
        struct file_operations *i_fop;            //文件操做函数集
        struct pipe_inode_info *i_pipe;           //管道文件指针
        ...
};
能够看到,在i节点的结构中有一个pipe_inode_info类型的指针i_pipe,在普通文件中这个指针的值为NULL,而在管道文件中这个指针则只想一个叫作管道节点信息结构的pipe_inode_info,以代表这是一个管道文件。pipe_inode_info的结构以下:

struct pipe_inode_info {
    wait_queue_head_t wait;            //等待进程队列
    unsigned int nrbufs, curbuf;
    struct page *tmp_page;
    unsigned int readers;
    unsigned int writers;
    unsigned int waiting_writers;
    unsigned int r_counter;            //以只读方式访问管道的进程计数器
    unsigned int w_counter;            //以只写方式访问管道的进程计数器
    struct fasync_struct *fasync_readers;
    struct fasync_struct *fasync_writers;
    struct inode *inode;
    struct pipe_buffer bufs[PIPE_BUFFERS];            //缓冲区数组
};
结构中的域bufs就是构成管道的内存缓冲区。该缓冲区用结构pipe_buffer来描述:

struct pipe_buffer {
    struct page *page;                    //缓冲页的结构
    unsigned int offset, len;
    const struct pipe_buf_operations *ops;            //缓冲区的操做函数集指针
    unsigned int flags;
    unsigned long private;
};
从上面的数据结构中能够看到,管道实质上就是一个被当作文件来管理的内存缓冲区。

在建立一个管道的i节点时,结构inode中的域i_fop被赋予rdwr_pipefifo_fops,即管道文件自己是既可读又可写的。rdwr_pipefifo_fops在文件linux/fs/pipe.c中的定义以下:

const struct file_operations rdwr_pipefifo_fops = {
    .llseek        = no_llseek,
    .read        = do_sync_read,
    .aio_read    = pipe_read,
    .write        = do_sync_write,
    .aio_write    = pipe_write,
    .poll        = pipe_poll,
    .unlocked_ioctl    = pipe_ioctl,
    .open        = pipe_rdwr_open,
    .release    = pipe_rdwr_release,
    .fasync        = pipe_rdwr_fasync,
};
而为进程所建立的打开文件描述符fildes[0]和fildes[1]中的i_fop,则被分别赋予了只读的函数操做集read_pipefifo_fops和只写的函数操做集write_pipefifo_fops。

read_pipefifo_fops和write_pipefifo_fops这两个操做函数集在文件linux/fs/pipe.c中分别定义以下:

const struct file_operations read_pipefifo_fops = {
    .llseek        = no_llseek,
    .read        = do_sync_read,
    .aio_read    = pipe_read,
    .write        = bad_pipe_w,
    .poll        = pipe_poll,
    .unlocked_ioctl    = pipe_ioctl,
    .open        = pipe_read_open,
    .release    = pipe_read_release,
    .fasync        = pipe_read_fasync,
};
 
const struct file_operations write_pipefifo_fops = {
    .llseek        = no_llseek,
    .read        = bad_pipe_r,
    .write        = do_sync_write,
    .aio_write    = pipe_write,
    .poll        = pipe_poll,
    .unlocked_ioctl    = pipe_ioctl,
    .open        = pipe_write_open,
    .release    = pipe_write_release,
    .fasync        = pipe_write_fasync,
};
建立匿名管道的进程与管道之间的关系以下图所示:

当一个进程调用函数pipe()建立一个管道后,管道的链接方式以下所示:

从图中能够看到,因为管道的出入口都在同一个进程之中,这种管道没有多大的用途的。可是当这个进程在建立一个新进程以后,状况就变得大不同了。

若是父进程建立一个管道以后,又建立了一个子进程,那么因为子进程继承了父进程的文件资源,因而管道在父子进程中的链接状况就变成以下图同样的状况了:

在肯定管道的传输方向以后,在父进程中关闭(close())文件描述符fildes[0],在子进程中关闭(close())文件描述符fildes[1],因而管道的链接状况就变成以下状况的单向传输管道:

也能够想象,经过关闭文件描述符的方法,在两个兄弟进程之间也能够实现通讯管道。

建立完管道以后,怎么利用管道来进行数据的通讯呢?

管道使用read()和write()函数,采用字节流的方式,具备流动性,读数据时,每读一段数据,则管道内会清除已读走的数据。

读管道时,若管道为空,则被堵塞,直至管道另外一端write将数据写入到管道为止。若写段已关闭,则返回0;
写管道时,若管道已满,则被阻塞,直到管道另外一端read将管道内数据取走为止。
用close()函数,在建立管道时,写端须要关闭fildes[0]描述符,读端须要关闭fildes[1]描述符。当进程关闭前,每一个进程须要将没有关闭的描述符都进行关闭。

匿名管道具备以下特色:

因为这种管道没有其余同步措施,因此为了避免产生混乱,它只能是半双工的,即数据只能向一个方向流动。若是须要双方互相传递数据,则须要创建两个管道;
只能在父子进程或兄弟进程这些具备亲缘关系的进程之间进行通讯;
匿名管道对于管道两端的进程而言,就是一个只存在于内存的特殊文件;
一个进程向管道中写的内容被管道另外一端的进程读取。写入的内容每次都添加在管道缓冲区的末尾,而且每次都是从缓冲区的头部读取数据。
匿名管道的局限性主要有两点:一是因为管道创建在内存中,因此它的容量不可能很大;二是管道所传送的是无格式字节流,这就要求使用管道的双方实现必须对传输的数据格式进行约定。

例子:在父子进程之间利用匿名管道通讯。

#include <unist.h>
#include <string.h>
#include <wait.h>
#include <stdio.h>
 
#define MAX_LINE 80
 
int main()
{
    int testPipe[2], ret;
    char buf[MAX_LINE + 1];
    const char * testbuf = "主程序发送的数据";
 
    if (pipe(testbuf) == 0) {
        if (fork() == 0) {
            ret = read(testPipe[0], buf, MAX_LINE);
            buf[ret] = 0;
            printf("子程序读到的数据为:%s", buf);
            close(testPipe[0]);
        }else {
            ret = write(testPipe[1], testbuf, strlen(testbuf));
            ret = wait(NULL);
            close(testPipe[1]);
        }
    }
    
    return 0;
}
 

命名管道
因为匿名管道没有名称,所以,它只能在一些具备亲缘关系的进程之间进行通讯,这使它在应用方面受到极大的限制。

命名管道是在实际文件系统上实现的一种通讯机制。因为它是一个与进程没有“血缘关系”的、真正且独立的文件,因此它能够在任意进程之间实现通讯。因为命名管道不支持诸如lseek()等文件定位操做,严格遵照先进先出的原则进行传输数据,即对管道的读老是从开始处返回数据,对它的写老是把数据添加到末尾,因此这种管道也叫作FIFO文件。

一样,因为须要由管道自身来保证通讯进程间的同步,命名管道也是一个只能单方向访问的文件,而且数据传输方式为FIFO方式。

也就是说,命名管道提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,在文件系统中产生一个物理文件,其余进程只要访问该文件路径,就能彼此经过管道通讯。在读数据端以只读方式打开管道文件,在写数据端以只写方式打开管道文件。

FIFO文件与普通文件的区别:

普通文件没法实现字节流方式管理,并且多进程之间访问共享资源会形成意想不到的问题;
FIFO文件采用字节流方式管理,遵循先入先出原则,不涉及共享资源访问。
操做流程为:mkfifo -> open -> read(write) -> close ->unlink。

在这里给你们提供一个学习交流的平台,C++,Linux服务器架构师群:812855908

具备1-5工做经验的,面对目前流行的技术不知从何下手,须要突破技术瓶颈的能够加群。

在公司待久了,过得很安逸,但跳槽时面试碰壁。须要在短期内进修、跳槽拿高薪的能够加群。

若是没有工做经验,但基础很是扎实,对C++Linux工做机制,经常使用设计思想,经常使用java开发框架掌握熟练的能够加群。

________________________________________________________________________________________________

加Java架构师进阶交流群获取C++Linux
Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等多个知识点高级进阶干货的直播免费学习权限

都是大牛带飞 让你少走不少的弯路的 群号是:812855908 对了 小白勿进 最好是有开发经验

注:加群要求

一、具备工做经验的,面对目前流行的技术不知从何下手,须要突破技术瓶颈的能够加。

二、在公司待久了,过得很安逸,但跳槽时面试碰壁。须要在短期内进修、跳槽拿高薪的能够加。

三、若是没有工做经验,但基础很是扎实,对C++工做机制,经常使用设计思想,经常使用c/c++开发框架掌握熟练的,能够加。

四、以为本身很牛B,通常需求都能搞定。可是所学的知识点没有系统化,很难在技术领域继续突破的能够加。

5.阿里Java高级大牛直播讲解知识点,分享知识,多年工做经验的梳理和总结,带着你们全面、科学地创建本身的技术体系和技术认知!

相关文章
相关标签/搜索