写点什么

百万并发「零拷贝」技术系列之 Linux 实现

用户头像
码农神说
关注
发布于: 2020 年 07 月 27 日
百万并发「零拷贝」技术系列之Linux实现

上一篇推文《百万并发「零拷贝」技术系列之初探门径》中的示例告诉我们:传统的 I/O 操作读取文件并通过 Socket 发送,需要经过 4 次上下文切换、2 次 CPU 数据拷贝和 2 次 DMA 控制器数据拷贝,如下图

从中也可以看得出提高性能可以从减少数据拷贝和上下文切换的次数着手,在 Linux 操作系统层面上有 4 种实现方案:内存映射 mmap、sendfile、splice、tee,这些实现中或多多少的减少数据拷贝次数或减少上下文切换次数。


操作系统层面的减少数据拷贝次数主要是指用户空间和内核空间的数据拷贝,因为只有他们的拷贝是大量消耗 CPU 时间片的,而 DMA 控制器拷贝数据 CPU 参与的工作较少,只是辅助作用。


现实中对零拷贝的概念有广义和狭义之分,广义上是指只要减少了数据拷贝的次数就称之为零拷贝;狭义上是指真正的零拷贝,比如上例中避免 2 和 3 的 CPU 拷贝。


下面我们逐一看看他们的设计思想和实现方案

mmap 内存映射


既然是内存映射,首先来了解解下虚拟内存和物理内存的映射关系,虚拟内存是操作系统为了方便操作而对物理内存做的抽象,他们之间是靠页表(Page Table)进行关联的,关系如下

每个进程都有自己的 PageTable,进程的虚拟内存地址通过 PageTable 对应于物理内存,内存分配具有惰性,它的过程一般是这样的:进程创建后新建与进程对应的 PageTable,当进程需要内存时会通过 PageTable 寻找物理内存,如果没有找到对应的页帧就会发生缺页中断,从而创建 PageTable 与物理内存的对应关系。虚拟内存不仅可以对物理内存进行扩展,还可以更方便地灵活分配,并对编程提供更友好的操作。


内存映射(mmap)是指用户空间和内核空间的虚拟内存地址同时映射到同一块物理内存,用户态进程可以直接操作物理内存,避免用户空间和内核空间之间的数据拷贝。


它的具体执行流程是这样的


  1. 用户进程通过系统调用 mmap 函数进入内核态,发生第 1 次上下文切换,并建立内核缓冲区;

  2. 发生缺页中断,CPU 通知 DMA 读取数据;

  3. DMA 拷贝数据到物理内存,并建立内核缓冲区和物理内存的映射关系;

  4. 建立用户空间的进程缓冲区和同一块物理内存的映射关系,由内核态转变为用户态,发生第 2 次上下文切换;

  5. 用户进程进行逻辑处理后,通过系统调用 Socket send,用户态进入内核态,发生第 3 次上下文切换;

  6. 系统调用 Send 创建网络缓冲区,并拷贝内核读缓冲区数据;

  7. DMA 控制器将网络缓冲区的数据发送网卡,并返回,由内核态进入用户态,发生第 4 次上下文切换;


总结


  1. 避免了内核空间和用户空间的 2 次 CPU 拷贝,但增加了 1 次内核空间的 CPU 拷贝,整体上相当于只减少了 1 次 CPU 拷贝;

  2. 针对大文件比较适合 mmap,小文件则会造成较多的内存碎片,得不偿失;

  3. 当 mmap 一个文件时,如果文件被另一个进程截获可能会因为非法访问导致进程被 SIGBUS 信号终止;

sendfile


sendfile 是在 linux2.1 引入的,它只需要 2 次上下文切换和 1 次内核 CPU 拷贝、2 次 DMA 拷贝,函数原型

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
复制代码

out_fd 为文件描述符,in_fd 为网络缓冲区描述符,offset 偏移量(默认 NULL),count 文件大小。


