Linux
零拷贝
零拷贝
是一种技术,它允许数据在内存中的移动或操作,而无需进行应用程序和内核之间的数据拷贝,也就是零次多余的拷贝。
什么是零拷贝
零拷贝字面上的意思包括两个,“零” 和 “拷贝”:
拷贝
:指的是数据从一个存储位置移至另一个存储位置的过程。零
:在此上下文中,代表零次多余的拷贝。
零拷贝
是在执行I/O操作时,避免CPU从一个存储区域向另一个存储区域复制数据的技术(应用程序和内核)。这样做可以降低系统的上下文切换次数,减少CPU的负载,并避免不必要的内存使用,从而提高I/O性能。
传统的IO执行流程
1 |
|
文件描述符
:diskfd
是一个文件描述符,指代在文件系统上的一个文件。sockfd
是一个文件描述符,指代一个已连接的socket。
read系统调用
:- 在用户空间:
- 当应用程序执行
read
函数时,它会通过系统调用请求OS执行数据读取的操作。
- 当应用程序执行
- 在内核空间:
- 从磁盘读取数据到内核缓冲区:这个操作是在内核空间中进行的。内核负责管理硬件资源,所以从磁盘读取数据的实际操作是在内核空间完成的。
- 从内核缓冲区拷贝到用户缓冲区:这也是在内核空间中完成的。系统需要将内核缓冲区中的数据拷贝到用户空间的缓冲区。
- 在用户空间:
write系统调用
:- 在用户空间:
- 当应用程序执行
write
函数时,它会通过系统调用请求OS执行数据写入的操作。
- 当应用程序执行
- 在内核空间:
- 从用户缓冲区拷贝到内核的socket缓冲区:这个操作是在内核空间完成的。系统需要将用户缓冲区中的数据拷贝到内核空间的socket缓冲区。
- 从socket缓冲区写入到网卡设备:这也是在内核空间完成的。内核通过网络协议栈处理数据,然后将数据发送到网卡。
- 在用户空间:
注意事项
:- 该方法是阻塞的:这意味着,如果
read
或write
系统调用不能立即完成(例如,磁盘很慢或网络有延迟),那么整个进程会被阻塞,直到操作完成。 - 在现实场景中,为了提高性能和响应时间,通常会使用非阻塞IO、多线程或异步IO。
- 该方法是阻塞的:这意味着,如果
- 用户应用进程调用read函数,向操作系统发起IO调用(系统调用),上下文从用户态转为内核态(切换1)。
- DMA控制器把数据从磁盘读取到内核缓冲区。
- CPU把内核缓冲区数据拷贝到用户应用缓冲区,上下文从内核态转为用户态(切换2),read函数返回。
- 用户应用进程通过write函数发起IO调用(系统调用),上下文从用户态转为内核态(切换3)。
- CPU将用户缓冲区中的数据拷贝到socket缓冲区。
- DMA控制器将数据从socket缓冲区传输到网卡设备,上下文从内核态切换回用户态(切换4),write函数返回。
总结:传统的IO流程确实会经历四次上下文切换和四次数据拷贝。
CPU上下文切换
一般我们说的上下文切换,是指操作系统在 CPU 上切换进程或线程的状态,包括用户态和内核态之间的转换,以及内核态下不同进程或线程之间的切换。进程从用户态切换到内核态,通常是通过系统调用来完成的。在系统调用的过程中,会涉及到 CPU 上下文的切换。这个过程包括保存当前进程或线程的 CPU 寄存器状态,然后加载新的进程或线程的 CPU 寄存器状态,以便新的进程或线程可以接着之前的状态继续运行。具体来说,CPU 寄存器里原来的用户态指令位置需要被保存起来,然后 CPU 寄存器需要更新为内核态指令的新位置,最后 CPU 才能跳转到内核态运行 内核任务
。
用户空间和内核空间
- 什么是系统内核
- 内核是操作系统的核心组成部分,它作为应用程序和硬件之间的桥梁,负责管理系统的进程、内存、设备驱动程序、文件系统和网络功能等。内核的设计和性能直接影响整个系统的性能和稳定性。
- 内核空间:系统内核运行的空间
- 用户空间:应用程序运行的空间
- 为什么要区分内核空间和用户空间:
- 为了确保操作系统的稳定性和可用性,应用程序被限制在用户空间运行,而不能直接访问硬件或执行某些关键的系统操作。这些底层的任务,如读写磁盘文件、分配和回收内存或从网络接口读写数据,都必须通过内核提供的接口来完成。
- 具体实现:比如 Intel 的 CPU 将特权等级分为 4 个级别:Ring0~Ring3
- 当进程运行在 Ring3 级别时被称为运行在用户态
- 而运行在 Ring0 级别时被称为运行在内核态
DMA拷贝
- DMA,全称为Direct Memory Access,即直接内存访问。DMA是一种硬件级的数据传输技术,通常由主板上的独立DMA控制器芯片实现。它允许外设设备和内存存储器之间直接进行IO数据传输,而不需要CPU的参与。
- DMA的主要任务是协助CPU处理IO请求和数据拷贝。
- DMA使硬件设备能够直接与内存存储器进行数据IO传输,提高了数据传输的效率。
虚拟内存
现代操作系统使用虚拟内存技术,其中系统为应用程序提供虚拟地址,而不是直接的物理地址。使用虚拟内存有以下好处:
- 虚拟内存空间通常远大于物理内存空间。这是因为虚拟内存不仅仅是物理内存,还包括使用磁盘上的一部分作为"交换空间"或"页面文件"。
- 多个虚拟地址可以映射到同一个物理地址。这使得多个进程或线程可以共享同一物理内存地址,从而实现内存共享。
正是因为多个虚拟地址可以映射到同一个物理地址,内核空间和用户空间的虚拟地址可以映射到同一个物理地址。这种方式可以减少在数据传输时的数据拷贝次数。
- 虚拟地址到物理地址的转换过程称为地址翻译。
- 通过多个虚拟地址共同指向同一个物理地址,可以在内核空间与用户空间或应用程序之间实现内存共享,从而减少数据复制。
实现
零拷贝的核心是减少无用的数据复制次数,优化IO。其实现是通过系统调用(调用系统内核函数)
mmap
使用 mmap
方法代替传统的IO方式,核心是利用虚拟内存的映射特性。这使得用户空间和内核空间可以共享相同的Read Buffer
,因此数据不再需要从内核空间复制到用户空间,从而避免了一次数据复制。
数据移动次数
:三次
上下文切换次数
:四次
- 用户进程通过
mmap
方法向操作系统内核发起IO调用,此时上下文从用户态切换为内核态。 - 利用DMA控制器,数据从硬盘被传输到内核缓冲区。
- 上下文从内核态切换回用户态,
mmap
方法返回。 - 用户进程通过
write
方法向操作系统内核发起IO调用,此时上下文从用户态切换为内核态。 - 数据在内核缓冲区中被准备好并通过DMA直接从socket缓冲区传输到网卡。
- 上下文从内核态切换回用户态,
write
调用返回。
优点
:操作文件的速度与操作内存相当,特别适合处理较大的文件,如在NIO
和RocketMQ
中的应用。
缺点
:
- 对于非常小的文件(例如小于4KB),可能会浪费内存,因为内存页面的最小单位通常为4KB。
- 如果系统频繁使用
mmap
操作,且每次映射的大小都不同,可能导致内存碎片化,从而缺乏连续的内存空间。
适用场景
对数据读取后需要进行加工处理的,比如NIO
,RocketMQ
sendfile
sendfile是Linux2.1内核版本后引入的一个系统调用函数,API如下:
1 |
|
- out_fd:为待写入内容的文件描述符,一个socket描述符。,
- in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是socket和管道。
- offset:指定从读入文件的哪个位置开始读,如果为NULL,表示文件的默认起始位置。
- count:指定在fdout和fdin之间传输的字节数。
sendfile
是一种专门用于在两个文件描述符之间传输数据的系统调用。它直接在内核中进行数据传输,避免了数据在内核空间与用户空间之间的中间拷贝,从而实现真正的零拷贝传输。这种优化减少了不必要的上下文切换和数据复制,从而提高了文件传输的效率。
sendfile实现的零拷贝流程如下:
数据移动次数:三次
上下文切换次数:两次,使用sendfile()系统调用替换Read和Write系统调用
- 用户进程发起sendfile系统调用,此时上下文从用户态切换到内核态。
- 通过DMA控制器,数据从硬盘直接拷贝到内核的页缓存。
- 数据从页缓存被转移到目标socket的内核缓冲区。
- 通过DMA控制器,数据从内核的socket缓冲区传输到网卡。
- 上下文从内核态切换回用户态,sendfile调用返回。
Linux 2.4内核优化SG-DMA拷贝
使用scatter/gather DMA
技术,允许数据直接从硬盘拷贝到网卡,无需中间内存间拷贝。
数据移动次数:两次 (都是DMA拷贝)
上下文切换次数:两次
优点
:
- 2次上下文切换,0次CPU拷贝,2次DMA拷贝 — 实现了真正意义上的零拷贝。
- 非常适合大文件传输。
缺点
:
- 需要硬件支持DMA技术。
- 不能进行数据的中间处理或修改。
适用场景
:
- 对于应用层不需要对数据进行处理的场景,例如,直接将硬盘上的文件发送到网卡。如
kafka
的高吞吐量实现。 - 需要注意的是,sendfile存在限制。特别是在某些版本的Linux内核中,数据源不能是socket,而数据的目标必须是socket。这意味着sendfile主要用于文件到网络的传输,限制了其使用范围
splice
鉴于 Sendfile 的缺点,在 Linux2.6.17 中引入了 Splice,它在读缓冲区和网络操作缓冲区之间建立管道避免 CPU 拷贝:先将文件读入到内核缓冲区,然后再与内核网络缓冲区建立管道。它的函数原型
1 |
|
- 用户进程调用
splice()
,此时从用户态切换到内核态。 - 如果源是一个普通文件,DMA(Direct Memory Access)将文件数据从存储设备读入内核缓冲区。
splice()
将数据从源文件描述符直接移动到目标文件描述符,避免了数据复制到用户空间的额外步骤。- 如果目标是网络套接字,数据会被传输到相关的网络缓冲区(通过修改数据的指针),并最终通过 DMA 被发送到网络接口。
管道设备
管道设备文件,也称为 FIFO (First-In, First-Out) 文件,数据流入一端并从另一端流出。虽然通常用于进程间通信,管道也能够配合 splice
等系统调用,实现高效的数据传输。
局限性
splice也有一些局限,它的两个文件描述符参数中有一个必须是管道设备
tee
tee类似splice但是两个fd都必须是管道,而且tee不消耗输入fd的数据