Linux IO原理和零拷贝机制

目录

1 概述

2 Linux I/O读写方式

2.1 I/O中断原理

2.2. DMA传输原理

2.3 传统I/O方式

3 零拷贝方式

3.1 用户态直接I/O

3.2 mmap + write

3.3 sendfile

3.4 sendfile + DMA gather copy

3.5 splice

3.6 写时复制

3.7 Linux零拷贝对比


1 概述

      零拷贝(Zero-copy)技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间。它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载。实现零拷贝用到的最主要技术是 DMA 数据传输技术内存区域映射技术

  • 零拷贝机制可以减少数据在内核缓冲区和用户进程缓冲区之间反复的 I/O 拷贝操作。
  • 零拷贝机制可以减少用户进程地址空间和内核地址空间之间因为上下文切换而带来的 CPU 开销。

2 Linux I/O读写方式

       Linux 提供了轮询、I/O 中断以及 DMA 传输这 3 种磁盘与主存之间的数据传输机制,具体实现方式如下:

  • 轮询方式:基于死循环对 I/O 端口进行不断检测。
  • I/O 中断方式是指当数据到达时,磁盘主动向 CPU 发起中断请求,由 CPU 自身负责数据的传输过程。
  • DMA 传输:在 I/O 中断的基础上引入了 DMA 磁盘控制器,由 DMA 磁盘控制器负责数据的传输,降低了 I/O 中断操作对 CPU 资源的大量消耗。

2.1 I/O中断原理

      在 DMA 技术出现之前,应用程序与磁盘之间的 I/O 操作都是通过 CPU 的中断完成的。每次用户进程读取磁盘数据时,都需要 CPU 中断,然后发起 I/O 请求等待数据读取和拷贝完成,每次的 I/O 中断都导致 CPU 的上下文切换。

  1. 用户进程向 CPU 发起 read 系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回。
  2. CPU 在接收到指令以后对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区。
  3. 数据准备完成以后,磁盘向 CPU 发起 I/O 中断。
  4. CPU 收到 I/O 中断以后将磁盘缓冲区中的数据拷贝到内核缓冲区,然后再从内核缓冲区拷贝到用户缓冲区。
  5. 用户进程由内核态切换回用户态,解除阻塞状态,然后等待 CPU 的下一个执行时间钟。

2.2. DMA传输原理

       整个数据传输操作在一个 DMA 控制器的控制下进行的。CPU 除了在数据传输开始和结束时做一点处理外(开始和结束时候要做中断处理),在传输过程中 CPU 可以继续进行其他的工作。这样在大部分时间里,CPU 计算和 I/O 操作都处于并行操作,使整个计算机系统的效率大大提高。

有了 DMA 磁盘控制器接管数据读写请求以后,CPU 从繁重的 I/O 操作中解脱,数据读取操作的流程如下:

  1. 用户进程向 CPU 发起 read 系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回。
  2. CPU 在接收到指令以后对 DMA 磁盘控制器发起调度指令。
  3. DMA 磁盘控制器对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区,CPU 全程不参与此过程。
  4. 数据读取完成后,DMA 磁盘控制器会接受到磁盘的通知,将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
  5. DMA 磁盘控制器向 CPU 发出数据读完的信号,由 CPU 负责将数据从内核缓冲区拷贝到用户缓冲区。
  6. 用户进程由内核态切换回用户态,解除阻塞状态,然后等待 CPU 的下一个执行时间钟。

2.3 传统I/O方式

      在 Linux 系统中,传统的访问方式是通过 write() 和 read() 两个系统调用实现的,通过 read() 函数读取文件到到缓存区中,然后通过 write() 方法把缓存中的数据输出到网络端口,伪代码如下:

