搞懂零拷贝这一篇文章就够了

 目录

一、缓冲I/O和直接I/O

  1、应用程序内存

  2、用户缓冲区

  3、内核缓冲区

二、内存映射文件与零拷贝

1、内存映射文件

2、零拷贝

实现方法1:利用直接I/O

实现方法2:利用内存映射文件

实现方式2:利用零拷贝技术


一、缓冲I/O和直接I/O

           理解缓冲I/O和直接I/O,先搞清以下几个概念:

  1、应用程序内存

         通常是指在代码中通过 malloc/free、new/delete等分配出来的内存。

  2、用户缓冲区

         用户进程通过系统调用访问系统资源的时候,需要切换到内核态,而这对应一些特殊的堆栈和内存环境,必须在系统调用前建 立好。而在系统调用结束后,cpu会从核心模式切回到用户模式,而堆栈又必须恢复成用户进程的上下文。而这种切换就会有大量的耗时。

      你看一些程序在读取文件时,会先申请一块内存数组,称为buffer,然后每次调用read,读取设定字节长度的数据,写入buffer。(用较小的次数填满buffer)。之后的程序都是从buffer中获取数据,当buffer使用完后,在进行下一次调用,填充buffer。所以说:用户缓冲区的目的是为了减少系统调用次数,从而降低操作系统在用户态与核心态切换所耗费的时间。除了在进程中设计缓冲区,内核也有自己的缓冲区。

        比如在C语言的FILE结构体里面的buffer。FILE结构体的定义如下,可以看到里面有定义的buffer

typedef struct {
   short level;
   short token;
   short bsize;
   char fd;
   unsigned flags;
   unsigned char hold;
   unsigned char *buffer;
   unsigned char * curp;
   unsigned istemp;
}FILE;

  3、内核缓冲区

          Linux操作系统的Page Cache。为了加快磁盘的I/O,Linux系统会把磁盘上的数据以Page 为单位缓存在操作系统的内存里,这里的Page是Linux系统定义的一个逻辑概念,一个Page一般为4K。

对于缓冲I/O,一个读操作有3次数据拷贝,一个写操作,有反向的3次数据拷贝:

  • 读:磁盘 ——> 内核缓冲区 ——> 用户缓冲区 ——> 应用程序内存;
  • 写:应用程序内存 ——> 用户缓冲区 ——>内核缓冲区 ——> 磁盘

 对于直接I/O,一个读操作会有2次数据拷贝,一个写操作,有反向的2次数据拷贝

  • 读:磁盘 ——> 内核缓冲区 ——> 应用程序内存
  • 写:应用程序内存 ——> 内核缓冲区 ——> 磁盘

   所以,所谓的“直接I/O”,其中直接的意思是指没有用户级的缓冲区,但操作系统本身的缓冲还是有的。

  缓冲I/O和直接I/O两者对比如下图所示:

二、内存映射文件与零拷贝

1、内存映射文件

      相比于直接I/O,内存映射文件往前更近了一步,当用户空间不再有物理内存,直接拿应用程序的逻辑内存地址映射到Linux操作系统内核缓冲区,应用程序虽然读写是自己的内存,但这个内存只是一个“逻辑地址”,实际读写的内存是内核缓冲区!

     如:Java中的 MappedByteBuffer类实现的就是内存映射文件

      数据拷贝次数从缓冲I/O的3次,到直接I/O的2次,再到内存映射文件,变成了1次。

  • 读:磁盘 ——> 内核缓冲区
  • 写: 内核缓冲区 ——> 磁盘。     

2、零拷贝

      零拷贝(Zero Copy)是提升I/O效率的一大利器,熟悉Kafka,Netty等原理的都知道,其实现原理就是通过零拷贝技术来实现读写性能的。

  •     实现方法1:利用直接I/O

    当我们把数据发送到网络中时,如果不使用零拷贝。  伪代码:

fd1 = 打开文件描述符
fd2 = 打开Socket描述符
buffer = 应用程序内存
read(fd1, buffer)  //先把数据从文件读取到应用程序内存
write(fd2, buffer) //再把应用内存中的数据写入到网络中发出

  如下图所示:整个文件会有4次数据拷贝,读文件2次,写网络2次。

  磁盘 ——> 内核缓冲区 ——> 应用程序内存 ——> Socket缓冲区 ——> 网络

  

  • 实现方法2:利用内存映射文件

    此种方式,整个过程会有3次数据拷贝,不再经过应用程序内存,直接把内核空间中从内核缓冲区拷贝到Socket缓冲区。,伪代码如下:

fd1 = 打开文件描述符
fd2 = 打开Socket描述符
buffer = 应用程序内存
mmap(fd1, buffer)  //先把磁盘数据映射到buffer上
write(fd2, buffer) //再通过网络发送数据

  如下图所示:

  

 注意:在这里需要分清“映射”和“拷贝”的区别。

         拷贝:是把数据从一块内存中复制到另外一块内存里;

         映射:相当于只是持有了数据的一个引用(或者叫地址),数据本身只有1分。

  • 实现方式2:利用零拷贝技术

       如果使用零拷贝,可能连内核缓冲区到Socket缓冲区的拷贝也省略了。在内核缓冲区和Socket缓冲区之间并没有做数据拷贝,只是一个地址的映射,底层的网卡驱动程序要读去数据并发送到网络的时候,看似读取的是Socket缓冲区的数据,但实际上直接读的是内核缓冲区的数据。

 

  在这里,我们看到虽然叫零拷贝,实际是2次数据拷贝,1次是从磁盘到内核缓冲区,1次是从内核缓冲区到网络。之所以叫零拷贝,是从内存的角度来看的,数据在内存中没有发生过数据拷贝,只是在内存和I/O之间传输

说明:对于把文件数据发送到网络的场景,直接I/O、内存映射文件、零拷贝对应的数据拷贝次数分别是4次、3次、2次,内存拷贝次数分别是2次、1次、0次。

参考资料  《软件架构设计——大型网站技术架构与业务架构融合之道》

   更纯洁的个人博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值