Linux零拷贝技术
文章目录
零拷贝从字面上理解就可以知道,零拷贝就是cpu不执行数据从一个区域拷贝到另外一个区域的的操作,从而减少上下文的切换以及cpu的拷贝时间
原因
因为Linux传统的IO接口(write read)都是基于数据拷贝的,这样的好处就是,通过中间缓存机制可以减少磁盘(disk)的操作(因为数据已经拷贝到了内核缓存),这时候就会有明显的坏处,大量的数据拷贝会造成用户态与内核态的频繁切换会消耗大量的cpu资源,严重影响数据的传输性能
作用
再数据报从网络设备到用户程序空间传递的过程,减少拷贝次数,减少系统调用,实现cpu零参与,消除cpu在这方面的负载
好处
- 节省了cpu周期
- 减少内存区域之间的数据拷贝,节省内存带宽
- 简述用户态与内核态之间的数据拷贝,提高数据传输效率
- 应用零拷贝技术可以减少用户态以及内核态的上下文切换
实现方式
- DMA数据传输技术
- mmap内存区域映射技术
基础知识
1. 物理内存和虚拟内存
由于操作系统的进程与进程之间是共享 CPU 和内存资源的,因此需要一套完善的内存管理机制防止进程之间内存泄漏的问题。为了更加有效地管理内存并减少出错,现代操作系统提供了一种对主存的抽象概念,即是虚拟内存(Virtual Memory)。虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)。
1.1. 物理内存
物理内存(Physical memory)是相对于虚拟内存(Virtual Memory)而言的。物理内存指通过物理内存条而获得的内存空间,而虚拟内存则是指将硬盘的一块区域划分来作为内存。内存主要作用是在计算机运行时为操作系统和各种程序提供临时储存。在应用中,自然是顾名思义,物理上,真实存在的插在主板内存槽上的内存条的容量的大小。
1.2. 虚拟内存
虚拟内存是计算机系统内存管理的一种技术。 它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间)。而实际上,虚拟内存通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换,加载到物理内存中来。 目前,大多数操作系统都使用了虚拟内存,如 Windows 系统的虚拟内存、Linux 系统的交换空间等等。

通过上面的介绍,我们可以简单的将用户进程申请并访问物理内存(或磁盘存储空间)的过程总结如下:
- 用户进程向操作系统发出内存申请请求
- 系统会检查进程的虚拟地址空间是否被用完,如果有剩余,给进程分配虚拟地址
- 系统为这块虚拟地址创建的内存映射(Memory Mapping),并将它放进该进程的页表(Page Table)
- 系统返回虚拟地址给用户进程,用户进程开始访问该虚拟地址
- CPU 根据虚拟地址在此进程的页表(Page Table)中找到了相应的内存映射(Memory Mapping),但是这个内存映射(Memory Mapping)没有和物理内存关联,于是产生缺页中断
- 操作系统收到缺页中断后,分配真正的物理内存并将它关联到页表相应的内存映射(Memory Mapping)。中断处理完成后 CPU 就可以访问内存了
- 当然缺页中断不是每次都会发生,只有系统觉得有必要延迟分配内存的时候才用的着,也即很多时候在上面的第 3 步系统会分配真正的物理内存并和内存映射(Memory Mapping)进行关联。
1.3 为什么用虚拟内存
在没有虚拟内存的时候会有下面的问题
- 寻址范围有限,32位总线也就寻址
4G - 如果真的为每一个进程分配真正的4G内存空间,那么内存很快就不够
- 没有内存隔离,会造成程序对于数据的践踏
- 内存随机分配不知道程序运行的哪了
1.4 虚拟内存的好处
操作系统为每一个进程分配4G的独立的虚拟地址
- 变相的增大了内存空间每一个进程独享4G的虚拟地址空间
- 内存隔离,保护了数据,同时实现了内存共享技术
- 因为虚拟内存是连续的,在一定程度上减少了内存碎片的产生
1.5 虚拟内存的坏处
- 虚拟内存需要页表进行管理,会消耗一定的空间
- 页表的频繁换入换出消耗时间与资源
- 虚拟地址通过页表转换成实际物理地址消耗时间
- 如果一个页表只保存一部分数据也会浪费
1.6 多级页表
首先为什么要多级页表,因为如果只有一级页表,那么管理一个4G大小的虚拟地址就需要一个大小为4M的页表,这时候如果有100个进程那么问题就来了光页表就需要400M顶不住
这时候就需要多级页表
我们用4K大小的二级页表取管理4M的一级页表,这时候你可能会有疑惑,这时候就会有4k+4M的空间消耗,其实不然,这里其实不是的
只有当二级页表当中有内存我们才去外存中调用一级页表。这时候其实发生的就是缺页中断
中断的流程
保存上下文环境---->判断终端类型----->进入中断程序处理----->恢复上下文环境
中断和异常的区别
中断
设备产生->处理器->操作系统内核
异常
处理器产生->操作系统内核
1.7 快表
快表是基于局部性原理,存放在高速缓存当中
作用就是加速虚拟内存到物理内存的转换
快表中存放着页表的一部分甚至全部内容
快表作为页表的高速缓存器(Cache),作用与页表的本质作用一致,只不过提高了页表的访问速度
原理:如果用页表做地址转换,则读写数据内存时,CPU要访问内存两次。当使用快表,有时只要访问一次高速缓存器,一次主存,这样就可以加速查找并提高指令执行速度
使用快表之后的地址转换流程:
根据虚拟地址中的页号查找快表
若该页在快表中,则直接从快表中读取对应的物理地址
若该页不在快表中,则访问内存中的页表,再从页表中读取物理地址,同时将该页表中的物理地址映射到快表中
当快表填满时,又要增加新的页表,就按照一定的淘汰策略删掉一
内核空间以及用户空间
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。 在 Linux 系统中,内核模块运行在内核空间,对应的进程处于内核态;而用户程序运行在用户空间,对应的进程处于用户态。
内核进程和用户进程所占的虚拟内存比例是 1:3,而 Linux x86_32 系统的寻址空间(虚拟存储空间)为 4G(2的32次方),将最高的 1G 的字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)供内核进程使用,称为内核空间;而较低的 3G 的字节(从虚拟地址 0x00000000 到 0xBFFFFFFF),供各个用户进程使用,称为用户空间。下图是一个进程的用户空间和内核空间的内存布局:

