操作系统的mmap,拷贝
总结的挺好的
内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
RocketMQ为什么快?kafka为什么快?什么是mmap?这些问题都逃不过一个点,就是零拷贝。
虽然还有其他的原因,但是这里主要讨论零拷贝。
传统的IO方式
传统的IO方式底层其实是调用read和write来实现;
- 用户进程通过read向操作系统发起系统调用,指示上下文从用户态转向内核态;
- DMA控制器把数据从硬盘拷贝到内核缓冲区;
- CPU把内核读缓冲区的数据拷贝到用户/应用缓冲区,上下文从内核态转为用户态,read返回;
- 用户进程通过write方法发起调用,上下文从用户态切换为内核态;
- CPU将用户/应用缓冲区中的数据拷贝到socket缓冲区(写缓冲);
- DMA控制器把数据从socket缓冲区拷贝到网卡(写入网卡设备),上下文从内核态切换回用户态,write返回。
什么是DMA拷贝?
对于一个IO操作而言,都是通过CPU发出对应的指令来完成的,但是相比于CPU来说,IO的速度太慢了,CPU有大量时间处于等待IO的状态,因此就产生了DMA直接内存访问技术,本质上来说DMA就是一块主板上独立的芯片,通过它来进行内存和IO设备的数据传输,从而减少了CPU的等待时间,但是不论是谁来拷贝,频繁的拷贝耗时也是对性能的影响。
一次简单的传统IO过程,发生了4次用户态和内核态的上下文切换,这在高并发场景下无疑会对性能产生极大的影响。
这整个过程中发生了4次用户态和内核态的上下文切换和4次的拷贝,如下图:
什么是零拷贝?
零拷贝技术是指计算机执行操作的时候,CPU不需要先将数据从某处内存复制到另一个特定的区域,这种技术通常用于网络传输文件时节省CPU周期和内存带宽。
那么针对零拷贝而言,并非是真的没有数据拷贝的过程,只不过是减少了用户态和内核态的切换次数,以及CPU的拷贝次数。
下面来谈谈几种常见的零拷贝技术:
1. mmap + write
简单来说就是通过mmap替换了read + write中的read操作,减少了一次CPU的拷贝。
mmap的主要实现方式是将内核读缓冲区中的地址和用户缓冲区中的地址进行映射,内核缓冲区和用户/应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝,那整个过程发生了4次用户态和内核态的上下文切换和3次拷贝,流程如下:
- 用户通过mmap方法向操作系统发起调用,上下文从用户态转向内核态;
- DMA控制器把数据从硬盘中拷贝到读缓冲区;
- 上下文从内核态转为用户态,mmap调用返回;
- 用户进程通过write方法发起调用,上下文从用户态切换为内核态;
- CPU将内核读缓冲区中的数据拷贝到socket缓冲区(写缓冲);
- DMA控制器把数据从socket缓冲区拷贝到网卡(写入网卡设备),上下文从内核态切换回用户态,write返回。
总的来说,mmap + write的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。
整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。mmap 主要的用处是提高 I/O 性能,特别是针对大文件。对于小文件,内存映射文件反而会导致碎片空间的浪费。
2. sendfile()方式
相比于mmap + write的方式来说,sendfile同样减少了一次CPU拷贝,而且还减少了两次上下文切换。
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
sendfile()是Linux2.1内核版本之后引入的一个系统调用函数,通过使用sendfile,数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝,同时由于sendfile()替代read+write从而减少了一次系统调用(两次用户态和内核态的上下文切换),具体流程如下:
将要读取的文件缓冲区的文件 fd 和要发送的Socket缓冲区的Socket fd 传给sendfile函数,Sendfile 调用中 I/O 数据对用户空间是完全不可见的。也就是说,这是一次完全意义上的数据传输过程。也就是说用户程序不能对数据进行修改,而只是单纯地完成了一次数据传输过程。整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。
3. Sendfile+DMA gather copy
它只适用于将数据从文件拷贝到 socket 套接字上的传输过程。
它将内核空间的读缓冲区(read buffer)中对应的数据描述信息(内存地址、地址偏移量)记录到相应的网络缓冲区( socket buffer)中,由 DMA 根据内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)拷贝到网卡设备中。
这样 DMA 引擎直接利用 gather 操作将页缓存中数据打包发送到网络中即可,本质就是和虚拟内存映射的思路类似。
整个拷贝过程会发生 2 次上下文切换、0 次 CPU 拷贝以及 2 次 DMA 拷贝。
4.Splice零拷贝技术
Splice相当于在Sendfile+DMA gather copy上的提升,Splice 系统调用可以在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline),从而避免了两者之间的 CPU 拷贝操作。
基于 Splice 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝。
总结
由于CPU和IO速度的差异问题产生DMA技术,通过DMA搬运来减少CPU的等待时间。
传统IO:2次DMA拷贝+2次CPU拷贝+4次上下文切换
mmap:2次DMA拷贝+1次CPU拷贝+4次上下文切换
sendfile():2次DMA拷贝+1次CPU拷贝+2次上下文切换
Sendfile+DMA gather copy : 2 次 DMA 拷贝+ 0 次 CPU 拷贝+ 2 次上下文切换splice(): 2 次 DMA 拷贝+ 0 次 CPU 拷贝+ 2 次上下文切换
但是使用sendfile()方式时,IO数据对于用户空间是不可见的。
DMA 技术的推出使得内存与其他组件,例如磁盘、网卡进行数据拷贝时,CPU 仅仅需要发出控制信号,而拷贝数据的过程则由 DMA 负责完成。
Linux 的零拷贝技术有多种实现策略,但根据策略可以分为如下几种类型:
-
减少甚至避免用户空间和内核空间之间的数据拷贝:在一些场景下,用户进程在数据传输过程中并不需要对数据进行访问和处理,那么数据在 Linux 的 Page Cache 和用户进程的缓冲区之间的传输就完全可以避免,让数据拷贝完全在内核里进行,甚至可以通过更巧妙的方式避免在内核里的数据拷贝。这一类实现一般是是通过增加新的系统调用来完成的,比如 Linux 中的 mmap(),sendfile() 以及 splice() 等。
-
绕过内核的直接 I/O:允许在用户态进程绕过内核直接和硬件进行数据传输,内核在传输过程中只负责一些管理和辅助的工作。这种方式其实和第一种有点类似,也是试图避免用户空间和内核空间之间的数据传输,只是第一种方式是把数据传输过程放在内核态完成,而这种方式则是直接绕过内核和硬件通信,效果类似但原理完全不同。
-
内核缓冲区和用户缓冲区之间的传输优化:这种方式侧重于在用户进程的缓冲区和操作系统的页缓存之间的 CPU 拷贝的优化。这种方法延续了以往那种传统的通信方式,但更灵活。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?