NIO的零拷贝
零拷贝(zero-copy)是一种目前只有在使用 NIO 和 Epoll 传输时才可使用的特性。零拷贝 指不是不拷贝,而且不是使用CPU拷贝。带来了更少的数据复制,更新的上下文切换,更少的CPU缓存等
详细参考 NIO和零拷贝
-
传统I/O拷贝
当执行read() 和 write() 操作的时候 如何使用变化的- (切换)当执行read操作的时候,会从 用户态(user context) 切换成 内核态(kernel context)
- (拷贝)然后通过DMA(Direct memory access,直接内存拷贝,不使用CPU)拷贝,被数据从硬盘(hard drive) 拷贝到 内核缓冲区(kernel buffer)
- (切换)系统在从 内核态(kernel context) 切换成 用户态(user context)
- (拷贝)在 使用 CPU 把数据从 内核缓冲区(kernel buffer) 拷贝到 用户缓冲区(user buffer),数据在用户缓冲区进行修改。
- (拷贝)在 使用 CPU 把数据从 用户缓冲区(user buffer)拷贝到 Socket缓冲区(Socket buffer)
- (切换)系统在从 用户态(user context) 切换成 内核态(kernel context)
- (拷贝)在使用DMA从 Socket缓冲区(Socket buffer) 拷贝到协议栈(protocol engine)
- (切换)系统在从 内核态(kernel context) 切换成 用户态(user context)
传统IO 一共进过了 4次切换,4次拷贝,两次DMA拷贝 两次CPU拷贝
-
内存映射方式I/O (mmap优化)
mmap 通过内存映射优化,将文件映射到内核缓冲区,同时用户空间可以共享内核的数据,这样就减少了一次CPU拷贝 。
也就是少了传统I/O拷贝中的第4步,不在 使用 CPU 把数据从 内核缓冲区(kernel buffer) 拷贝到 用户缓冲区(user buffer)。因为内核缓冲区和用户缓冲区的数据是共享的
内存映射方式一共进过了 4次切换,3次拷贝,两次DMA拷贝 一次CPU拷贝
- 内核空间内部传输I/O (sendFile)
Linux 2.1 版本提供了sendFile函数,其基本原理是:数据根本不进过用户态,直接从内核缓冲区进入到 SocketBuffer。同时因为和用户态无关系,所以就减少了一次上下文切换
- (切换)sendfile()的时候,会从 用户态(user context) 切换成 内核态(kernel context)
- (拷贝)然后通过DMA(Direct memory access,直接内存拷贝,不使用CPU)拷贝,被数据从硬盘(hard drive) 拷贝到 内核缓冲区(kernel buffer)
- (切换)系统在从 内核态(kernel context) 切换成 用户态(user context)
- (拷贝)在 使用 CPU 把数据从 内核缓冲区(kernel buffer)拷贝到 Socket缓冲区(Socket buffer)
- 在使用DMA从 Socket缓冲区(Socket buffer) 拷贝到协议栈(protocol engine)
内核空间内部传输I/O (sendFile) 进行了 2次切换,3次拷贝,两次DMA拷贝 一次CPU拷贝
-
升级版-内核空间内部传输I/O (sendFile优化)
- (切换)sendfile()的时候,会从 用户态(user context) 切换成 内核态(kernel context)
- (拷贝)然后通过DMA(Direct memory access,直接内存拷贝,不使用CPU)拷贝,被数据从硬盘(hard drive) 拷贝到 内核缓冲区(kernel buffer)
- (切换)系统在从 内核态(kernel context) 切换成 用户态(user context)
- (拷贝 可忽略不计)在 使用 CPU 把少量关键数据从 内核缓冲区(kernel buffer)拷贝到 Socket缓冲区(Socket buffer)
- (拷贝)在使用DMA从 内核缓冲区(kernel buffer) 拷贝到协议栈(protocol engine)
升级版-内核空间内部传输I/O (sendFile) 进行了 2次切换,2次拷贝,两次DMA拷贝 一次CPU拷贝可忽略不计
-
总结
- mmap 适合小数据量的读写,sendFile适合大文件传输
- mmap需要4次上下文切换,3次数据拷贝。而sendFile只需要3次上下文切换,2次数据拷贝
- sendFile利用DMA方式,较少了CPU拷贝。而mmap则必须要走socker缓冲区
-
举例
// 直接使用了transferTo()进行通道间的数据0拷贝 fileChannel.transferTo(0, fileChannel.size(), socketChannel);