为什么会有内核态用户态的切换
每个普通的用户进程都有一个单独的用户空间,处于用户态的进程不能访问内核空间中的数据,也不能直接调用内核函数的 ,因此要进行系统调用的时候,就要将进程切换到内核态才行。
内核态可以执行任意命令,调用系统的一切资源,而用户态只能执行简单的运算,不能直接调用系统资源。用户态必须通过系统接口(System Call),才能向内核发出指令。比如,当用户进程启动一个 bash 时,它会通过 getpid() 对内核的 pid 服务发起系统调用,获取当前用户进程的 ID;当用户进程通过 cat 命令查看主机配置时,它会对内核的文件子系统发起系统调用。
内核态可以为所欲为但是你用户态只能够进行一些简单计算,其他的都需要借助系统接口
Linux I/O读写方式
Linux 提供了轮询、I/O 中断以及DMA 传输这 3 种磁盘与主存之间的数据传输机制。
-
轮询方式是基于死循环对 I/O 端口进行不断检测。
-
I/O中断方式是指当数据到达时,磁盘主动向 CPU 发起中断请求,由 CPU 自身负责数据的传输过程。 -
DMA传输则在I/O中断的基础上引入了DMA 磁盘控制器,由DMA 磁盘控制器负责数据的传输,降低了I/O 中断操作对 CPU 资源的大量消耗。
I/O中断原理
在 DMA 技术出现之前,应用程序与磁盘之间的 I/O 操作都是通过 CPU 的中断完成的。每次用户进程读取磁盘数据时,都需要 CPU 中断,然后发起 I/O 请求等待数据读取和拷贝完成,每次的 I/O 中断都导致 CPU 的上下文切换。

