Linux I/O体系之零拷贝

普通I/O

Linux常规io读取文件流程

  1. 用户调用读取文件函数
  2. 将文件读取至Linux内核页高速缓存
  3. 将页高速缓存中的数据拷贝至用户内存空间

Linux内核将文件从io设备读取至内核高速缓存这个动作是通过DMA(直接内存访问)完成

直接内存访问(DMA)

在最初PC中CPU是系统中唯一的总线主控器,即CPU是唯一可以驱动内存地址/数据总线的硬件设备。随着更多诸如PCI(Peripheral Component Interconnect外设部件互连标准)这样的现代总线体系结构的出现,如果提供合适的电路,每一个外围设备都可以充当总线主控器。因此,现在所有的PC都包含一个辅助的DMA电路,它可以用来控制在RAM和IO设备之间数据的传送。
PCI总线,拆过实体机的同学应该见过,如下图的内存条插槽在这里插入图片描述

页高速缓存

页高速缓存是一种软件机制,它允许系统将通常存放在磁盘上的一些数据保留在RAM中,以便对这些数据的访问可以不用再访问磁盘,磁盘IO的性能代价是昂贵的,通过页高速缓存可以使Linux内核更快的得到想要访问的数据。
页高速缓存是Linux内核所使用的主要磁盘高速缓存。
页高速缓存中的页可能是下面的类型:

  • 含有普通文件数据的页
  • 含有目录页
  • 含有直接从块设备文件(跳过文件系统层)读出的数据的页
  • 含有用户态进程数据的页
  • 属于特殊文件系统的页,如共享内存的进程间通信(Interprocess Communication,IPC)所使用的特殊文件系统shm

基树、红黑树

Linux支持大到几个TB的文件。访问大文件时页高速缓存中可能充满太多的文件页,扫描这些文件页需要消耗大量的时间。为了更高效的查找,Linux2.6使用了大量的搜索树,例如:radix tree 基树、redblack tree 红黑树

直接I/O

Direct I/O 直接IO。Linux普通IO对于磁盘的操作都必须通过中断和直接内存访问(DMA)处理块硬件设备,而且这只能在内核态完成。但是还有一些非常复杂的程序(自缓存应用程序,self-caching application)更愿意具有控制IO数据传送的全部权利。例如,考虑高性能数据库服务器:它们大都实现了自己的高速缓存机制,对于这类程序,内核页高速缓存毫无帮助;相反,因为以下原因它可能是有害的:

  • 很多页框浪费在 **复制已在RAM中的磁盘数据 **上
  • 处理页高速缓存和预读的多余指令降低了read和write系统调用的执行效率,也降低了与文件内存映射相关的分页操作
  • 用户与磁盘不是直接传送数据,而是分两次,中间增加了一层内核的缓存操作

零拷贝

零拷贝的场景需求之一如下:实现一个应用在读取一个文件时同时将读取的文件通过网络写入另一台远程服务器。功能就像是MySQL的高可用主从架构

简单实现

常规操作流程下该需求实现,涉及的上下文切换以及copy次数如下图。

  1. 用户读取文件至内存,通过DMA访问IO设备将数据读取至Linux内核高速缓存。DMA copy数据,用户态切换至内核态
  2. Linux内核将页高速缓存中的数据拷贝至用户应用缓存。CPU copy数据,内核态切换至用户态
  3. 将读取的数据写入socket缓存。CPU copy数据至socket缓存,用户态切换至Linux内核态
  4. socket将缓存数据写入协议引擎。DMA copy,写入数据完成返回用户应用程序,内核态切换至用户态

在这里插入图片描述

mmap(memory mapping内存映射)优化

使用内存映射代替读,优化后的流程如下

  1. 调用mmap替换read调用,使文件内容被 DMA 引擎拷贝到内核缓存中。这个缓存是和用户进程共享的,在内核和用户内存空间中没有执行任何拷贝。用户态切换至内核态,DMA copy,内核态切换为用户态
  2. 将mmap映射的数据写入socket缓存。CPU copy数据至socket缓存,用户态切换至Linux内核态
  3. socket将缓存数据写入协议引擎。DMA copy,写入数据完成返回用户应用程序,内核态切换至用户态