它的内部执行流程是这样的


  1. 用户进程系统调用 senfile,由用户态进入内核态,发生第 1 次上下文切换;

  2. CPU 通知 DMA 控制器把文件数据拷贝到内核缓冲区;

  3. 内核空间自动调用网络发送功能并拷贝数据到网络缓冲区;

  4. CPU 通知 DMA 控制器发送数据;

  5. sendfile 系统调用结束并返回,进程由内核态进入用户态,发生第 2 次上下文切换;


总结


  1. 数据处理完全是由内核操作,减少了 2 次上下文切换,整个过程 2 次上下文切换、1 次 CPU 拷贝,2 次 DMA 拷贝;

  2. 虽然可以设置偏移量,但不能对数据进行任何的修改;

sendfile+DMA gather


Linux2.4 对 sendfile 进行了优化,为 DMA 控制器引入了 gather 功能,就是在不拷贝数据到网络缓冲区,而是将待发送数据的内存地址和偏移量等描述信息存在网络缓冲区,DMA 根据描述信息从内核的读缓冲区截取数据并发送。它的流程是如下



  1. 用户进程系统调用 senfile,由用户态进入内核态,发生第 1 次上下文切换;

  2. CPU 通知 DMA 控制器把文件数据拷贝到内核缓冲区;

  3. 把内核缓冲区地址和 sendfile 的相关参数作为数据描述信息存在网络缓冲区中;

  4. CPU 通知 DMA 控制器,DMA 根据网络缓冲区中的数据描述截取数据并发送;

  5. sendfile 系统调用结束并返回,进程由内核态进入用户态,发生第 2 次上下文切换;


总结

  1. 需要硬件支持,如 DMA;

  2. 整个过程 2 次上下文切换,0 次 CPU 拷贝,2 次 DMA 拷贝,实现真正意义上的零拷贝;

  3. 依然不能修改数据;


但那时的 sendfile 有个致命的缺陷,如果你查看 Sendfild 手册,你会发现如下描述



in_fd 不仅仅不能是 socket,而且在 2.6.33 之前 Sendfile 的 out_fd 必须是 socket,因此 sendfile 几乎成了专为网络传输而设计的,限制了其使用范围比较狭窄。2.6.33 之后 out_fd 才可以是任何 file,于是乎出现了 splice。

splice


鉴于 Sendfile 的缺点,在 Linux2.6.17 中引入了 Splice,它在读缓冲区和网络操作缓冲区之间建立管道避免 CPU 拷贝:先将文件读入到内核缓冲区,然后再与内核网络缓冲区建立管道。它的函数原型

ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
复制代码

它的执行流程如下



  1. 用户进程系统调用 splice,由用户态进入内核态,发生第 1 次上下文切换;

  2. CPU 通知 DMA 控制器把文件数据拷贝到内核缓冲区;

  3. 建立内核缓冲区和网络缓冲区的管道;

  4. CPU 通知 DMA 控制器,DMA 从管道读取数据并发送;

  5. splice 系统调用结束并返回,进程由内核态进入用户态,发生第 2 次上下文切换;


总结

  1. 整个过程 2 次上下文切换,0 次 CPU 拷贝,2 次 DMA 拷贝,实现真正意义上的零拷贝;

  2. 依然不能修改数据;

  3. fd_in 和 fd_out 必须有一个是管道;


tee

tee 与 splice 类同,但 fd_in 和 fd_out 都必须是管道。


写在最后



各种 I/O 方案总结对比如上,下一篇将带你感受下 Java 的零拷贝方案及实现,敬请关注。


最新、更多漫画请关注微信公众号:码农神说。

码农神说:图解码农技术,大话码农故事,漫画感悟码农人生,助力码农翻身!




发布于: 2020 年 07 月 27 日阅读数: 205
用户头像

码农神说

关注

欢迎关注公众号【码农神说】 2018.11.09 加入

图解码农技术,大话码农故事,漫画感悟码农人生,助力码农翻身!

评论

发布
暂无评论
百万并发「零拷贝」技术系列之Linux实现