搜索
简帛阁>技术文章>搞懂零拷贝这一篇文章就够了

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

 目录

一、缓冲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次。

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

   更纯洁的个人博客

录一、缓冲I/O和直接I/O1、应用程序内存2、用户缓冲区3、内核缓冲区二、内存映射文件与零拷贝1、内存映射文件2、零拷贝实现方法1:利用直接I/O实现方法2:利用内存映射文件实现方式2:利用零拷贝
Netty是Java领域有名的开源网络库,特点是高性能和高扩展性,因此很多流行的框架都是基于它来构建的,比如我们熟知的Dubbo、Rocketmq、Hadoop等,针对高性能RPC,一般都是基于Net
目录前言一、普通宏定义1防止一个头文件被重复包含2重新定义一些类型3得到指定地址上的一个字节或字4求最大值和最小值5得到一个field在结构体(struct)中的偏移量6得到一个结构体中field所占
背景在我们日常工作中,代码写着写着就出现下列的一些臭味。但是还好我们有SOLID把‘尺子’,可以拿着它不断去衡量我们写的代码,除去代码臭味。就是我们要学习SOLID原则的原因所在。设计的臭味僵化性
c++类的成员函数、对象拷贝、私有成员看一篇一综述:类是我们自己定义的数据类型(新类型)设计类时要考虑的角度:站在设计和实现者的角度。站在使用者的角度考虑。父类,子类;父类从子类中抽象出来。二
C++拷贝构造函数看一篇默认情况下,类对象的拷贝,是每个成员变量逐个拷贝。如果一个类的构造函数的第一个参数,是所属类类型的引用,如果还有其他额外参数,额外参数都有默认值。函数默认参数必须放在
目录注册表定制右键菜单前言注册表参数参数和解释验证参数注册表参数总结右击文件菜单配置多级菜单先添加一级菜单再添加二级菜单还可以添加三级菜单看下效果图通过注册表文件创建各种位置的注册表右击桌面空白位置右
摘要:在本文中,总结开发过程中最为常见的几种MySQL抛出的异常以及如何解决,包括高版本驱动的问题、时区配置问题、SSL连接问题等,是一篇经验总结贴。前言在本文中,总结开发过程中最为常见的几种My
SwiftUI是一种新颖的构建UI方式和全新的编码风格,本文以通俗易懂的语言,从Swift51语法新特性和SwiftUI的优势方面进行分享,希望对热爱移动端的同学有一定的帮助,让大家尽可能快速、全面和
1导入数据library(tidyverse)library(survival)library(survminer)test_datareadtable(survivaltxt,headerT,sep