在这里插入图片描述
使用mmap代替read,优化减少一次拷贝。当大量的数据被传输时,这能产生相当好的效果。共享型内存映射在线性区上的任何写操作都会修改磁盘上的文件,而且,如果进程对共享映射中的一个页进行写,那么这种修改对于其他映射了这同一文件的所有进程来说都是可见的。因此,使用 mmap+write 方法有些隐藏的陷阱,当你在内存映射一个文件,然后调用 write,同时另一个进程截断相同的文件时,你将会掉入其中的一个陷阱中。你的 write 系统调用将会被总线的错误信号 SIGBUG 中断,因为你执行了一个错误的内存访问。那个信号的默认行为是杀死该进程并转存内核——不是网络服务器最理想的处理方式。有两种方式解决这个问题。
第一种方式是为 SIGBUS 信号安装一个信号处理程序,然后在处理程序中简单地调用 return。通过这样做,write 系统调用返回在它被中断之前写的字节数,并且把 errno 置为 success。这是一个不好的解决方案,一个解决治标不治本的方案。因为 SIGBUS 信号表示进程已经发生了非常严重的错误,不鼓励使用这个解决方案。
第二种方式涉及到内核中的文件租赁(在 Microsoft Windows 中叫作 “机会锁定”)。这是这个问题正确的解决方案。通过在文件描述符中使用租赁,你可以使用内核租赁一个特殊的文件。然后你可以从内核请求读/写租约。当另一个进程尝试截断你正在传输的文件时,内核会给你发送一个实时信号——RT_SIGNAL_LEASE 信号。它告诉你内核正在破坏你在文件的读/写租约。你的 write 调用在你的程序访问到一个非法地址,并被 SIGBUS 信号杀死之前被中断。write 调用的返回值是在中断之前写的字节数,并且设置 errno 为 success。

sendfile优化

在内核 2.1 版本中,sendfile 系统调用被引入,以简化网络和两个本地文件之间的数据传输。sendfile 的引入不仅减少了数据拷贝,也减少了上下文切换
使用sendfile代替read和write

  1. sendfile 系统调用使文件内容被 DMA 引擎拷贝到内核缓存中。然后数据被内核拷贝到与 sockets 关联的内核缓存中。用户态切换至内核态,DMA copy,CPU copy
  2. socket将缓存数据写入协议引擎。DMA copy,写入数据完成返回用户应用程序,内核态切换至用户态

在这里插入图片描述
当另一个进程截断sendfile系统调用传输的文件时,如果我们不注册任何信号处理程序,sendfile 只返回它在中断之前传输的字节数,而且 errno 会被设置为 success。
调用 sendfile 之前,如果我们从内核获得文件租约,则行为和返回状态是完全一样的。在 sendfile 调用返回之前,我们也可以获得 RT_SIGNAL_LEASE 信号。
到此为止,我们已经能够避免发生一些拷贝,但是我们仍然还有一次处理器拷贝。为了消除处理器的数据拷贝,我们需要一个支持聚集操作的网络接口。这仅仅意味着等待传输的数据不需要连续的内存空间;它可以分散不同的内存位置。在内核 2.4 版本中,socket 缓存描述符被修改以适应这些需求——在 Linux 中被称作零拷贝(Zero Copy)。这种方式不仅减少了多个上下文切换,也减少了处理器的数据拷贝
在这里插入图片描述
支持聚集操作的硬件从内存的多个位置获取数据,消除处理器拷贝

  1. sendfile 系统调用导致文件内容被 DMA 引擎拷贝到内核缓存中。
  2. 没有数据被拷贝到 socket 缓存中。取而代之的是只有关于数据的位置和长度的信息的描述符附加到套接字缓冲区。DMA 引擎直接将数据从内核缓存传输到协议引擎,因此消除了最后的拷贝。

因为数据仍然是从磁盘拷贝到内存,从内存到导线,有人可能会说这不是真正的零拷贝。站在操作系统的角度,这是零拷贝,因为在内核缓存之间没有了数据拷贝。当使用零拷贝时,不就有避免拷贝的性能收益,还有像更少的上下文切换,较少的 CPU 数据缓存污染和没有 CPU 校验和计算。
参考:https://www.linuxjournal.com/article/6345,《深入理解Linux内核 第三版》

发布了81 篇原创文章 · 获赞 85 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u010597819/article/details/103950889
今日推荐