0%

常见的IO模型

常见的IO模型

同步与阻塞

  • IO模型是按照同步与阻塞来区分的,所以需要首先理解同步与阻塞的概念—-这部分对于同步与阻塞的概念的理解是很重要的

  • 首先,可以认为一个IO操作包含两个部分:

    • 发起IO请求(阻塞与非阻塞)
      • 在发起IO请求时,对于NIO来说通过channel发起IO操作请求后,其实就返回了,所以是非阻塞,如果发出请求后一直等待结果就是阻塞的
    • 实际的IO读写操作(同步与异步)
      • 在实际的IO读写操作,如果操作系统帮你完成了再通知你,那就是异步,如果发起请求的一方不断的轮询结果并需要自己执行数据从内核态到用户态的转移工作的就叫做同步

从操作系统的角度理解IO

  • 从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。

  • 当应用程序发起 I/O 调用后,会经历两个步骤

    • 内核等待 I/O 设备(硬盘、网卡)准备好数据
    • 内核将数据从内核空间拷贝到用户空间
  • 对于服务器来说

    • 服务器的网络驱动接受到消息之后,向内核申请空间,并在收到完整的数据包(这个过程会产生延时,因为有可能是通过分组传送过来的)后,将其复制到内核空间;
    • 数据从内核空间拷贝到用户空间;
    • 用户程序进行处理。

Linux下五种IO模型

同步阻塞 I/O

  • 进程发起IO系统调用后,进程、线程被阻塞(此时对应的线程会因为丧失时间片而被挂起处于ready线程状态,不是因为锁的阻塞),转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据
    • 典型应用:阻塞socket、Java BIO
    • 特点:
      • 实现难度低、开发应用较容易;
      • 适用并发量小的网络应用开发;
      • 不适用并发量大的应用:因为一个请求IO会阻塞进程、线程,所以,得为每请求分配一个处理进程(线程)以及时响应,系统开销大。Tomcat应该就是用到了这种模型
  • 在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的(使用线程池构建伪异步IO)。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量
  • 总结来说就是,来一个连接后就专门分配一个线程去处理请求,且发出IO请求后线程直接挂起(阻塞)等待IO处理完毕(同步)
    • 相对于同步非阻塞来说,同步阻塞因为不占用CPU时间,所以CPU利用率会比较高,而同步非阻塞则需要不断的轮询而占用CPU时间
    • 一个连接对应一个线程,连接未必是有效的请求

同步非阻塞 I/O

  • 进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。
    • 典型应用:socket是非阻塞的方式(设置为NONBLOCK)
    • 特点:
      • 虽然需要执行IO操作要等待时,因为是非阻塞会直接返回错误,但是内核还是会尝试去准备数据,而进程则需要使用同步的特性也就是不断的轮询去判断IO数据是否已经准备到缓存
      • 应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的,相对于同步阻塞IO来说,后者直接挂起是被消耗CPU资源的,但是不挂起而是直接返回是相对灵活自由的
  • 与同步阻塞式IO的区别就是如果需要等待IO操作时,不是挂起而是直接返回错误,CPU的使用率偏低,吞吐率偏低,但是响应时间很短

I/O 多路复用

  • 多个的进程的IO(在Linux中实际上就是文件描述符fd)可以注册到一个复用器(select)上,然后用一个进程阻塞调用该select, select会监听所有注册进来的IO;如果select所有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,select调用就会返回;而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO的read方法(read方法本身还是堵塞的从内核态拷贝数据到用户态),读取内核中准备好的数据。可以看到,多个进程注册IO后,只有另一个select调用进程被阻塞。

    image-20210621151451168

    • 典型应用:系统层面支持select、poll、epoll三种方案(Nginx就可以配置使用其中一个多路复用方案)、Java NIO
    • 特点:
      • 非阻塞
      • 实现、开发应用难度较大;
      • 适用高并发服务应用开发:一个进程(线程)响应多个请求
      • 结合Redis的单线程模型来说明IO多路复用的优势
        • 没有创建多线程以及线程切换的开销
        • 没有线程同步的开销以及死锁等风险
    • 一个请求对应一个线程
  • Linux中IO复用的实现方式主要有select、poll和epoll

    • Select:注册IO、阻塞扫描,监听的IO最大连接数不能多于FD_SIZE;

      • 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长
      • 有连接数的限制
    • Poll:原理和Select相似,没有数量限制,但IO数量大扫描线性性能下降,时间复杂度同为O(n);

      • poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的
    • Epoll :事件驱动不阻塞,mmap实现内核与用户空间的消息传递,数量很大,Linux2.6后内核支持;

      • epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(通过回调函数通知)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
        • 内部使用红黑树存储fd
    • 以上三种IO复用的实现方式的使用场景的区别

      • select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景select 可移植性更好,几乎被所有主流平台所支持

        • 超时参数就是等待数据有效的超时时间,如果指定时间内,没有有效数据就直接返回了
      • poll对于fd没有数量限制

      • 大量的描述符要注册,并且都是长连接

        • 如果描述符比较少,或者都是短连接,状态很容易变化,则不适合使用epoll

          因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试

    • select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步非阻塞I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间

  • 感性认识就是找了一个选择器做管家,帮助用户线程去阻塞等待IO操作,IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。

    • 对于服务器端,客户端的请求(网卡数据)就是IO操作要等待的数据,客户端连接建立后实际上是注册在selector上的,然后等待客户端真正的数据传输到服务器端后,才会分配用户线程去尝试读取数据

