惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Security Blog
Microsoft Security Blog
aimingoo的专栏
aimingoo的专栏
I
InfoQ
C
Check Point Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
Apple Machine Learning Research
Apple Machine Learning Research
Vercel News
Vercel News
Last Week in AI
Last Week in AI
GbyAI
GbyAI
P
Proofpoint News Feed
量子位
Stack Overflow Blog
Stack Overflow Blog
Microsoft Azure Blog
Microsoft Azure Blog
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
B
Blog
T
The Blog of Author Tim Ferriss
H
Help Net Security
云风的 BLOG
云风的 BLOG

博客园 - 整鬼专家

反向代理服务 Http认证(鉴权) 跨域问题 SSE客户端C++实现(使用libcurl) 使用libcurl/curlpp进行http通信 libuv进程 libuv线程 libuv网络操作 libuv文件系统 libuv概述 MySQL Cluster与Replication Redis集群 Redis持久化 Redis脚本 Redis进阶——事务、TTL、排序、消息通知、管道 Redis数据类型 Redis安装 行为设计模式 结构型设计模式
零拷贝IO
整鬼专家 · 2024-09-02 · via 博客园 - 整鬼专家

以下内容参考和转载自:小林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,系统会自动将数据复制到用户的接收缓冲区中。