传统的文件传输有啥缺点?
传统IO的工作方式是,数据读取和写入是从用户空间和内核空间来回复制,内核空间的数据时通过操作系统层面的IO接口从磁盘读取或写入。
通过上图可以看出,在我们执行read和writer之间,一共发生了4次用户态和内核态上下文切换,在高并发的场景下,用户态和内核态上下文切换带来的性能消耗将会极大的降低系统的性能。
除了上下文的切换,在这个过程也一共发生了4次数据拷贝,其中包含两次DMA拷贝,两次CPU拷贝。
- 第一次拷贝:借助DMA完成,把磁盘上的数据拷贝到内核的缓冲区里
- 第二次拷贝:借助CPU完成,把内核缓冲区的数据拷贝到用户的缓冲区,以便应用程序可以操作这些数据
- 第三次拷贝:把用户缓冲区数据再拷贝到内核中Sokcet的缓冲区中,需要借助CPU完成
- 第四次拷贝:借助DMA完成,将内核Socket缓冲区中的数据拷贝到网卡的缓冲区中
可以看到我们在一个主机上如果想把某个文件的内容通过网络发送出去,将会进行4次切换和4次拷贝,但在这4次拷贝的过程中拷贝的都是同一份数据,过多的数据拷贝造成了系统性能的下降。
因此,为了优化文件的传输性能,我们需要减少上下切换和拷贝的次数。
如何实现零拷贝?
- mmap + write
- sendfile
mmap + write
read()系统调用会把内核缓冲区的内容拷贝到用户空间,为了减少这一次的拷贝,我们使用mmap()系统调用替换掉read()
mmap函数会直接把内核缓冲区的数据映射到用户空间,这样操作系统内核和用户空间就不需要再进行数据的拷贝。
- 应用进程调用mmap函数以后,DMA会把磁盘的数据拷贝到内核缓冲区里,接着应用程序会和操作系统内核共享这个缓冲区
- 应用进程再调用write函数,操作系统直接将内核中的数据拷贝到Socket缓冲区,这一步需要借助CPU
- 最后再借助DMA将Socket缓冲区的数据拷贝到网卡缓冲区里
mmap + write的实现方式需要3次数据拷贝和4次上下文切换
sendfile
sendfile是Linux内核2.1版本中专门发送文件的系统调用函数,函数形式如下:
- # include <sys/socket.h>
- ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
- out_fd:目的端文件描述符
- in_fd:源端文件描述符
- offset:源端的偏移量
- count:复制数据的长度
- ssize_t:返回参数,实际复制数据的长度
sendfile可以替代read()和write()这两个系统调用,因此系统调用从2次变为1次,相应的上下文切换也会变为2次。
其次,sendfile可以直接把内核缓冲区的数据拷贝到Socket缓冲区,因此通过sendfile函数,我们可以将上下文切换减少为2次,数据拷贝3次。
如何实现真正的零拷贝?
上述零拷贝还不是真正的零拷贝,如果网卡支持SG-DMA技术的话,我们可以进一步减少数据拷贝的次数(即减少CPU把内核缓冲区的内容拷贝到Socket缓冲区的过程)。
$ ethtool -k eth0 | grep scatter-gather
上述命令可以查看网卡是否支持SG-DMA技术。
在Linux内核2.4y以后,对于网卡支持SG-DMA技术的情况下,sendfile系统调用的过程也发生了变化:
- 首先通过DMA拷贝将磁盘的数据拷贝到内核缓冲区
- 第二步,将缓冲区描述符和数据长度传到Socket缓冲区,网卡的SG-DMA控制器可以直接将内核缓冲区的数据拷贝到网卡的缓冲区里
通过以上技术,我们真正实现了零拷贝,数据拷贝次数发生两次,并且我们全程没有让CPU介入数据拷贝过程,通过DMA技术实现了数据的拷贝。总体来看,零拷贝技术可以把文件性能至少提高一倍以上。
PageCache是什么?
在我们上面一直提到一个内核缓冲区,该内核缓冲区就是PageCache(磁盘高速缓存)。
PageCache的优点?
- PageCache存在于内存中,读写内存速度远远快于读写磁盘速度
- 根据程序局部性规则,刚刚访问的数据在短时间内被访问的概率很高,因此PageCache缓存了最近被访问的数据,当读磁盘数据时,优先在PageCache中查找,如果数据存在可以直接返回,如果没有就需要从磁盘中读取,当空间不足时需要淘汰最久未被访问的缓存
- PageCache使用了预读功能,比如read方法每次只读取32kb,但内核实际上会将后面的32kb-64kb的内容也读取到PageCache,这样对于读取后续的数据成本就会变低
PageCache的缺点?
- 不适合传输大文件(GB级别),在传输大文件时,PageCache会失效,也就是说DMA的数据拷贝是一次多余的操作。
- PageCache如果长时间被大文件占据,热点的小文件就无法使用到PageCache
所以针对大文件的传输,不应该使用零拷贝技术。
如何解决大文件传输问题?
异步IO + 直接IO。
异步IO主要解决read方法调用时的阻塞问题,通过上图可以看出:
- 在内核向磁盘发起读请求时,可以不等待数据就位就可以返回,此时进程依旧可以处理其他任务
- 当磁盘中的数据拷贝到用户缓冲区时,进程将收到内核的通知,此时进程就可以去处理数据了
异步IO整个过程没有涉及到PageCache,绕开PageCache的IO也可以成为直接IO。通常对于磁盘来说,异步IO只支持直接IO。
所以在传输大文件时,可以使用异步IO+直接IO无阻塞的读取文件。
直接IO的使用场景?
- 应用程序已经实现了磁盘数据的缓存,那么便不再需要PageCache了。在MySQL数据库中,可以通过参数开启直接IO
- 传输大文件时,由于大文件无法命中PageCache缓存,因此也应该使用直接IO