23 DMA与零拷贝技术

磁盘可以说是计算机系统中最慢的硬件之一,读写速度相差内存10倍以上,所以针对优化磁盘的技术非常的多,比如:零拷贝,直接I/O,异步I/O等等。

这里我们就以文件传输为切入点,分析I/O工作方式,以及如何优化文件传输的性能。

为什么要有DMA技术

DMA技术,全称为Direct Memory Access(又称直接内存访问技术)。

没有DMA技术前的I/O过程

image.png

可以看到整个数据传输的过程:

  1. 首先在用户进程进行read()系统调用的时候,操作系统会由用户态切换到内核态,然后由CPU向磁盘发起IO请求。
  2. 磁盘在接收到IO请求以后,进行数据准备工作,将数据放在磁盘控制器缓冲区里。
  3. 在数据准备工作完成以后,磁盘向CPU发出IO中断信号。
  4. CPU收到中断信号后,会先将磁盘缓冲区中的文件copy到PageCache中,再将数据从PageCache中copy到用户缓冲区中。在这期间CPU是无法执行其他任务的。copy完成之后read()调用返回,操作系统刚从内核态切换回用户态。

故可以得知:在数据传输的过程,需要CPU亲自的去拷贝数据,并且在这期间CPU无法去做其他事情。简单的搬运几个字符没有问题,但是当处理大量数据的时候,如果每次都让CPU来搬运,显然忙不过来。

有DMA技术之后的I/O过程

image.png

  1. 当有了DMA控制器以后,在进程向CPU发出read()调用以后,CPU向DMA控制器发起IO请求,然后DMA控制器再向磁盘发出IO请求。

  2. 当磁盘接收到IO请求以后,会进行数据准备工作,将数据放到磁盘数据缓冲区当中。

  3. 当磁盘的数据准备工作完成以后,不再向CPU发出中断信号,而是通知DMA控制器。

  4. DMA控制器在接收到通知以后,将数据从磁盘控制器缓冲区中copy到内核缓冲区中。在这期间并不占用CPU,CPU可以处理其他的事情,执行其他的任务。

  5. DMA控制器处理完之后向CPU发出中断信号,由CPU将数据从内核缓冲区copy至用户缓冲区中。copy完成之后read()调用返回,操作系统从内核态切换回用户态。

    注意:起初的DMA控制器只在主板上,但是现在IO设备越来越多,数据传输的需求也不尽相同,所以现在每个IO设备中都有DMA控制器。

image.png

在进行read调用的时候,操作系统由用户态转为内核态,需要进行DMA拷贝和CPU拷贝各一次。(DMA拷贝是指由DMA控制器将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,CPU拷贝是指将数据从内核缓冲区中copy到用户缓冲区中),在read调用结束以后,操作系统再从内核态切换回用户态。

在进行write调用的时候,操作系统由用户态转为内核态,需要进行CPU拷贝和DMA拷贝各一次。(CPU拷贝是指将用户缓冲区的数据拷贝到socket缓冲区中。而DMA拷贝是指将socket缓冲区中的数据拷贝到网卡中。)

在文件传输场景下,我们无需在用户空间对数据进行再加工。因此用户缓冲区的存在是没有必要的。

如何实现零拷贝

实现零拷贝的技术主要有两种:mmap+write,sendfile

下面就谈一谈,他们是如何减少上下文切换和数据拷贝的次数的。

mmap+write

利用mmap替换read系统调用函数。

mmap系统调用函数会直接将内核缓冲区的数据映射到用户空间,这样操作系统内核与用户空间之间就不需要任何的拷贝操作。

image.png

具体流程如下:
应用进程调用了mmap系统调用函数之后,DMA会把磁盘缓冲区的数据拷贝到内核缓冲区中。接着,应用进程和操作系统内核共享这个缓冲区。
应用进程再调用write函数,操作系统直接将内核缓冲区的数据拷贝到socket缓冲区。再由DMA控制器copy到网卡。

这还不是最理想的零拷贝因为还是需要两次系统调用,四次上下文切换,3次拷贝。

sendfile

在linux内核版本2.1中,提供了一个新的系统调用函数sendfile。这个系统调用函数可以代替read和write两个系统调用函数。

image.png

这样系统调用的次数就变成了一次,上下文切换的次数减小到了两次,数据拷贝次数为3次。

这还不是真正的零拷贝技术:
如果网卡支持SG—DMA,就可以将数据拷贝次数减小为两次。

image.png

这就是所谓的零拷贝技术,我们没有在内存层面区拷贝数据,也就是说全过程都没有使用CPU搬运数据,所有数据都是通过DMA进行传输的。

整个过程只需要两次上下文的切换和两次数据拷贝。

kafka、rocketMQ、Nginx都使用到了零拷贝技术。

PageCache

零拷贝的内核缓冲区正是使用了PageCache技术。在零拷贝技术中,用PageCache来缓存最近被访问过的数据,当空间不足时淘汰最久未使用的缓存。

并且给予局部性原理,进行预读,减少IO次数。

但是在传输大文件的时候,性能损失非常大。因为在传输大文件的时候如果采用PageCache,PageCache容量有限,则PageCache由于长时间被大文件占据,其他热点小文件可能就无法充分使用到。这样磁盘的读写性能就会降低。并且PageCache中的大文件数据,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷贝到 PageCache 一次,造成资源的浪费。

因此PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,这样在高并发的环境下,会带来严重的性能问题。

大文件传输采用什么实现

在最初的方案中:

  1. 调用read方法的时候,操作系统会由用户态切换为内核态。
  2. 然后内核向磁盘发起IO请求,磁盘收到IO请求以后,会将数据放在磁盘控制器缓冲区中。
  3. 磁盘完成该操作以后,向内核发起中断信号,然后内核将磁盘控制器缓冲区中的信号拷贝到PageCache中。
  4. 然后内核再将PageCache中的数据拷贝到用户缓冲区中。
  5. 完成以后,read调用返回。操作系统由内核态窃魂会用户态。

image.png

在read方法返回前,进程一直处于阻塞的状态。

针对阻塞的问题,可以采用异步IO来解决。

异步IO的流程图如下所示:

image.png

核心思想在于:在发起异步IO读以后,不等待数据就位就可以立即返回,然后去处理其他任务。直到收到内核返回的读取成功通知,才去处理数据。这样就解决了高并发场景下零拷贝技术大文件读取的阻塞问题。

因此,对于传输大文件,应该使用异步IO+直接IO的方法。而对于小文件,则应该使用零拷贝。

猜你喜欢

转载自blog.csdn.net/weixin_68930048/article/details/130176478
23