
前言
总所周知,磁盘读写速度令人堪忧,特别是机械硬盘,读写速度相差内存 10 倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝、直接 I/O、异步 I/O 等等,下面我们就分几个点来讲讲零拷贝技术。
DMA技术是什么?
直接内存访问(Direct Memory Access) 技术,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务
在没有 DMA 技术前,I/O 的过程是这样的:
- CPU发起IO指令给磁盘
- 磁盘收到指令,把数据放入磁盘缓冲区,然后产生一个中断;
- CPU 收到中断信号后,把磁盘缓冲区的数据读进内核缓冲区,然后再把内核缓冲区写入到用户缓冲区,而在数据传输的
期间 CPU 是无法执行其他任务的
。

有 DMA 技术之后,I/O 的过程是这样的:

可以看到, 整个数据传输的过程,CPU 不再参与数据搬运的工作,而是全程由 DMA 完成,大大提高了CPU的性能。
传统IO
传统文件传输,将磁盘上的文件读取出来,然后通过网络协议发送给客户端。

流程: 磁盘->内核缓冲区->用户缓存区->socket缓冲区->网卡缓冲区
分析: 4次拷贝(2次DMA,2次CPU),4次状态切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态
结论: 想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数
- 要想减少上下文切换到次数,就要减少系统调用的次数
- 在用户空间我们并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的
mmap内存映射
针对传统IO的缺陷,一个优化方案就是通过mmap系统替代read系统调用,这个函数的作用是让用户缓冲区共享内核缓冲区

流程: 硬盘->内核缓冲区=用户缓存区->socket缓冲区->网卡缓冲区
分析: 3次拷贝,4次状态切换,因为mmap系统调用也是2次状态切换
结论: 但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换
sendFile
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数
sendfile()
,它可以替代前面的read()
和write()
这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。

流程: 硬盘->内核缓冲区->socke缓冲区->网卡缓冲区
分析: 3次拷贝,2次状态切换,只有一次sendFile系统调用,2次状态切换
优化: linux2.4后续优化:硬盘->内核缓冲区->网卡缓冲区

2次拷贝,2次状态切换,源头到目标,零拷贝,全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的,这就是真正意义上的零拷贝
。
应用场景
事实上,Kafka 这个开源项目,就利用了「零拷贝」技术,从而大幅提升了 I/O 的吞吐率,这也是 Kafka 在处理海量数据为什么这么快的原因之一。
如果你追溯 Kafka 文件传输的代码,你会发现,最终它调用了 Java NIO 库里的 transferTo
方法:
arduino
@Overridepublic
long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}
如果 Linux 系统支持 sendfile()
系统调用,那么 transferTo()
实际上最后就会使用到 sendfile()
系统调用函数。