详解 I/O 优化
DMA技术
在没有 DMA 技术之前,I/O过程是这样的:
- CPU 发出对应的指令给磁盘控制器,饭后返回。
- 磁盘控制器收到指令后,于是就开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断。
- CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。
可以看到整个数据传输的过程,都需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来。
于是 DMA 技术问世了,也就是直接内存访问(Direct Memory Access) 技术。
什么是 DMA 技术?就是在进行 I/O 设备与内存的数据传输的时候,数据搬运的工作全部交给DMA控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务了。
使用 DMA 控制器进行数据传输的过程如下:
注意:图中的内核缓冲区就是上图中的 PageCache
可以看到,整个传输数据的过程,CPU 不再参与数据搬运的工作,而是全称由 DMA 完成,但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。
传统的文件传输
如果服务器要提供文件传输的功能,一般想到的就是将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统 I/O 的工作方式:数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
具体过程如下如所示:
首先,期间共发生了 4 次用户态与内核态的上下文切换,上下文切换的成本并不小,在高并发的场景下,影响系统性能。还发生了 4 次数据拷贝,两次是 DMA拷贝,两次是 CPU 拷贝。
我们只是搬运一份数据,结果却搬运了4次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。
这种简单又传统的传输方式,存在冗余的上下文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。
所以,想要提高文件传输的性能,就需要减少用户态与内核态的上下文切换和内存拷贝的次数。
优化传输性能
如何减少用户态与内核态的上下文切换的次数呢?
读取磁盘数据的时候,之所以要发生上下文切换,是因为用户空间没有权限操作磁盘或网卡,内核的权限最高,这些操作设备的过程都需要交由操作系统内核来完成,所以一般要通过内核去完成某些任务的时候,就需要使用操作系统提供的系统调用函数。
而一次系统调用必然会发生 2 次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。
所以,要减少上下文切换的次数,就要减少系统调用的次数。
如何减少数据拷贝的次数?
传统的文件传输方式会历经 4 次数据拷贝,而且这里面,从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区拷贝到 socket 缓存区,这个过程是没有必要的。
因为文件传输的应用场景中,在用户空间我们并不会对数据再加工,所以数据实际上不用搬运到用户空间,因此用户的缓存区是没有必要存在的。
如何实现零拷贝
零拷贝技术实现的方式通常有2中:
- mmap + write
- sendfile
mmap + write
前面我们知道,read()系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以使用 mmap() 替换 read()系统调用函数。
mmap()系统调用会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间不需要再进行任何的数据拷贝操作。
具体过程如下:
这还不是最理想的零拷贝,仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,仍然需要4次上下文切换,因为系统调用还是 2 次。
sendfile
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),它可以代替前面的 read()和 write()这两个系统调用,这样就可以减少一次系统调用,也就减少了2次上下文切换的开销。
该系统调用可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有2次上下文切换,和3次数据拷贝,如下图:
但是这不是真正的零拷贝技术,如果网卡支持 SG-DMA 技术,可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。于是从 Linux 2.4 版本开始,对于网卡支持 SG-DMA 技术的情况下,sendfile()系统调用的过程发生了点变化,具体过程如下:
这就是零拷贝技术,因为我们没有在内存层面去拷贝数据,也就是说全称没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
零拷贝技术的文件传输方式相比传统的文件传输方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝过程,都不需要通过 CPU,2次都是有 DMA 来搬运。
总体上看,零拷贝技术可以把文件传输的性能提高至少一倍以上。
使用零拷贝的项目:
- Kafka 的文件传输代码里面,调用了 Java NIO 库里面的 transferTO 方法,如果 Linux 系统支持 sendfile()系统调用,那么transferTo()实际上最后就会使用到 sendfile()系统调用函数。
- Nginx 也支持零拷贝技术,可在配置文件中配置。
参考:小林coding图解系统