read(file_fd, tmp_buf, len);
write(socket_fd, tmp_buf, len);

       下图分别对应传统 I/O 操作的数据读写流程,整个过程涉及 2 次 CPU 拷贝、2 次 DMA 拷贝总共 4 次拷贝,以及 4 次上下文切换,下面简单地阐述一下相关的概念。

  • 上下文切换:当用户程序向内核发起系统调用时,CPU 将用户进程从用户态切换到内核态;当系统调用返回时,CPU 将用户进程从内核态切换回用户态。
  • CPU拷贝:由 CPU 直接处理数据的传送,数据拷贝时会一直占用 CPU 的资源。
  • DMA拷贝:由 CPU 向DMA磁盘控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,从而减轻了 CPU 资源的占有率。

3 零拷贝方式

在 Linux 中零拷贝技术主要有 3 个实现思路:用户态直接 I/O、减少数据拷贝次数以及写时复制技术。

  • 用户态直接 I/O:应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间。因此,直接 I/O 不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。
  • 减少数据拷贝次数:在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝,这也是当前主流零拷贝技术的实现思路。
  • 写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作。

3.1 用户态直接I/O

        用户态直接 I/O 使得应用进程或运行在用户态(user space)下的库函数直接访问硬件设备,数据直接跨过内核进行传输,内核在数据传输过程除了进行必要的虚拟存储配置工作之外,不参与任何其他工作,这种方式能够直接绕过内核,极大提高了性能。

       用户态直接 I/O 只能适用于不需要内核缓冲区处理的应用程序,这些应用程序通常在进程地址空间有自己的数据缓存机制,称为自缓存应用程序,如数据库管理系统就是一个代表。其次,这种零拷贝机制会直接操作磁盘 I/O,由于 CPU 和磁盘 I/O 之间的执行时间差距,会造成大量资源的浪费,解决方案是配合异步 I/O 使用。 

3.2 mmap + write

       一种零拷贝方式是使用 mmap + write 代替原来的 read + write 方式,减少了 1 次 CPU 拷贝操作。mmap 是 Linux 提供的一种内存映射文件方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址,mmap + write 的伪代码如下:

tmp_buf = mmap(file_fd, len);
write(socket_fd, tmp_buf, len);

       使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程,然而内核读缓冲区(read buffer)仍需将数据到内核写缓冲区(socket buffer),大致的流程如下图所示:

       基于 mmap + write 系统调用的零拷贝方式,整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝,用户程序读写数据的流程如下:

  1. 用户进程通过 mmap() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. 将用户进程的内核空间的读缓冲区(read buffer)与用户空间的缓存区(user buffer)进行内存地址映射。
  3. CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  4. 上下文从内核态(kernel space)切换回用户态(user space),mmap 系统调用执行返回。
  5. 用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  6. CPU将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
  7. CPU利用DMA控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
  8. 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回。

       mmap 主要的用处是提高 I/O 性能,特别是针对大文件。对于小文件,内存映射文件反而会导致碎片空间的浪费,因为内存映射总是要对齐页边界,最小单位是 4 KB,一个 5 KB 的文件将会映射占用 8 KB 内存,也就会浪费 3 KB 内存。

       mmap 的拷贝虽然减少了 1 次拷贝,提升了效率,但也存在一些隐藏的问题。当 mmap 一个文件时,如果这个文件被另一个进程所截获,那么 write 系统调用会因为访问非法地址被 SIGBUS 信号终止,SIGBUS 默认会杀死进程并产生一个 coredump,服务器可能因此被终止。

3.3 sendfile

       sendfile 系统调用在 Linux 内核版本 2.1 中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。sendfile 系统调用的引入,不仅减少了 CPU 拷贝的次数,还减少了上下文切换的次数,它的伪代码如下:

