Linux

零拷贝

零拷贝 是一种技术,它允许数据在内存中的移动或操作,而无需进行应用程序和内核之间的数据拷贝,也就是零次多余的拷贝。

什么是零拷贝

零拷贝字面上的意思包括两个,“零” 和 “拷贝”:

  • 拷贝:指的是数据从一个存储位置移至另一个存储位置的过程。
  • :在此上下文中,代表零次多余的拷贝。

零拷贝 是在执行I/O操作时,避免CPU从一个存储区域向另一个存储区域复制数据的技术(应用程序和内核)。这样做可以降低系统的上下文切换次数,减少CPU的负载,并避免不必要的内存使用,从而提高I/O性能。

传统的IO执行流程

1
2
while((n = read(diskfd, buf, BUF_SIZE)) > 0)
write(sockfd, buf , n);
  1. 文件描述符:
    • diskfd 是一个文件描述符,指代在文件系统上的一个文件。
    • sockfd 是一个文件描述符,指代一个已连接的socket。
  2. read系统调用:
    • 在用户空间:
      • 当应用程序执行read函数时,它会通过系统调用请求OS执行数据读取的操作。
    • 在内核空间:
      1. 从磁盘读取数据到内核缓冲区:这个操作是在内核空间中进行的。内核负责管理硬件资源,所以从磁盘读取数据的实际操作是在内核空间完成的。
      2. 从内核缓冲区拷贝到用户缓冲区:这也是在内核空间中完成的。系统需要将内核缓冲区中的数据拷贝到用户空间的缓冲区。
  3. write系统调用:
    • 在用户空间:
      • 当应用程序执行write函数时,它会通过系统调用请求OS执行数据写入的操作。
    • 在内核空间:
      1. 从用户缓冲区拷贝到内核的socket缓冲区:这个操作是在内核空间完成的。系统需要将用户缓冲区中的数据拷贝到内核空间的socket缓冲区。
      2. 从socket缓冲区写入到网卡设备:这也是在内核空间完成的。内核通过网络协议栈处理数据,然后将数据发送到网卡。
  4. 注意事项:
    • 该方法是阻塞的:这意味着,如果 readwrite 系统调用不能立即完成(例如,磁盘很慢或网络有延迟),那么整个进程会被阻塞,直到操作完成。
    • 在现实场景中,为了提高性能和响应时间,通常会使用非阻塞IO、多线程或异步IO。

  1. 用户应用进程调用read函数,向操作系统发起IO调用(系统调用),上下文从用户态转为内核态(切换1)
  2. DMA控制器把数据从磁盘读取到内核缓冲区。
  3. CPU把内核缓冲区数据拷贝到用户应用缓冲区,上下文从内核态转为用户态(切换2),read函数返回。
  4. 用户应用进程通过write函数发起IO调用(系统调用),上下文从用户态转为内核态(切换3)
  5. CPU将用户缓冲区中的数据拷贝到socket缓冲区。
  6. 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,因此数据不再需要从内核空间复制到用户空间,从而避免了一次数据复制。

数据移动次数:三次

上下文切换次数:四次

  1. 用户进程通过mmap方法向操作系统内核发起IO调用,此时上下文从用户态切换为内核态。
  2. 利用DMA控制器,数据从硬盘被传输到内核缓冲区。
  3. 上下文从内核态切换回用户态,mmap方法返回。
  4. 用户进程通过write方法向操作系统内核发起IO调用,此时上下文从用户态切换为内核态。
  5. 数据在内核缓冲区中被准备好并通过DMA直接从socket缓冲区传输到网卡。
  6. 上下文从内核态切换回用户态,write调用返回。

优点:操作文件的速度与操作内存相当,特别适合处理较大的文件,如在NIORocketMQ中的应用。

缺点

  • 对于非常小的文件(例如小于4KB),可能会浪费内存,因为内存页面的最小单位通常为4KB。
  • 如果系统频繁使用mmap操作,且每次映射的大小都不同,可能导致内存碎片化,从而缺乏连续的内存空间。

适用场景

对数据读取后需要进行加工处理的,比如NIO,RocketMQ

sendfile

sendfile是Linux2.1内核版本后引入的一个系统调用函数,API如下:

1
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • out_fd:为待写入内容的文件描述符,一个socket描述符。,
  • in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是socket和管道。
  • offset:指定从读入文件的哪个位置开始读,如果为NULL,表示文件的默认起始位置。
  • count:指定在fdout和fdin之间传输的字节数。

sendfile 是一种专门用于在两个文件描述符之间传输数据的系统调用。它直接在内核中进行数据传输,避免了数据在内核空间与用户空间之间的中间拷贝,从而实现真正的零拷贝传输。这种优化减少了不必要的上下文切换和数据复制,从而提高了文件传输的效率。

sendfile实现的零拷贝流程如下:

数据移动次数:三次

上下文切换次数:两次,使用sendfile()系统调用替换Read和Write系统调用

  1. 用户进程发起sendfile系统调用,此时上下文从用户态切换到内核态。
  2. 通过DMA控制器,数据从硬盘直接拷贝到内核的页缓存。
  3. 数据从页缓存被转移到目标socket的内核缓冲区。
  4. 通过DMA控制器,数据从内核的socket缓冲区传输到网卡。
  5. 上下文从内核态切换回用户态,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
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(),此时从用户态切换到内核态。
  2. 如果源是一个普通文件,DMA(Direct Memory Access)将文件数据从存储设备读入内核缓冲区。
  3. splice() 将数据从源文件描述符直接移动到目标文件描述符,避免了数据复制到用户空间的额外步骤。
  4. 如果目标是网络套接字,数据会被传输到相关的网络缓冲区(通过修改数据的指针),并最终通过 DMA 被发送到网络接口。

管道设备

管道设备文件,也称为 FIFO (First-In, First-Out) 文件,数据流入一端并从另一端流出。虽然通常用于进程间通信,管道也能够配合 splice 等系统调用,实现高效的数据传输。

局限性

splice也有一些局限,它的两个文件描述符参数中有一个必须是管道设备

tee

tee类似splice但是两个fd都必须是管道,而且tee不消耗输入fd的数据


Linux
https://wugengfeng.cn/2022/05/29/Linux/
作者
wugengfeng
发布于
2022年5月29日
许可协议