sendfile
Contents
sendfile
在了解零拷贝之前,我们先来看看标准的的 I/O 操作.
传统 IO, 缓存 IO, Buffer I/O, 标准 IO
标准 IO 又被称作缓存 IO 或传统 IO, 大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存 (page cache) 中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。写的过程就是数据流反方向。
缓存 I/O 有以下这些优点:
- 缓存 I/O 使用了操作系统内核缓冲区,在一定程度上分离了应用程序空间和实际的物理设备。
- 缓存 I/O 可以减少读盘的次数,从而提高性能。
对于读操作: 当应用程序要去读取某块数据的时候,如果这块数据已经在页缓存中,那就返回之。而不需要经过硬盘的读取操作了。如果这块数据不在页缓存中,就需要从硬盘中读取数据到页缓存。
对于写操作: 应用程序会将数据先写到页缓存中,数据是否会被立即写到磁盘,这取决于所采用的写操作机制:
如果用户采用的是同步写机制 (synchronous writes), 那么数据会立即被写回到磁盘上,应用程序会一直等到数据被写完为止; 如果用户采用的是延迟写机制 (deferred writes), 那么应用程序就完全不需要等到数据全部被写回到磁盘,数据只要被写到页缓存中(内核的缓冲区)去就可以了。在延迟写机制的情况下,操作系统会定期地将放在页缓存中的数据刷到磁盘上。与异步写机制 (asynchronous writes) 不同的是,延迟写机制在数据完全写到磁盘上的时候不会通知应用程序,而异步写机制在数据完全写到磁盘上的时候是会返回给应用程序的。所以延迟写机制本身是存在数据丢失的风险的,而异步写机制则不会有这方面的担心。
传统 IO 的缺点
在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样的话,数据在传输过程中需要在应用程序地址空间和页缓存之间进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
当然也可以采用直接 I/O 技术来满足自缓存应用程序 ( self-caching applications) 的需求。
对于某些特殊的应用程序来说,能够绕开内核缓冲区能够获取更好的性能,这就是直接I/O出现的意义。
直接 I/O
凡是通过直接 I/O 方式进行数据传输,数据直接从用户态地址空间写入到磁盘中,直接跳过内核缓冲区。对于一些应用程序,例如: 数据库。他们更倾向于自己的缓存机制,这样可以提供更好的缓冲机制提高数据库的读写性能。
直接 I/O 优点
最大的优点就是减少操作系统缓冲区和用户地址空间的拷贝次数。降低了 CPU 的开销,和内存带宽。对于某些应用程序来说简直是福音,将会大大提高性能。
直接 I/O 缺点
直接 IO 并不总能让人如意。直接 IO 的开销也很大,应用程序没有控制好读写,将会导致磁盘读写的效率低下。磁盘的读写是通过磁头的切换到不同的磁道上读取和写入数据,如果需要写入数据在磁盘位置相隔比较远,就会导致寻道的时间大大增加,写入读取的效率大大降低。
直接IO方式确实能够减少 CPU 的使用率以及内存带宽的占用,但是有时候也会造成性能的影响。所以在使用直接 IO 之前一定要清楚它的原理,只有在各项都清晰的情况下,才考虑使用。
为什么需要零拷贝技术?
如今,很多网络服务器都是基于 客户端 - 服务器 这一模型的。在这种模型中,客户端向服务器端请求数据或者服务;服务器端则需要响应客户端发出的请求,并为客户端提供它所需要的数据。随着网络服务的逐渐普及,video 这类应用程序发展迅速。当今的计算机系统已经具备足够的能力去处理 video 这类应用程序对客户端所造成的重负荷,但是对于服务器端来说,它应付由 video 这类应用程序引起的网络通信量就显得捉襟见肘了。而且,客户端的数量增长迅速,那么服务器端就更容易成为性能瓶颈。而对于负荷很重的服务器来说,操作系统通常都是引起性能瓶颈的罪魁祸首。举个例子来说,当数据"写"操作或者数据"发送"操作的系统调用发出时,操作系统通常都会将数据从应用程序地址空间的缓冲区拷贝到操作系统内核的缓冲区中去。操作系统这样做的好处是接口简单,但是却在很大程度上损失了系统性能,因为这种数据拷贝操作不单需要占用 CPU 时间片,同时也需要占用额外的内存带宽。
一般来说,客户端通过网卡向服务器端发送请求,操作系统将这些客户端的请求传递给服务器端应用程序,服务器端应用程序会处理这些请求,请求处理完成以后,操作系统还需要将处理得到的结果通过网络适配器传递回去。
什么是零拷贝技术?
简单一点来说,零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。
零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率。
而且,零拷贝技术减少了用户应用程序地址空间和操作系统内核地址空间之间因为上下文切换而带来的开销。进行大量的数据拷贝操作其实是一件简单的任务,从操作系统的角度来说,如果 CPU 一直被占用着去执行这项简单的任务,那么这将会是很浪费资源的;如果有其他比较简单的系统部件可以代劳这件事情,从而使得 CPU 解脱出来可以做别的事情,那么系统资源的利用则会更加有效。
零拷贝技术的要点
避免操作系统内核缓冲区之间进行数据拷贝操作。 避免操作系统内核和用户应用程序地址空间这两者之间进行数据拷贝操作。 用户应用程序可以避开操作系统直接访问硬件存储。 数据传输尽量让 DMA 来做。
DMA
DMA: 是指外部设备不通过 CPU 而直接与系统内存交换数据的接口技术。网卡寄存器 -> 内核为网卡分配的缓冲区 ring buffer
零拷贝技术分类
Linux 中的零拷贝技术主要有下面这几种
- 直接 I/O
- mmap
- sendfile
- splice
sendfile 实现零拷贝的原理
描述 sendfile 系统调用在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了数据在内核缓冲区和用户缓冲区之间的拷贝,操作效率很高,被称之为零拷贝。
原理 sendfile() 系统调用利用 DMA 引擎将文件中的数据拷贝到操作系统内核缓冲区中,然后数据被拷贝到与 socket 相关的内核缓冲区中去。接下来,DMA 引擎将数据从内核 socket 缓冲区中拷贝到协议引擎中去。
sendfile() 系统调用不需要将数据拷贝或者映射到应用程序地址空间中去,所以 sendfile() 只是适用于应用程序地址空间不需要对所访问数据进行处理的情况。因为 sendfile 传输的数据没有越过 用户应用程序 / 操作系统内核 的边界线,所以 sendfile () 也极大地减少了存储管理的开销。
简单归纳上述的过程
sendfile 系统调用利用 DMA 引擎将文件数据拷贝到内核缓冲区,之后数据被拷贝到内核socket缓冲区中 DMA 引擎将数据从内核 socket 缓冲区拷贝到协议引擎中 这里没有用户态和内核态之间的切换,也没有内核缓冲区和用户缓冲区之间的拷贝,大大提升了传输性能。
带有 DMA 收集拷贝功能的 sendfile (The Scatter-Gather Direct Memory Access)
上面介绍的 sendfile() 技术在进行数据传输仍然还需要一次多余的数据拷贝操作,通过引入一点硬件上的帮助,这仅有的一次数据拷贝操作也可以避免。为了避免操作系统内核造成的数据副本,需要用到一个支持收集操作的网络接口。主要的方式是待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。这样一来,从文件中读出的数据就根本不需要被拷贝到 socket 缓冲区中去,而只是需要将缓冲区描述符传到网络协议栈中去,之后其在缓冲区中建立起数据包的相关结构,然后通过 DMA 收集拷贝功能将所有的数据结合成一个网络数据包。网卡的 DMA 引擎会在一次操作中从多个位置读取包头和数据。Linux 2.4 版本中的 socket 缓冲区就可以满足这种条件,这种方法不但减少了因为多次上下文切换所带来开销,同时也减少了处理器造成的数据副本的个数。对于用户应用程序来说,代码没有任何改变。
主要过程如下
首先,sendfile() 系统调用利用 DMA 引擎将文件内容拷贝到内核缓冲区去;然后,将带有文件位置和长度信息的缓冲区描述符添加到 socket 缓冲区中去,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,DMA 引擎会将数据直接从内核缓冲区拷贝到协议引擎中去,这样就避免了最后一次数据拷贝。
注意事项:只有网卡支持 SG-DMA (The Scatter-Gather Direct Memory Access)技术才可以通过传递文件描述符的方式避免内核空间内的一次 CPU 拷贝。这意味着此优化取决于 Linux 系统的物理网卡是否支持 (Linux 在内核 2.4 版本里引入了 DMA 的 scatter/gather – 分散/收集功能,只要确保 Linux 版本高于 2.4 即可)。
总结
上述的两种几种I/O操作对比:
传统I/O 硬盘—>内核缓冲区—>用户缓冲区—>内核socket缓冲区—>协议引擎
sendfile 硬盘—>内核缓冲区—>内核socket缓冲区—>协议引擎
sendfile ( DMA 收集拷贝) 硬盘—>内核缓冲区—>协议引擎
Tips: 用户态和内核态切换的代价在哪?
首先,用户态一个进程,内核态一个进程,切换就要进行进程间的切换。 拿系统调用举例来说,系统调用一般都需要保存用户程序得上下文(context), 在进入内核得时候需要保存用户态得寄存器,在内核态返回用户态得时候会恢复这些寄存器得内容。这是一个开销的地方。
如果需要在不同用户程序间切换的话,那么还要更新cr3寄存器,这样会更换每个程序的虚拟内存到物理内存映射表的地址,也是一个比较高负担的操作。
而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。
参考资料
- Linux 中直接 I/O 机制的介绍
- Linux 中的零拷贝技术,第 1 部分
- Linux 中的零拷贝技术,第 2 部分
作者: 叫我不矜持 链接: https://www.jianshu.com/p/028cf0008ca5 来源: 简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
https://blog.csdn.net/qq_38293564/article/details/99203065
https://cloud.tencent.com/developer/news/406991
https://spongecaptain.cool/SimpleClearFileIO/2.%20DMA%20%E4%B8%8E%E9%9B%B6%E6%8B%B7%E8%B4%9D%E6%8A%80%E6%9C%AF.html
Author -
LastMod 2021-04-21