Linux 内核与 Zero-Copy 零拷贝

文章目录

  • 前言
  • [传统 Linux IO:read() + wirte()](#传统 Linux IO:read() + wirte())
  • [零拷贝 Zero Copy](#零拷贝 Zero Copy)
    • [mmap() + write()](#mmap() + write())
    • sendfile()
  • [扩展:Page Cache 和 Buffer Pool Page Cache](#扩展:Page Cache 和 Buffer Pool Page Cache)
  • 总结对比

前言

Linux 系统安全性更高的原因之一就是系统是区分用户态、内核态。如果想要进行硬件调用操作,必须要切换到内核态空间。对于用户进程来说,是没办法操作任何操作系统硬件的。

如果当前应用程序需要进行一个文件读写操作(例如 MySQL 写数据到磁盘),该操作需要将数据写入到磁盘,其实就需要转换成内核态,首先将数据写入到 Kernel Buffer CachePage Cache

Linux 2.4 之前,Kernel Buffer Cache区域属于 Page Cache 和 Buffer Cache 组合;Linux 2.4 之后 Buffer Cache 已经和 Page Cache 统一了,Buffer Cache 现在只是 Page Cache 的一个视图(用于块设备元数据),不再是两块独立的内存

磁盘文件读写操作单位是单页 Page Cache(4KB大小),数据按照 Page 页写入到 Page Cache 之后就是 Dirty Page,然后通过 flush 方式写入到磁盘 Disk。

如果是读磁盘操作,会先看 Page Cache 中是否有对应数据,有直接返回;没有就从磁盘加载到 Page Cache,然后从 Page Cache 进行命中返回,Page Cache 会根据 LRU 算法来进行 Page 的定期淘汰(跟 Redis 内存淘汰策略类似)。如下图:用户数据从用户态到内核态到磁盘的大致流程。

传统 Linux IO:read() + wirte()

2次 CPU Copy + 2次 DMA Copy

传统 Linux 系统的 IO 读写主要就是两个系统调用函数read() + write(),其实可以这么理解,只要是涉及到磁盘写入、读取操作,本质背后就是调用这两个系统函数罢了,不管是MySQL、Redis、Kafka 等其他程序写磁盘操作。

如下图:一次IO读写操作操作是需要 4 次用户态、内核态上下文切换,就是发生在系统调用 read()、write() 调用时候进行切换,经历2次CPU Copy + 2次 DMA Copy,DMA 控制器是不需要占用CPU资源的,也就是说从磁盘中读写数据操作是不需要CPU占用等待的,所以就相当于2次CPU Copy 需要额外占用资源,尤其是IO密集型应用场景,频繁地 CPU Copy 操作会损失大量的很多性能。所以接下来考虑到如何避免掉 CPU Copy 操作,Zero-Copy 零拷贝 就是可以做到。

零拷贝 Zero Copy

零拷贝大白话来说,就是不需要数据从内核态进行 CPU Copy 到用户态,更不需要从用户态 CPU Copy 到内核态,直接就从内核态 Kernel Buffer Cache 同步到 Socket Buffer 网卡缓存,然后基于 DMA Copy 进行网卡数据发送,这样甚至连 CPU Copy 的操作都不需要了,尽量避免 CPU 参与的数据拷贝,尤其避免数据在"内核态 ↔ 用户态"之间来回复制。

mmap() + write()

1次 CPU Copy + 2次 DMA Copy

一种简单的 Zero-Copy 实现方案就是通过系统调用 mmap() 替换原本的 read(),mmap 是内存映射,相当于把用户态 User Buffer 中的内存缓冲区映射到内核态 Kernel Buffer 缓冲区,这样就减少了一次 CPU Copy,整体流程大致如下:用户进程系统调用mmap进入内核态,内核缓冲区映射用户缓冲区。将硬盘数据基于 DMA Copy 把数据复制到内核缓冲区,然后系统调用 write 开始进行 CPU Copy到套接字缓冲区(Socket Buffer),然后在基于 DMA Copy 到网卡进行数据传输。

对比传统 Linux IO,不仅少了一次CPU Copy,而且还节省了一半的内存区域,只需要从 Kernel Buffer Cache 进行 CPU Copy 到 Socket Buffer 就行了,用户态内核态切换次数还是4次。

sendfile()

Linux 2.1 版本引入系统调用 sendfile(),sendfile 的本质是 read + write 合并成一次系统调用,上下文切换减半,它相当于是 mmap + wirte 二合一。

扩展:Page Cache 和 Buffer Pool Page Cache

有一个容易混淆的概念,前面一直在说 OS 的 Page Cache(4KB),那 MySQL InnoDB 的 Buffer Pool Page(16KB) 两者什么关系?

OS Page Cache(4KB) 是通用的,无论什么文件都按 4KB 切块存;InnoDB Buffer Pool(16KB) 只缓存数据库页。一个 InnoDB 的 16KB Page,在 OS 层对应 4 个连续的 4KB Page Cache 页,MySQL 读一条数据的完整链路是这样的:

SQL 查询

↓ Buffer Pool 未命中(16KB 的数据页)

↓ InnoDB 调用 read() 向 OS 要数据

↓ OS 检查 Page Cache(对应 4 个 4KB 页)

↓ 未命中 DMA Copy:磁盘 → Page Cache

↓ CPU Copy:Page Cache → Buffer Pool

↓ InnoDB 在 16KB 页里定位到具体行,返回给 SQL 执行器

总结对比

对比项 read/write mmap + write sendfile
核心思路 先读到用户态,再写回内核 把内核文件页映射给用户访问,再写 socket 内核直接把文件发到 socket
是否经过用户缓冲区 不走传统用户缓冲区拷贝,但用户可通过映射访问 不会
典型路径 内核 -> 用户 -> 内核 内核页缓存映射给用户,再进入网络发送路径 内核 -> 内核
CPU copy 数量 通常 2 次明显 CPU copy read/write 少 1 次 最少
系统调用 read + write mmap + write sendfile
应用是否直接拿到数据 会看到映射数据 不会
是否属于零拷贝 部分算,常叫半零拷贝 是,最典型
适合场景 通用业务处理 文件映射、随机访问 文件直接发网络、纯转发
优点 通用、简单、灵活 少一次复制,访问文件高效 CPU 占用更低,吞吐更高
缺点 拷贝多,CPU 开销更大 语义更复杂,不是最彻底 适用场景没那么通用
相关推荐
戴为沐13 小时前
Linux内存扩容指南
linux
zylyehuo1 天前
Linux 彻底且安全地删除文件
linux
用户805533698032 天前
主线 U-Boot 上 RK3506:和闭源 rkbin 拔河的三个隐性契约
linux·嵌入式
用户034095297912 天前
linux fcitx 5 雾凇拼音 设置在中文输入法下仍然输入英文标点
linux
乘云数字DATABUFF2 天前
5分钟部署开源APM Databuff:OpenTelemetry全链路追踪入门实战
运维·后端
Web3探索者3 天前
可视化服务器管理和传统命令行区别是什么?新手教程:Linux 运维到底该用图形界面还是 SSH 命令行?
linux·ssh
zylyehuo3 天前
Linux系统中网线与USB网络共享冲突
linux
荣--3 天前
一键部署不是为了省时间 —— 它是把"买来的 PaaS"变成"自己的平台"的拐点
运维·zabbix·工程化·一键部署·平台化·边界设计
江华森4 天前
动手实战学 Docker — 从零到集群编排完全指南
运维
Avan_菜菜4 天前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https