1. 用户进程向 CPU 发起 read 系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回。
2. CPU 在接收到指令以后对 DMA 磁盘控制器发起调度指令。
3. DMA 磁盘控制器对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区,CPU 全程不参与此过程。
4. 数据读取完成后,DMA 磁盘控制器会接受到磁盘的通知,将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
5. DMA 磁盘控制器向 CPU 发出数据读完的信号,由 CPU 负责将数据从内核缓冲区拷贝到用户缓冲区。
6. 用户进程由内核态切换回用户态,解除阻塞状态,然后等待 CPU 的下一个执行时间钟。
传统的IO操作
read(file_fd, tmp_buf, len);
write(socket_fd, tmp_buf, len);
下图分别对应传统 I/O 操作的数据读写流程,整个过程涉及 2 次 CPU 拷贝、2 次 DMA 拷贝总共 4 次拷贝,以及 4 次上下文切换,下面简单地阐述一下相关的概念。
零拷贝技术
在 Linux 中零拷贝技术主要有 3 个实现思路:
-
户态直接 I/O
-
减少数据拷贝次数
-
写时复制技术。
- 用户态直接 I/O:应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间。因此,直接 I/O 不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。
- 减少数据拷贝次数:在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝,这也是当前主流零拷贝技术的实现思路。
- 写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作。(写时拷贝读时共享)
实现方法
- 用户态直接IO
mmpsendfileDMA辅助sendfile- 写时复制
用户态直接IO

其实就是用户态的程序绕过内核直接访问磁盘IO
但是磁盘读取速度远小于CPU这时候为了避免大量的资源浪费,这时候最好结合异步IO使用
mmp+write
mmap 是 Linux 提供的一种内存映射文件方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址

少一次拷贝
- 用户进程通过 mmap() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
- 将用户进程的内核空间的读缓冲区(read buffer)与用户空间的缓存区(user buffer)进行内存地址映射。
- CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
- 上下文从内核态(kernel space)切换回用户态(user space),mmap 系统调用执行返回。
- 用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
- CPU将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
- CPU利用DMA控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
- 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回。
mmap主要的用处是提高 I/O 性能,特别是针对大文件。对于小文件,内存映射文件反而会导致碎片空间的浪费,因为内存映射总是要对齐页边界,最小单位是 4 KB,一个 5 KB 的文件将会映射占用 8 KB 内存,也就会浪费 3 KB 内存。
mmap 的拷贝虽然减少了 1 次拷贝,提升了效率,但也存在一些隐藏的问题。当 mmap 一个文件时,如果这个文件被另一个进程所截获,那么 write 系统调用会因为访问非法地址被SIGBUS信号终止,SIGBUS 默认会杀死进程并产生一个coredump,服务器可能因此被终止。
sendfile
通过 sendfile 系统调用,数据可以直接在内核空间内部进行 I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝。与 mmap 内存映射方式不同的是,sendfile 调用中 I/O 数据对用户空间是完全不可见的。也就是说,这是一次完全意义上的数据传输过程。

基于 sendfile 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝,用户程序读写数据的流程如下:
- 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
- CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
- CPU 将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
- CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
- 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。
相比较于 mmap 内存映射的方式,sendfile 少了 2 次上下文切换,但是仍然有 1 次 CPU 拷贝操作。sendfile 存在的问题是用户程序不能对数据进行修改,而只是单纯地完成了一次数据传输过程。
DMA辅助sendfile
Linux 2.4 版本的内核对 sendfile 系统调用进行修改,为 DMA 拷贝引入了 gather 操作。它将内核空间(kernel space)的读缓冲区(read buffer)中对应的数据描述信息(内存地址、地址偏移量)记录到相应的网络缓冲区( socket buffer)中,由 DMA 根据内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)拷贝到网卡设备中,这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作
就剩一次,一共两次拷贝,两次用户态内核态切换

写时复制
在某些情况下,内核缓冲区可能被多个进程所共享,如果某个进程想要这个共享区进行 write 操作,由于 write 不提供任何的锁操作,那么就会对共享区中的数据造成破坏,写时复制的引入就是 Linux 用来保护数据的。
写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么就需要将其拷贝到自己的进程地址空间中。这样做并不影响其他进程对这块数据的操作,每个进程要修改的时候才会进行拷贝,所以叫写时拷贝。这种方法在某种程度上能够降低系统开销,如果某个进程永远不会对所访问的数据进行更改,那么也就永远不需要拷贝。
本文探讨了Linux零拷贝技术的基础知识,包括物理内存与虚拟内存的作用、内核态与用户态切换、传统IO操作与零拷贝对比。重点介绍了实现方法,如用户态直接IO、mmap、sendfile和DMA辅助sendfile,以及写时复制的应用。通过减少数据拷贝和上下文切换,提升数据传输效率和CPU性能。
4476

被折叠的 条评论
为什么被折叠?



