利用零拷贝技术优化IO操作

date
Mar 17, 2024
slug
use-zero-copy-to-optimize-io
status
Published
tags
编程开发
summary
相信每个程序员多少都遇到这样的场景,需要将磁盘上的文件读取出来然后发送给客户端。要实现这样一个功能并不难,基本上每一门语言都提供了对操作系统的IO操作的类库。不过如果我们的场景换成是如何在高并发的场景下,为客户端提供读取文件的操作时,可能在实现上就没有那么简单了。
type
Post
相信每个程序员多少都遇到这样的场景,需要将磁盘上的文件读取出来然后发送给客户端。要实现这样一个功能并不难,基本上每一门语言都提供了对操作系统的IO操作的类库。
不过如果我们的场景换成是如何在高并发的场景下,为客户端提供读取文件的操作时,可能在实现上就没有那么简单了。这是因为在正常流程下,这个过程将会涉及到四次数据的拷贝操作。
  • 第一次数据拷贝。当我们的应用程序发起对文件的读取read()操作时,当前的上下文会从用户态切换到内核态,此时DMA(Direct Memory Access)引擎会从磁盘文件中读取数据,并将其存储到内核态缓冲区(DMA拷贝)中。
  • 第二次数据拷贝。数据将会从内核态的缓冲区拷贝到用户态的缓冲区中(CPU拷贝),此时应用程序便收到数据了。
  • 第三次数据拷贝。应用程序发起对客户端的写write()操作,当前上下文将从用户态切换至内核态,数据从用户态缓冲区被拷贝到 Socket 缓冲区 (CPU 拷贝)。
  • 第四次数据拷贝。write() 系统调用结束后返回到用户进程,当前上下文从内核态切换至用户态,这一次的数据拷贝为异步执行,从 Socket 缓冲区拷贝到网卡 (DMA 拷贝)。
notion image
如果我们要读取一个32MB的文件,这4次的数据拷贝相当于让我们操作了一个128MB的文件。而且如果应用程序分配给缓冲区的空间为32KB,则要完成4000次(4*32MB/32KB)的数据拷贝操作。如果在并发不高时,也倒还好。但如果并发一高,系统性能自然受影响。那是否有方法去优化这个文件读取和传输的过程呢?这个方法就是接下来要说的零拷贝技术。
从之前的流程中我们知道,这个过程同样会涉及到四次的上下文切换,其中的第一次的上下文切换是发起read()读取操作时从用户态到内核态的切换,第四次的上下文切换是内核执行完写操作后从内核态回到用户态。这两次的切换是无法避免的。
同样的道理,在四次的数据拷贝中,第一次的数据拷贝是磁盘到内核态PageCache的拷贝,第四次是内核态中数据到网卡的拷贝,这两次的数据拷贝也是无法避免的。
如果操作系统能够将内核态中的数据直接拷贝给socket缓冲区,不就可以省去了中间的两次切换,并将四次的数据拷贝降低为三次,这就是零拷贝技术要做的事情。
notion image
如果网卡支持SG-DMA(The Scatter-Gather Direct Memory Access)技术,还能实现将PageCache中的数据直接拷贝到网卡,将三次数据拷贝降为两次。
notion image
如果你使用的是go语言,可是直接使用io.Copy来直接使用零拷贝技术。之前的案例优化之后可以变成这样:
那是不是任何场景都可以使用零拷贝技术呢?其实并不是如此。零拷贝不适合用于大文件传输的场景,例如你要传输GB级别的文件。
这是因为零拷贝技术会使用到PageCache,而PageCache是一种磁盘的高速缓存,既然是缓存就会有满的时候,如果满了就会根据LRU算法淘汰最久未被访问的数据。
操作系统在读取磁盘中的文件时,读取后会将其放到PageCache中,还会预先将下一次读取的数据也体现放到PageCache中。如果我们的文件太大,将会将PageCache的空间占满。
另外由于大文件中很多内容被再次访问的概率是非常低的,也浪费了PageCache的用途的。所以如果是大文件读取的场景,还是不要使用零拷贝技术,我们可以使用异步IO的方式来读取这些文件。
当我们采用异步IO的方式去读取文件时,操作系统则不会将数据存放到PageCache中。异步的好处就是当操作系统在进行IO操作时,我们的程序还可以做其他事情,等到操作系统读取完数据再通知我们的应用程序来处理,这样也不用担心读取大文件时的阻塞问题。
综上所述,小文件我们就采用零拷贝技术来读取,大文件就采用异步IO来读取。