磁盘可以说是计算机系统中最慢的硬件之一,读写速度相差内存10倍以上,所以针对优化磁盘的技术非常的多,比如:零拷贝,直接I/O,异步I/O等等。
这里我们就以文件传输为切入点,分析I/O工作方式,以及如何优化文件传输的性能。
DMA技术,全称为Direct Memory Access(又称直接内存访问技术)。
可以看到整个数据传输的过程:
故可以得知:在数据传输的过程,需要CPU亲自的去拷贝数据,并且在这期间CPU无法去做其他事情。简单的搬运几个字符没有问题,但是当处理大量数据的时候,如果每次都让CPU来搬运,显然忙不过来。
当有了DMA控制器以后,在进程向CPU发出read()调用以后,CPU向DMA控制器发起IO请求,然后DMA控制器再向磁盘发出IO请求。
当磁盘接收到IO请求以后,会进行数据准备工作,将数据放到磁盘数据缓冲区当中。
当磁盘的数据准备工作完成以后,不再向CPU发出中断信号,而是通知DMA控制器。
DMA控制器在接收到通知以后,将数据从磁盘控制器缓冲区中copy到内核缓冲区中。在这期间并不占用CPU,CPU可以处理其他的事情,执行其他的任务。
DMA控制器处理完之后向CPU发出中断信号,由CPU将数据从内核缓冲区copy至用户缓冲区中。copy完成之后read()调用返回,操作系统从内核态切换回用户态。
注意:起初的DMA控制器只在主板上,但是现在IO设备越来越多,数据传输的需求也不尽相同,所以现在每个IO设备中都有DMA控制器。
在进行read调用的时候,操作系统由用户态转为内核态,需要进行DMA拷贝和CPU拷贝各一次。(DMA拷贝是指由DMA控制器将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,CPU拷贝是指将数据从内核缓冲区中copy到用户缓冲区中),在read调用结束以后,操作系统再从内核态切换回用户态。
在进行write调用的时候,操作系统由用户态转为内核态,需要进行CPU拷贝和DMA拷贝各一次。(CPU拷贝是指将用户缓冲区的数据拷贝到socket缓冲区中。而DMA拷贝是指将socket缓冲区中的数据拷贝到网卡中。)
在文件传输场景下,我们无需在用户空间对数据进行再加工。因此用户缓冲区的存在是没有必要的。
实现零拷贝的技术主要有两种:mmap+write,sendfile
下面就谈一谈,他们是如何减少上下文切换和数据拷贝的次数的。
利用mmap替换read系统调用函数。
mmap系统调用函数会直接将内核缓冲区的数据映射到用户空间,这样操作系统内核与用户空间之间就不需要任何的拷贝操作。
具体流程如下:
应用进程调用了mmap系统调用函数之后,DMA会把磁盘缓冲区的数据拷贝到内核缓冲区中。接着,应用进程和操作系统内核共享这个缓冲区。
应用进程再调用write函数,操作系统直接将内核缓冲区的数据拷贝到socket缓冲区。再由DMA控制器copy到网卡。
这还不是最理想的零拷贝因为还是需要两次系统调用,四次上下文切换,3次拷贝。
在linux内核版本2.1中,提供了一个新的系统调用函数sendfile。这个系统调用函数可以代替read和write两个系统调用函数。
这样系统调用的次数就变成了一次,上下文切换的次数减小到了两次,数据拷贝次数为3次。
这还不是真正的零拷贝技术:
如果网卡支持SG—DMA,就可以将数据拷贝次数减小为两次。
这就是所谓的零拷贝技术,我们没有在内存层面区拷贝数据,也就是说全过程都没有使用CPU搬运数据,所有数据都是通过DMA进行传输的。
整个过程只需要两次上下文的切换和两次数据拷贝。
kafka、rocketMQ、Nginx都使用到了零拷贝技术。
零拷贝的内核缓冲区正是使用了PageCache技术。在零拷贝技术中,用PageCache来缓存最近被访问过的数据,当空间不足时淘汰最久未使用的缓存。
并且给予局部性原理,进行预读,减少IO次数。
但是在传输大文件的时候,性能损失非常大。因为在传输大文件的时候如果采用PageCache,PageCache容量有限,则PageCache由于长时间被大文件占据,其他热点的小文件可能就无法充分使用到。这样磁盘的读写性能就会降低。并且PageCache中的大文件数据,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷贝到 PageCache 一次,造成资源的浪费。
因此PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,这样在高并发的环境下,会带来严重的性能问题。
在最初的方案中:
在read方法返回前,进程一直处于阻塞的状态。
针对阻塞的问题,可以采用异步IO来解决。
异步IO的流程图如下所示:
核心思想在于:在发起异步IO读以后,不等待数据就位就可以立即返回,然后去处理其他任务。直到收到内核返回的读取成功通知,才去处理数据。这样就解决了高并发场景下零拷贝技术大文件读取的阻塞问题。
因此,对于传输大文件,应该使用异步IO+直接IO的方法。而对于小文件,则应该使用零拷贝。