sendfile(socket_fd, file_fd, len);

       通过 sendfile 系统调用,数据可以直接在内核空间内部进行 I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝。与 mmap 内存映射方式不同的是, sendfile 调用中 I/O 数据对用户空间是完全不可见的。也就是说,这是一次完全意义上的数据传输过程。

       基于 sendfile 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝,用户程序读写数据的流程如下:

  1. 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  3. CPU 将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
  4. CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
  5. 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。

       相比较于 mmap 内存映射的方式,sendfile 少了 2 次上下文切换,但是仍然有 1 次 CPU 拷贝操作。sendfile 存在的问题是用户程序不能对数据进行修改,而只是单纯地完成了一次数据传输过程

3.4 sendfile + DMA gather copy

       Linux 2.4 版本的内核对 sendfile 系统调用进行修改,为 DMA 拷贝引入了 gather 操作。它将内核空间(kernel space)的读缓冲区(read buffer)中对应的数据描述信息(内存地址、地址偏移量)记录到相应的网络缓冲区( socket buffer)中,由 DMA 根据内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)拷贝到网卡设备中,这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作,sendfile 的伪代码如下:

sendfile(socket_fd, file_fd, len);

       在硬件的支持下,sendfile 拷贝方式不再从内核缓冲区的数据拷贝到 socket 缓冲区,取而代之的仅仅是缓冲区文件描述符和数据长度的拷贝,这样 DMA 引擎直接利用 gather 操作将页缓存中数据打包发送到网络中即可,本质就是和虚拟内存映射的思路类似。

 

       基于 sendfile + DMA gather copy 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换、0 次 CPU 拷贝以及 2 次 DMA 拷贝,用户程序读写数据的流程如下:

  1. 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  3. CPU 把读缓冲区(read buffer)的文件描述符(file descriptor)和数据长度拷贝到网络缓冲区(socket buffer)。
  4. 基于已拷贝的文件描述符(file descriptor)和数据长度,CPU 利用 DMA 控制器的 gather/scatter 操作直接批量地将数据从内核的读缓冲区(read buffer)拷贝到网卡进行数据传输。
  5. 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。

        sendfile + DMA gather copy 拷贝方式同样存在用户程序不能对数据进行修改的问题,而且本身需要硬件的支持,它只适用于将数据从文件拷贝到 socket 套接字上的传输过程。

3.5 splice

       sendfile 只适用于将数据从文件拷贝到 socket 套接字上,同时需要硬件的支持,这也限定了它的使用范围。Linux 在 2.6.17 版本引入 splice 系统调用,不仅不需要硬件支持,还实现了两个文件描述符之间的数据零拷贝。splice 的伪代码如下:

splice(fd_in, off_in, fd_out, off_out, len, flags);

       splice 系统调用可以在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline),从而避免了两者之间的 CPU 拷贝操作。

       基于 splice 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝,用户程序读写数据的流程如下:

  1. 用户进程通过 splice() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  3. CPU 在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline)。
  4. CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
  5. 上下文从内核态(kernel space)切换回用户态(user space),splice 系统调用执行返回。

      splice 拷贝方式也同样存在用户程序不能对数据进行修改的问题。除此之外,它使用了 Linux 的管道缓冲机制,可以用于任意两个文件描述符中传输数据,但是它的两个文件描述符参数中有一个必须是管道设备。

3.6 写时复制

        写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么就需要将其拷贝到自己的进程地址空间中。这样做并不影响其他进程对这块数据的操作,每个进程要修改的时候才会进行拷贝,所以叫写时拷贝。这种方法在某种程度上能够降低系统开销,如果某个进程永远不会对所访问的数据进行更改,那么也就永远不需要拷贝。

       缓冲区共享方式完全改写了传统的 I/O 操作,因为传统 I/O 接口都是基于数据拷贝进行的,要避免拷贝就得去掉原先的那套接口并重新改写,所以这种方法是比较全面的零拷贝技术,目前比较成熟的一个方案是在 Solaris 上实现的 fbuf(Fast Buffer,快速缓冲区)。

       fbuf 的思想是每个进程都维护着一个缓冲区池,这个缓冲区池能被同时映射到用户空间(user space)和内核态(kernel space),内核和用户共享这个缓冲区池,这样就避免了一系列的拷贝操作。

      缓冲区共享的难度在于管理共享缓冲区池需要应用程序、网络软件以及设备驱动程序之间的紧密合作,而且如何改写 API 目前还处于试验阶段并不成熟。 

