一点一滴成长

导航

零拷贝IO

以下内容参考和转载自:小林coding,原来 8 张图,就可以搞懂「零拷贝」了

1、DMA

  在没有DMA(直接内存访问)技术之前,调用read()读取磁盘文件的话,会有5个步骤:CPU向磁盘发起IO请求—>磁盘控制器将数据放到磁盘缓冲区中后产生一个IO中断—>CPU收到IO中断信号后将磁盘缓冲区数据拷贝到内核缓冲区(PageCache)—>CPU将内核缓冲区的数据拷贝到用户缓存区—>然后read()返回。

  在CPU收到IO中断信号一直到read()返回中间这些步骤都是靠CPU完成的,而有了DMA技术后,磁盘不再产生IO中断而是通知DMA控制器,DMA将数据从磁盘缓冲区拷贝到内核缓冲区(PageCache)后向CPU发出信号,CPU再从内核缓冲区中数据拷贝到用户缓冲区。也就是说原来CPU干的活(将数据从磁盘缓冲区拷贝到内核缓冲区)现在交给了DMA,这样CPU在这段时间就能去干其他事情了。

  现在每个 I/O 设备里面都有自己的 DMA 控制器。

 2、上下文切换

  传统 I/O 的工作方式是,数据读取/写入是从用户空间到内核空间来回切换,如下所示的是从磁盘文件读取数据后发送到网卡的过程,一次read()或write()就会产生两次切换。之所以要发生上下文切换,这是因为用户空间没有权限操作磁盘或网卡,设备的操作都需要交由系统内核来完成:当read()/wirte()的时候,从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。

   

3、零拷贝

  从上面我们可以看到,一次read()+write()会产生2次+2次即4次拷贝:比如read()会将数据从磁盘拷贝到内核缓冲区(PageCache),然后再将数据从内核缓冲区拷贝到用户缓冲区,调用write()到socket的话,会将数据从用户缓冲区拷贝到socket缓冲区,然后再将数据从socket缓冲区拷贝到网卡缓冲区后发送数据。而read()+write()总共会产生4次上下文切换。如果我们能减少数据拷贝次数,并且减少上下文切换次数的话就会提高IO效率,这就是零拷贝技术。

  可以使用mmap()+sendfile()来实现零拷贝:mmap()可以将内核缓冲区内容「映射」到用户空间,这样内核缓冲区就是用户缓冲区,用户缓冲区也就是内核缓冲区,从而减少内核缓冲区到用户缓冲区的这一次拷贝。而使用sendfile()替换read()+write()的话,如果网卡支持 SG-DMA技术,那么sendfile()内部将内核缓冲区描述符和数据长度传到 socket 缓冲区后,SG-DMA 控制器可以直接将内核缓冲区的数据发送到网卡缓冲区,这样原来的4次拷贝就变成了现在的2次。而且由于内核缓冲区和socket缓冲区都属于内核态,原来的4次上下文切换现在也变成了2次,如下图所示。因为拷贝次数和上下文切换都少了一半,而且两次拷贝都不需要通过CPU,所以零拷贝技术可以将文件传输性能提高一倍以上。

  

4、PageCache

  前面所说的内核缓冲区,实际上是磁盘高速缓存(PageCache),PageCache会缓存最近被访问的数据(当空间不足时淘汰最久未被访问的缓存):读磁盘数据的时候,优先在 PageCache 找,如果数据存在则可以直接返回;如果没有,则从磁盘中读取,然后缓存到PageCache 中。PageCache还使用了“预读功能”:比如我们read() 0到32KB的数据的时候,其还会将后面32~64 KB的数据读取到PageCache,这样后面读取 32~64 KB 的成本就很低。

  所以,使用PageCache 的优点主要是两个:缓存最近被访问的数据,预读功能,这两个优点会大大提高读写磁盘的性能。

  然而,传输大文件的时候,使用PageCache会出现一些问题:一个就是大文件每当用户访问这些大文件的时候,内核就会把它们载入 PageCache 中,于是 PageCache 空间很快被这些大文件占满,所以现在PageCache中全是大文件的缓存数据,然而这些数据再次被访问的概率是比较低的,这样缓存光占了PageCache空间而未起作用。第二个问题就是由于文件太大,PageCache 空间很容易被这些大文件的缓存数据占满,这样其他「热点」的小文件数据就无法缓存到PageCache中。

5、大文件传输与异步IO

  大文件传输使用PageCache的话,很难享受到PageCache缓存带来的好处,那也就没必要将数据缓存到PageCache上了,而异步IO就是这样,它不使用PageCache:在发起读请求后立即返回,数据直接从磁盘控制器拷贝到用户缓冲区,然后再通知用户,如下图所示。绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O。

  

  

  所以,传输文件的时候,我们要根据文件的大小来使用不同的方式:

  • 传输大文件的时候,使用「异步 I/O + 直接 I/O」;
  • 传输小文件的时候,则使用「零拷贝技术」;

  在 nginx 中,我们可以用如下配置,来根据文件的大小来使用不同的方式,当文件大小大于1G后,使用「异步 I/O + 直接 I/O」,否则使用「零拷贝技术」:

location /video/ { 
    sendfile on; 
    aio on; 
    directio 1024m; 
}

  如果应用已经实现了磁盘数据的缓存,那么可以不需要 PageCache 再次缓存,以减少额外的性能损耗。比如在 MySQL 数据库中,可以通过参数来开启直接 I/O(避免使用PageCache),默认是不开启的。

5、异步IO与完成端口

  实际上,从形式上来讲,异步IO也是属于零拷贝IO,比如对于读来说是直接从网卡拷贝到用户缓冲区,没有中间的DMA拷贝。在网上看到说使用完成端口实现异步通信的时候,需要将SO_RCVBUF设置为0以实现零拷贝,但是有另一种说法是在Win2K之后,不再需要将SO_RCVBUF设置为0,系统会自动将数据复制到用户的接收缓冲区中。

posted on 2024-09-02 13:04  整鬼专家  阅读(19)  评论(0编辑  收藏  举报