零拷贝
以文件传输作为切入点,分析零拷贝原理。
1. 为什么要有DMA技术?
在没有DMA技术前,I/O过程如下:
- CPU发出对应指令给磁盘控制器,然后返回;
- 磁盘控制器收到指令后,开始准备数据并放入磁盘控制器的内部缓冲区中,然后产生一个中断;
- CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。
缺陷是CPU需要参与整个数据传输过程。于是有了DMA技术(Direcr Memory Access)即直接内存访问技术。在进行I/O设备和内存的数据传输的时候,数据搬运的工作全部交给DMA控制器,而CPU不再参与任何与数据搬运相关的事情,这样CPU就可以去处理别的事务。
DMA收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用CPU,CPU可以执行其他任务。
2. 传统文件传输有多糟糕?
如果服务端要提供文件传输功能,传统的I/O工作方式,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的I/O接口从磁盘读取或写入。一般需要两个系统调用:
过程如下:
期间发生了4次用户态与内核态的上下文切换,还发生了4次数据拷贝,其中两次DMA的拷贝,两次CPU拷贝。
- 第一次拷贝,把磁盘上的数据拷贝到用户缓冲区里,通过DMA完成。
- 第二次拷贝,把内核缓冲区的数据拷贝到用户缓冲区中,由CPU完成。
- 第三次拷贝,把刚才拷贝到用户缓冲区中的数据再拷贝到内核socket的缓冲区中,由CPU完成。
- 第四次拷贝,把内核socket缓冲区里的数据,拷贝到网卡的缓冲区中,由DMA完成。
要想提高文件传输的性能,就需要减少用户态与内核态的上下文切换和内存拷贝的次数。
3. 如何优化文件传输的性能?
发生上下文切换是因为用户空间没有权限操作磁盘或网卡,所以,要想减少上下文切换的次数,就要减少系统调用的次数。
文件传输场景中,用户空间不会对数据加工,所以用户缓冲区是没必要存在的。这样就可以减少数据拷贝次数。
4. 如何实现零拷贝?
零拷贝技术实现方式有两种:
- mmap+write
- sendfile
mmap+write
read()
的系统调用会把内核缓冲区数据拷贝到用户缓冲区,为了减少这一步开销,可以使用mmap()
替代read()
系统调用函数。
mmap()
系统调用函数会直接把内核缓冲区的数据映射到用户空间,这样,操作系统内核与用户空间就不需要在进行任何数据拷贝操作。
这样就可以减少一次数据拷贝过程。但仍然有四次上下文切换。
sendfile
Linux内核版本2.1中,提供了一个专门发送文件的系统调用函数sendfile()
,可以替代前面的read()和write()
这两个系统调用。这样就可以减少一次系统调用,即减少两次上下文切换的开销,其次,该系统调用可以直接把内核缓冲区里的数据直接拷贝到socket缓冲区中,减少一次数据拷贝。
但这还不是真正的零拷贝技术,如果网卡支持SG-DMA技术,可以进一步减少通过CPU把内核缓冲区数据拷贝到socket缓冲区的过程。
对于Linux内核版本2.4,如果网卡支持SG-DMA技术,sendfile()
系统调用有如下变化:
- 第一步,通过DMA将磁盘上数据拷贝到内核缓冲区中;
- 第二步,缓冲区描述符和数据长度传到socket缓冲区,这样网卡的SG-DMA控制器就可以直接将内核缓冲区中的数据拷贝到网卡缓冲区中。
此过程只需要2次数据拷贝。
所谓的零拷贝(Zero-copy)技术,没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
5. PageCache有什么作用?
上述的第一步,将磁盘文件拷贝到内核缓冲区,这个内核缓冲区实际上就是磁盘高速缓存(PageCache)。则零拷贝使用了PageCache技术。
读写磁盘相比读写内存速度慢很多,因此通过DMA把磁盘数据搬运到内存中,这样就可以用读内存替换读磁盘。但由于内存空间小,因此PageCache缓存最近被访问数据和预读功能。
但是对于大文件(GB级别)传输的时候,PageCache 会不起作用,那就白白浪费 DMA 多做的一次数据拷贝,造成性能的降低,即使使用了 PageCache 的零拷贝也会损失性能。
所以,针对大文件的传输,不应该使用 PageCache,也就是说不应该使用零拷贝技术,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,这样在高并发的环境下,会带来严重的性能问题。
6. 大文件传输用什么方式实现?
当调用read方法读取文件时,进程实际上会阻塞read方法的调用,因为要等待磁盘数据的返回,如下图:
对于阻塞问题,可以使用异步I/O来解决:
他将读操作分为两部分:
- 前半部分,内核向磁盘发起读请求,但是可以不等待数据就位就可以返回,于是进程此时可以处理其他任务;
- 后半部分,当内核将磁盘中的数据拷贝到进程缓冲区后,进程收到内核通知,再去处理数据。
异步I/O并没有涉及到PageCache,所以使用异步I/O就要绕开PageCache,绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O。
因此,在高并发场景下,针对大文件的传输方式,应该使用异步I/O+直接I/O来代替零拷贝技术。