3.7 Linux零拷贝对比

        无论是传统 I/O 拷贝方式还是引入零拷贝的方式,2 次 DMA Copy 是都少不了的,因为两次 DMA 都是依赖硬件完成的。下面从 CPU 拷贝次数、DMA 拷贝次数以及系统调用几个方面总结一下上述几种 I/O 拷贝方式的差别。

拷贝方式CPU拷贝DMA拷贝系统调用上下文切换
传统方式()22read/write4
内存映射()12mmap/write4
sendfile12sendfile2
sendfile + DMA + gather copy02sendfile2
splice02sendfile2

参考:

1、深入剖析Linux IO原理和几种零拷贝机制的实现

2、Linux零拷贝原理

<think>好的,用户现在让我介绍一下Linux中的零拷贝技术。之前他们问过splice()、vmsplice()sendfile()的区别,现在可能想更全面地了解零拷贝的概念原理。我需要先回顾一下之前的回答,确保内容一致,同时补充更多基础知识应用场景。 用户可能是开发者或者系统管理员,对性能优化感兴趣。他们可能已经知道这些系统调用,但想深入理解背后的机制。需要解释清楚零拷贝解决的问题,传统拷贝方式的不足,以及各种零拷贝技术的实现方法。 要注意避免重复之前的内容,但可以适当引用之前的例子。可能需要比较不同的技术,比如sendfile、splice、mmap等,以及它们的适用场景。还要考虑用户可能想知道零拷贝的实际应用,比如在高性能服务器、大数据处理中的应用。 需要检查技术细节的准确性,比如DMA、内核缓冲区、用户空间内核空间的交互。确保术语正确,比如零拷贝并非完全无拷贝,而是减少不必要的拷贝次数。此外,可能需要提到一些工具或命令来验证零拷贝的效果,比如perf或者strace。 最后,结构要清晰,分点说明,帮助用户逐步理解。可能需要用例子或比喻来形象地说明传统拷贝零拷贝的区别。还要注意语言简洁,避免过于技术化,让不同层次的读者都能理解。</think>在Linux系统中,**零拷贝(Zero-Copy)**是一种通过减少数据在内核空间用户空间之间复制次数来提升I/O性能的核心技术。它避免了传统数据拷贝中CPU的冗余操作,尤其在高吞吐量场景(如网络传输、大文件处理)中效果显著。 --- ### 一、传统数据拷贝的瓶颈 #### 典型场景(以读取文件并发送到网络为例): 1. **磁盘 → 内核缓冲区**:通过DMA(直接内存访问)将磁盘数据拷贝到内核缓冲区(无需CPU参与)。 2. **内核缓冲区 → 用户缓冲区**:CPU将数据从内核空间拷贝到用户空间。 3. **用户缓冲区 → Socket缓冲区**:CPU再次将数据拷贝到内核的Socket缓冲区。 4. **Socket缓冲区 → 网卡**:通过DMA将数据发送到网卡。 **问题**:步骤23涉及两次**CPU拷贝**四次**上下文切换**,消耗大量资源。 --- ### 二、零拷贝的核心实现方式 #### 1. **`sendfile()`** - **原理**:直接在内核中完成文件到Socket的数据传输,无需用户空间参与。 - **步骤**: 1. 磁盘 → 内核缓冲区(DMA) 2. 内核缓冲区 → Socket缓冲区(CPU拷贝) 3. Socket缓冲区 → 网卡(DMA) - **拷贝次数**:仅1次CPU拷贝(传统方式为2次) - **系统调用**:`sendfile(out_fd, in_fd, offset, count)` #### 2. **结合`mmap()``write()`** - **原理**:通过内存映射(`mmap`)将文件映射到用户空间,减少一次拷贝。 - **步骤**: 1. 磁盘 → 内核缓冲区(DMA) 2. 内核缓冲区映射到用户空间(零拷贝) 3. 用户空间直接调用`write()`发送到Socket - **拷贝次数**:1次CPU拷贝(内核到Socket缓冲区) - **系统调用**:`mmap()` + `write()` #### 3. **`splice()` + 管道** - **原理**:利用管道作为内核缓冲区的中转,连接两个文件描述符。 - **步骤**(文件 → 管道 → Socket): 1. `splice()`将文件数据移动到管道(内核完成) 2. `splice()`将管道数据移动到Socket - **拷贝次数**:0次CPU拷贝(仅DMA操作) - **系统调用**:`splice(fd_in, fd_out, len, flags)` #### 4. **`vmsplice()`(用户空间优化)** - **原理**:将用户空间的内存页面直接绑定到内核管道缓冲区。 - **步骤**: 1. 用户分配页对齐内存(如`posix_memalign`) 2. `vmsplice()`将内存“挂载”到管道写端 3. `splice()`将管道数据发送到目标(如Socket) - **拷贝次数**:0次CPU拷贝(但需用户内存锁定) --- ### 三、零拷贝技术的对比 | 技术 | 适用场景 | CPU拷贝次数 | 用户空间访问数据 | 限制条件 | |--------------------|----------------------------|-------------|------------------|----------------------------| | `sendfile()` | 文件→Socket | 1次 | 否 | 目标必须为Socket或支持O_DIRECT的文件 | | `mmap()`+`write()` | 文件随机读写 | 1次 | 是 | 小文件可能不划算 | | `splice()` | 任意fd间传输(需管道中转) | 0次 | 否 | 至少一端必须是管道 | | `vmsplice()` | 用户内存→内核传输 | 0次 | 是(但需锁定) | 内存必须页对齐 | --- ### 四、零拷贝的实际应用 1. **Web服务器**(如Nginx): - 使用`sendfile()`高效发送静态文件。 - 大文件下载时吞吐量提升可达2-3倍。 2. **数据库系统**(如Kafka): - 通过`mmap()`快速加载日志文件到内存。 - 减少数据从磁盘到网络的处理延迟。 3. **视频流媒体**: - 使用`splice()`将视频文件从存储直接推送到网络接口。 - 支持高并发实时流传输。 4. **分布式存储**: - 利用零拷贝加速节点间的数据同步。 --- ### 五、验证零拷贝效果的工具 1. **`strace`**: ```bash strace -e trace=sendfile,splice,vmsplice <应用程序> ``` 观察是否实际调用了零拷贝系统调用。 2. **`perf`性能分析**: ```bash perf stat -e cpu-cycles,instructions,cache-references,cache-misses <应用程序> ``` 对比传统拷贝零拷贝的CPU周期缓存命中率。 3. **`vmstat`监控上下文切换**: ```bash vmstat 1 # 查看cs(context switch)次数变化 ``` 零拷贝技术会显著减少上下文切换。 --- ### 六、注意事项 1. **内核版本**: - `sendfile()`在2.2+内核中支持文件→Socket,2.6.33+支持文件→文件。 - `splice()``vmsplice()`需要2.6.17+内核。 2. **数据修改需求**: - 零拷贝技术通常**不允许修改数据**(如加密、压缩),需结合其他方案(如COW复制)。 3. **内存对齐**: - `vmsplice()`要求用户内存按页对齐(通常4KB),否则退化为普通拷贝。 4. **文件类型限制**: - 某些技术不支持特殊文件(如`/proc`下的虚拟文件)。 > **扩展阅读**:Linux零拷贝的底层依赖于**Page Cache****DMA**机制,可进一步研究内核的`io_uring`(新一代异步I/O框架)对零拷贝的优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值