信号驱动 I/O

  • 当进程发起一个IO操作后直接返回不阻塞当内核数据就绪时内核像发起IO请求的进程发送一个SIGIO信号,进程便在信号处理函数中调用IO读取数据(阻塞)。特点:SIGIO信号通知、实现、开发应用难度大
  • 实际上是同步非阻塞的

异步 I/O

  • 用户进程进行系统调用后立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号(与信号驱动IO类似)
  • 真正的异步,在五种Linux的IO中,只有这个IO是异步IO其余的都是同步IO,当前进程不关心具体IO的实现,后来再通过回调函数,或信号量通知当前进程直接对IO返回结果(注意此时IO数据已经完成了从内核态到用户态的转移,可以直接在用户态处理,而不是用户态调用系统接口阻塞着去内核态读取数据,这也是同步与异步的区别)进行处理
  • 可以说整个IO包括同步IO与异步IO,而同步IO又包括同步阻塞IO、同步非阻塞IO、IO多路复用、信号驱动IO
image-20210621151930015

Java中3种常见的IO模型

  • JVM封装了操作系统的五种IO模型,在Java世界中常用的IO模型有以下3种

BIO

  • 对应的实际就是系统层面的同步阻塞模型
  • 对于服务器来说是一个连接一个线程
  • 如果要让 BIO 通信模型能够同时处理多个客户端请求,就必须使用多线程(主要原因是socket.accept()socket.read()、socket.write()` 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 线程池机制 改善(后边说的伪异步IO),线程池还可以让线程的创建和回收成本相对较低。使用FixedThreadPool 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制
  • 伪异步IO:为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽
    • 伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题

NIO

  • 对应的实际就是系统层面的IO多路复用

  • Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , SelectorBuffer 等抽象。NIO 中的 N 可以理解为Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO

  • 一个请求一个线程,客户端(相对于服务器端)发送的连接请求都会注册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程进行处理

  • Java中的NIO直接使用的话难度比较大,可以直接使用Netty

    • JDK 的 NIO 底层由 epoll 实现
  • Java中NIO与普通IO(BIO)的区别

    • 非阻塞IO

      • IO流是阻塞的,NIO流是不阻塞的
    • 面向缓冲区

      • IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。

        • Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。
        • 在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作
        • 最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区
          • 更加熟悉的一个是DirectByteBuffer直接在堆外内存构建buffer,常用于NIO框架中,性能更好,在Unsafe介绍文档中学习过
    • 通道

      • NIO 通过Channel(通道) 进行读写
      • 通道是双向的,可读也可写,而流的读写是单向的(意思应该是同一个流同时只能读或者写)。无论读写,通道只能和缓冲区交互。因为 缓冲区的存在,通道可以异步地读写。
        • NIO读数据时,创建一个缓冲区,然后请求通道读取数据
        • NIO写数据时,创建一个缓冲区,然后请求通道写入数据
    • 选择器

      • NIO有选择器,而IO没有
      • 选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的
image-20210328144808690

AIO

  • 对应的是系统层面的异步IO
  • AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型
  • 一个有效请求一个线程, AIO一般适用于连接数目多且连接比较长(重操作)的架构
    • 对于一个有效请求一个线程的理解是:客户端的IO请求都是由操作系统先完成了IO操作,将数据拷贝到用户态之后,再通知服务器用其用户线程进行处理,没有阻塞和同步的从内核态拷贝数据的过程
  • AIO方式适用于连接数目多且连接比较长(重操作)的架构,与之对应的是NIO更适合于连接数目多但是连接比较短的架构

零拷贝技术

  • 零拷贝技术的典型就是kafka,使用零拷贝技术是kafka高性能的原因之一

为什么引入零拷贝技术

  • 一般的中间件或者提供网络服务的组件,抽象起来就是做了两件事

    • 从磁盘读数据到用户缓存
    • 将数据发送到网卡进而通过网络传输数据
  • 以kafka为例。可以抽象出以下这样一个流程图

    image-20210907140712220

    • 在没有任何优化技术使用的背景下,操作系统为此会进行 4 次数据拷贝,以及 4 次上下文切换

      • 4 次 copy:

        • CPU 负责将数据从磁盘搬运到内核空间的 Page Cache 中;

        • CPU 负责将数据从内核空间的 Socket 缓冲区搬运到网卡中;

        • CPU 负责将数据从内核空间的 Page Cache 搬运到用户空间的缓冲区;

        • CPU 负责将数据从用户空间的缓冲区搬运到内核空间的 Socket 缓冲区中。

      • 4 次上下文切换:

        • read 系统调用时:用户态切换到内核态;

        • read 系统调用完毕:内核态切换回用户态;

        • write 系统调用时:用户态切换到内核态;

        • write 系统调用完毕:内核态切换回用户态。

    • CPU和网络以及磁盘打交道,会导致比较长时间的IO等待以及频繁的由CPU负责的硬件间的数据拷贝以及CPU状态的切换,虽然可以通过多线程或者多进程的调度避免IO等待,但是还是需要尽量减轻高速的CPU的IO等待时间、以及避免CPU去负责大量的数据拷贝,由此引入零拷贝技术

什么是零拷贝技术

  • 零拷贝的特点是 CPU 不全程负责内存中的数据写入其他组件以及从其他数据读入内存这两个步骤,CPU 仅仅起到管理的作用

    • 零拷贝不是不进行拷贝,而是 CPU 不再全程负责数据拷贝时的搬运工作。如果数据本身不在内存中,那么必须先通过某种方式拷贝到内存中(这个过程 CPU 可以不参与),因为数据只有在内存中,才能被转移,才能被 CPU 直接读取计算
  • Linux 的零拷贝技术有多种实现策略,但根据策略可以分为如下几种类型

    • 减少甚至避免用户空间和内核空间之间的数据拷贝:在一些场景下,用户进程在数据传输过程中并不需要对数据进行访问和处理,那么数据在 Linux 的 Page Cache 和用户进程的缓冲区之间的传输就完全可以避免,让数据拷贝完全在内核里进行,甚至可以通过更巧妙的方式避免在内核里的数据拷贝。这一类实现一般是是通过增加新的系统调用来完成的,比如 Linux 中的 mmap(),sendfile() 以及 splice() 等。
    • 绕过内核的直接 I/O:允许在用户态进程绕过内核直接和硬件进行数据传输,内核在传输过程中只负责一些管理和辅助的工作。这种方式其实和第一种有点类似,也是试图避免用户空间和内核空间之间的数据传输,只是第一种方式是把数据传输过程放在内核态完成,而这种方式则是直接绕过内核和硬件通信,效果类似但原理完全不同
    • 内核缓冲区和用户缓冲区之间的传输优化:这种方式侧重于在用户进程的缓冲区和操作系统的页缓存之间的 CPU 拷贝的优化。这种方法延续了以往那种传统的通信方式,但更灵活
  • 常见的零拷贝的实现有以下四种

    • sendfile
      • 基于Page Cache与DMA技术实现,通过使用 DMA 技术以及传递文件描述符,实现了 zero copy
    • mmap
      • 同样基于Page Cache与DMA技术实现,但是实现方式与sendfile不同:将内核空间地址映射为用户空间地址,write 操作直接作用于内核空间。通过 DMA 技术以及地址映射技术,用户空间与内核空间无须数据拷贝,实现了 zero copy
    • Direct I/O
      • 不基于Page Cache实现,读写操作直接在磁盘上进行,不使用 page cache 机制,通常结合用户空间的用户缓存使用。通过 DMA 技术直接与磁盘/网卡进行数据交互,实现了 zero copy

DMA技术

  • DMA技术依赖于硬件DMAC即DMA控制器,在进行内存和 I/O 设备的数据传输的时候,我们不再通过 CPU 来控制数据传输,而直接通过 DMA 控制器(DMA Controller,简称 DMAC)来执行控制

    • 比如说,我们用千兆网卡或者硬盘传输大量数据的时候,如果都用 CPU 来搬运的话,肯定忙不过来,所以可以选择 DMAC。而当数据传输很慢的时候,DMAC 可以等数据到齐了,再发送信号,给到 CPU 去处理,而不是让 CPU 在那里忙等待
  • 使用DMA之后的系统结构如下图

    image-20210907152312147
  • DMA技术是由一定限制的,DMA 仅仅能用于设备之间交换数据时进行数据拷贝,但是设备内部的数据拷贝还需要 CPU 进行

    • 典型场景比如说DMA只能完成硬盘或者网卡到内存的数据传递,但是内存内部的buffer之间的拷贝,仍然需要CPU负责执行

Page Cache技术

  • Page Cache以Page为单位,缓存文件内容。缓存在Page Cache中的文件数据,能够更快的被用户读取。同时对于带buffer的写入操作,数据在写入到Page Cache中即可立即返回,而不需等待数据被实际持久化到磁盘,进而提高了上层应用读写文件的整体性能
  • 与page cache类似的一个技术叫做buffer cache
    • 其目的在于避免低效的磁盘扇区为基本单位的磁盘访问,内核会在磁盘sector上构建一层缓存,他以sector的整数倍力度单位(block),缓存部分sector数据在内存中,当有数据读取请求时,他能够直接从内存中将对应数据读出。当有数据写入时,他可以直接再内存中直接更新指定部分的数据,然后再通过异步方式,把更新后的数据写回到对应磁盘的sector中。这层缓存则是块缓存Buffer Cache
      • 对于磁盘访问的优化无外乎就是缓存异步批量读写

sendfile

使用场景

  • 用户从磁盘读取一些文件数据后不需要经过任何计算与处理就通过网络传输出去(无害通过)。此场景的典型应用是消息队列
    • 对应的如果用户程序需要对拷贝的数据进行写操作则不能使用sendfile,因为用户态的进程无法从sendfile的调用中获得数据

原理

  • sendfile 主要使用到了两个技术:

    • DMA 技术

      • sendfile 依赖于 DMA 技术,将四次 CPU 全程负责的拷贝与四次上下文切换减少到两次
        • 硬盘到page cache的拷贝与socket buffer到网卡的数据拷贝由DMA负责
        • 不需要用户态执行write与read两个系统调用将数据拷贝到用户态(因为应对的是无害通过的场景,没有必要将数据拷贝到用户态),只需要一次系统调用,将数据从内存中的page cache转移到socket buffer或者反之即可
    • 传递文件描述符代替数据拷贝

      • 因为page cache 以及 socket buffer 都在内核空间中,并且数据传输过程前后没有任何写操作,所以可以直接传递文件描述符,而不是数据拷贝

        注意事项:只有网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术才可以通过传递文件描述符的方式避免内核空间内的一次 CPU 拷贝。这意味着此优化取决于 Linux 系统的物理网卡是否支持(Linux 在内核 2.4 版本里引入了 DMA 的 scatter/gather – 分散/收集功能,只要确保 Linux 版本高于 2.4 即可)

    image-20210907153901004

优缺点分析

  • 优点不言自明,主要就是在数据无害通过的场景下,可以直接从内核态的缓存中进行数据转移,并且使用DMA减轻CPU负担
  • 缺点主要在于使用场景是有限的,如果有写数据的需求则不能使用sendfile

Direct I/O

使用场景

  • 此技术常用于数据库系统中,MySQL中即使用到了Direct IO,普通IO要走read/write系统调用,需要定位inode然后定位磁盘地址,然后直接IO就是直接对磁盘读写,buffer IO是有缓存的。只是这点区别而已,性质是一样的
  • 在DB系统中,DB会将数据缓存在自己的cache中,换入、换出算法由DB控制,因为DB比kernel更了解哪些数据应该换入换出,比如innodb的索引,要求常驻内存,而redo log直接写入磁盘就好

原理

  • Direct IO的特点就在于Direct

    • 不基于系统缓存(page cache)直接与磁盘交互

      • 数据交互过程依赖于DMA技术
    • 数据直接存储到用户态的进程缓存中,而不经过内核(意味着没有系统调用,没有用户态到内核态的切换)

      • 缓存作为必要的性能优化手段是肯定会用的,只不过在这里使用的是用户级别的自定义的缓存,而不是系统控制的系统缓存

      事实上,即使 Direct I/O 还是可能需要使用操作系统的 fsync 系统调用。为什么?

      这是因为虽然文件的数据本身没有使用任何缓存,但是文件的元数据仍然需要缓存,包括 VFS 中的 inode cache 和 dentry cache 等

      在部分操作系统中,在 Direct I/O 模式下进行 write 系统调用能够确保文件数据落盘,但是文件元数据不一定落盘。如果在此类操作系统上,那么还需要执行一次 fsync 系统调用确保文件元数据也落盘。否则,可能会导致文件异常、元数据确实等情况。MySQL 的 O_DIRECT 与 O_DIRECT_NO_FSYNC 配置是一个具体案例

  • Direct I/O 的读写非常有特点

    • Write 操作:由于其不使用 page cache,所以其进行写文件,如果返回成功,数据就真的落盘了(不考虑磁盘自带的缓存);
    • Read 操作:由于其不使用 page cache,每次读操作是真的从磁盘中读取,不会从文件系统的缓存中读取

image-20210907163539836

优缺点分析

  • Linux 中的直接 I/O 技术省略掉缓存 I/O 技术中操作系统内核缓冲区的使用,数据直接在应用程序地址空间和磁盘之间进行传输,从而使得自缓存应用程序可以省略掉复杂的系统级别的缓存结构,而执行程序自己定义的数据读写管理,从而降低系统级别的管理对应用程序访问数据的影响
    • 对应的缺点如下
      • 如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘进行加载,这种直接加载会非常缓慢
      • 在应用层引入直接 I/O 需要应用层自己管理,这带来了额外的系统复杂性
  • 与其他零拷贝技术一样,避免了内核空间到用户空间的数据拷贝,如果要传输的数据量很大,使用直接 I/O 的方式进行数据传输,而不需要操作系统内核地址空间拷贝数据操作的参与,这将会大大提高性能
    • 对应的引入的缺陷为:由于设备之间的数据传输是通过 DMA 完成的,因此用户空间的数据缓冲区内存页必须进行 page pinning(页锁定),这是为了防止其物理页框地址被交换到磁盘或者被移动到新的地址而导致 DMA 去拷贝数据的时候在指定的地址找不到内存页从而引发缺页错误,而页锁定的开销并不比 CPU 拷贝小,所以为了避免频繁的页锁定系统调用,应用程序必须分配和注册一个持久的内存池,用于数据缓冲

mmap(mmap+write)

使用场景

  • mmap本质上就是使用内存映射的方式实现零拷贝,其使用场景比较侧重并发下的使用(由操作系统保证线程安全与线程可见性)
  • 多个线程以只读的方式同时访问一个文件,这是因为 mmap 机制下多线程共享了同一物理内存空间,因此节约了内存
  • mmap 非常适合用于进程间通信,这是因为对同一文件对应的 mmap 分配的物理内存天然多线程共享,并可以依赖于操作系统的同步原语(由操作系统保证线程安全与线程可见性)
  • mmap 虽然比 sendfile 等机制多了一次 CPU 全程参与的内存拷贝,但是用户空间与内核空间并不需要数据拷贝,因此在正确使用情况下并不比 sendfile 效率差;

原理

  • mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read、write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享

    • 多个虚拟地址可以指向同一段物理内存地址,当内核虚拟地址与用户空间虚拟地址指向同一个物理地址时,就可以高效的进行用户态和内核态的数据共享,而不是笨拙的数据复制
  • mmap的特点如下:

    • mmap 向应用程序提供的内存访问接口是内存地址连续的,但是对应的磁盘文件的 block 可以不是地址连续的

    • mmap 提供的内存空间是虚拟空间(虚拟内存),而不是物理空间(物理内存),因此完全可以分配远远大于物理内存大小的虚拟空间(例如 16G 内存主机分配 1000G 的 mmap 内存空间)

    • mmap 负责映射文件逻辑上一段连续的数据(物理上可以不连续存储)映射为连续内存,而这里的文件可以是磁盘文件、驱动假造出的文件(例如 DMA 技术)以及设备

    • mmap 由操作系统负责管理,对同一个文件地址的映射将被所有线程共享,操作系统确保线程安全以及线程可见性

    • mmap 下进程可以采用指针的方式进行磁盘文件读写操作

image-20210907193759752

  • 上图所示为mmap的IO模型

    • 利用 DMA 技术来取代 CPU 来在内存与其他组件之间的数据拷贝,例如从磁盘到内存,从内存到网卡
    • 用户空间的 mmap file 使用虚拟内存,实际上并不占据物理内存,只有在内核空间的 kernel buffer cache 才占据实际的物理内存
    • mmap() 函数需要配合 write() 系统调动进行配合操作,这与 sendfile() 函数有所不同,后者一次性代替了 read() 以及 write();因此 mmap 也至少需要 4 次上下文切换
    • mmap 仅仅能够避免内核空间到用户空间的全程 CPU 负责的数据拷贝,但是内核空间内部还是需要全程 CPU 负责的数据拷贝
  • 利用 mmap() 替换 read(),配合 write() 调用的整个流程如下

    • 用户进程调用 mmap(),从用户态陷入内核态,将内核缓冲区映射到用户缓存区;

      • 使用mmap构建用户态的mmap file到内核态的page cache的映射,避免了内存页的数据从内核空间拷贝到用户空间
    • DMA 控制器将数据从硬盘拷贝到内核缓冲区(可见其使用了 Page Cache 机制)

    • mmap() 返回,上下文从内核态切换回用户态;

    • 用户进程调用 write(),尝试把文件数据写到内核里的套接字缓冲区,再次陷入内核态;

    • CPU 将内核缓冲区中的数据拷贝到的套接字缓冲区(CPU Copy);

    • DMA 控制器将数据从套接字缓冲区拷贝到网卡完成数据传输;

    • write() 返回,上下文从内核态切换回用户态

优缺点

优点
  • 读写效率提高:避免内核空间到用户空间的数据拷贝
    • mmap 被认为快的原因是因为建立了页到用户进程的虚地址空间映射,以读取文件为例,避免了页从内核空间拷贝到用户空间
  • 简化用户进程编程
    • 基于缺页异常的懒加载
    • 数据一致性由 OS 确保
缺点
  • 由于 mmap 使用时必须实现指定好内存映射的大小,因此 mmap 并不适合变长文件
  • 如果更新文件的操作很多,mmap 避免两态拷贝的优势就被摊还,最终还是落在了大量的脏页回写及由此引发的随机 I/O 上,所以在随机写很多的情况下,mmap 方式在效率上不一定会比带缓冲区的一般写快;
  • 读/写小文件(例如 16K 以下的文件),mmap 与通过 read 系统调用相比有着更高的开销与延迟;同时 mmap 的刷盘由系统全权控制,但是在小数据量的情况下由应用本身手动控制更好;
  • mmap 受限于操作系统内存大小:例如在 32-bits 的操作系统上,虚拟内存总大小也就 2GB,但由于 mmap 必须要在内存中找到一块连续的地址块,此时你就无法对 4GB 大小的文件完全进行 mmap,在这种情况下你必须分多块分别进行 mmap,但是此时地址内存地址已经不再连续,使用 mmap 的意义大打折扣,而且引入了额外的复杂性

从应用的角度理解零拷贝技术的使用

kafka

  • Kafka 作为一个消息队列,涉及到磁盘 I/O 主要有两个操作:

    • Provider 向 Kakfa 发送消息,Kakfa 负责将消息以日志的方式持久化落盘;
    • Consumer 向 Kakfa 进行拉取消息,Kafka 负责从磁盘中读取一批日志消息,然后再通过网卡发送。
  • Kakfa 服务端接收 Provider 的消息并持久化的场景下使用 mmap 机制,能够基于顺序磁盘 I/O 提供高效的持久化能力,使用的 Java 类为 java.nio.MappedByteBuffer。

  • Kakfa 服务端向 Consumer 发送消息的场景下使用 sendfile 机制,这种机制主要两个好处:

    • sendfile 避免了内核空间到用户空间的 CPU 全程负责的数据移动
    • sendfile 基于 Page Cache 实现,因此如果有多个 Consumer 在同时消费一个主题的消息,那么由于消息一直在 page cache 中进行了缓存,因此只需一次磁盘 I/O,就可以服务于多个 Consumer
  • 使用 mmap 来对接收到的数据进行持久化,使用 sendfile 从持久化介质中读取数据然后对外发送是一对常用的组合。但是注意,你无法利用 sendfile 来持久化数据,利用 mmap 来实现 CPU 全程不参与数据搬运的数据拷贝

MySQL

  • 待整理

Java

参考