零拷贝
“零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。
零拷贝给我们带来的好处
1、减少甚至完全避免不必要的CPU拷贝,从而让CPU解脱出来去执行其他的任务
2、减少内存带宽的占用
3、通常零拷贝技术还能够减少用户空间和操作系统内核空间之间的上下文切换
零拷贝的实现
零拷贝实际的实现并没有真正的标准,取决于操作系统如何实现这一点。零拷贝完全依赖于操作系统。操作系统支持,就有;不支持,就没有。不依赖Java本身。
传统I/O
在Java中,我们可以通过InputStream从源数据中读取数据流到一个缓冲区里,然后再将它们输入到OutputStream里。这种IO方式传输效率是比较低的。
1、程序发起read()系统调用。系统由用户空间向内核空间发起读取数据的系统调用,系统收到之后,系统由用户空间模式切换为内核空间模式(第一次上线文切换),系统向硬盘(文件是存在硬盘上)发起读取数据的请求,磁盘上的数据文件会通过DMA(Direct Memory Access){直接内存访问}的方式读取到内核空间的缓冲区(kernel buffer)。{DMA过程中CPU不需要参与数据的读写,而是DMA处理器直接将硬盘数据通过总线传输到内存中}{CPU不参与数据的读写}{
2、程序要读取的数据文件已经写入内核空间的缓冲区以后,程序会将数据文件原封不动的由内核空间的缓存区,拷贝到用户空间的缓存区中(数据文件在内核空间中,用户空间是没有办法使用的){CPU参与数据的读写}{第一次拷贝},系统由内核空间的模式切换为用户空间的模式(第二次上下文切换)。有业务需求,执行相应的业务。
3、程序发起write()系统调用(目的:发送到网络的另外一端)。系统由用户空间向内核空间发起写入数据的系统调用,系统收到之后,系统由用户空间模式切换到内核空间模式(第三次上下文切换),将之前内核空间拷贝到用户空间的文件数据又原封不动(之前由内核空间拷贝到用户空间之后没有对数据文件做修改的话)的拷贝到内核空间的缓冲区(网络缓冲区{Socket Buffer}[跟上面提到的缓冲区是两个概念]){第二次拷贝}{CPU参与数据的读写}。
4、内核空间会将文件数据写入到网络中(传输),然后write()返回结果给用户空间,系统由内核空间切换到用户空间(第四次上下文切换)
传统的I/O方式会经过4次用户空间和内核空间的切换(上下文切换),CPU中内存中进行数据读写的过程。这种拷贝过程相对来说比较消耗资源,因为用户空间只是作为媒介(临时存放数据的载体),用户空间不做任何处理的话。如果用户空间对数据库文件做额外处理,则无法避免。
内存映射方式I/O
系统调用方法,这种方式的I/O原理就是将用户空间缓冲区(user buffer)的内存地址和内核空间缓冲区(kernel buffer)的内存地址做一个映射,也就是说系统在用户态可以直接读取并操作内核空间的数据。
这是使用的系统调用方法,这种方式的I/O原理就是将用户空间缓冲区(user buffer)的内存地址和内核缓冲区(kernel buffer)的内存地址做一个映射,也就是说系统在用户态可以直接读取并操作内核空间的数据。
1、系统调用首先会使用DMA的方式将文件数据读取到内核空间缓冲区,然后通过内存映射的方式,使用户空间缓冲区和内核空间读缓冲区的内存地址为同一内存地址,也就是说不需要CPU再讲数据从内核空间读缓冲区复制到用户空间缓冲区。
2、当使用write()系统调用的时候,cpu将内核空间缓冲区(等同于用户空间缓冲区)的数据直接写入到网络发送缓冲区(socket buffer),然后通过DMA的方式将数据传入到网卡驱动程序中准备发送。
可以看到这种内存映射的方式减少了CPU的读写次数,但是用户空间到内核空间的切换(上下文切换)减少了两次,同时需要注意在进行这种内存映射的时候,有可能会出现并发线程操作同一块内存区域而导致的严重的数据不一致问题,所以需要进行合理的并发编程来解决这些问题。
通过sendfile实现的零拷贝I/O
1、通过sendfile()系统调用,可以做到内核空间内部直接进行I/O传输。
2、sendfile()系统调用也会引起用户态到内核态的切换,与内存映射方式不同的是,用户空间此时是无法看到或修改数据内容,也就是说这是一次完全意义上的数据传输过程。
3、从磁盘读取到内存是DMA的方式,从内核空间读缓冲区读取到网络发送缓冲区,依旧需要CPU参与拷贝,而从网络发送缓冲区到网卡中的缓冲区依旧是DMA方式。
依旧有一次CPU进行数据拷贝,两次用户空间和内核空间的切换操作,相比较于内存映射的方式有了很大的进步,但问题是程序不能对数据进行修改,而只是单纯地进行了一次数据的传输过程。
理想状态下的零拷贝I/O
依旧是系统调用sendfile()
可以看到,这是真正意义上的零拷贝,因为其间CPU已经不参与数据的拷贝过程,也就是说完全通过其他硬件和中断的方式来实现数据的读写过程吗,但是这样的过程需要硬件的支持才能实现。
系统调用sendfile()发起后,磁盘数据通过DMA方式读取到内核空间缓冲区(kernel buffer),内核空间缓冲区中的数据通过DMA聚合网络缓冲区(内核空间拷贝文件描述符的信息到socket buffer{文件数据的内存地址和文件数据的大小}),协议引擎会(protocal engine)真正完成数据的发送通过聚集的方式(Gathering:通过读取kernel buffer 和socket buffer的信息)。
可以看到在这种模式下,是没有一次CPU进行数据拷贝的,所以就做到了真正意义上的零拷贝,虽然和前一种是同一个系统调用,但是这种模式实现起来需要硬件的支持,但对于基于操作系统的用户来讲,操作系统已经屏蔽了这种差异,它会根据不同的硬件平台来实现这个系统调用(linux 2.4 之后的版本已实现)
参考
https://www.jianshu.com/p/497e7640b57c