Linux 系统零拷贝--什么是零拷贝以及零拷贝实现原理

原文链接:https://blog.devgenius.io/linux-zero-copy-d61d712813fe

许多 Web 应用程序都会提供大量静态文件内容,这相当于从磁盘读取数据后再将完全相同的数据写回 socket。每次数据经过用户态内核边界时,都必须进行数据拷贝,这个过程会消耗 CPU 时间片,占用内存带宽。

零拷贝技术在这种场景下就可以发挥作用,零拷贝的目的是消除内核态和用户态之间所有不必要的数据拷贝。无论是 Kafka 还是 Netty,都用到了零拷贝的知识。那么到底什么是零拷贝?我们将在本文中进行探究。

什么是零拷贝

  • :意味着拷贝数据的次数是 0 次;
  • 拷贝:意味着数据需要从一种存储介质转移到另外一种存储介质。

所以,如果我们把 拷贝 组合在一起,零拷贝是指计算机在进行 IO 操作时,CPU 不需要将数据从一个存储介质拷贝到另一个存储介质,从而减少上下文切换和 CPU 拷贝时间,零拷贝是一种 IO 操作优化技术。

传统 IO 的执行流程

例如我们要实现一个下载功能,服务器的任务就是将服务器主机磁盘上的文件从连接的 socket 中发送出去。关键代码如下:

1
2
while((n = read(diskfd, buf, BUF_SIZE)) > 0)
write(sockfd, buf , n);

传统的 IO 流程包括读和写的过程:

  • :从磁盘读取数据到内核缓冲区,再拷贝到用户缓冲区;
  • :首先将数据写入到 socket 缓冲区,最后写到网卡设备。

完整的流程如下所示:

  • 应用程序调用 read 函数,向操作系统发起 IO 请求,上下文从用户态切换到内核态;
  • DMA 控制器从磁盘读取数据到内核缓冲区;
  • CPU 读取内核缓冲区数据并将数据拷贝到用户应用程序缓冲区,上下文从内核态切换到用户态,read 函数调用返回结果;
  • 用户应用进程通过 write 函数发起 IO 请求,上下文从用户态切换到内核态并将数据复制到 socket 缓冲区;
  • DMA 控制器将数据从 socket 缓冲区复制到网卡设备,上下文从内核态切换到用户态,此时 write 函数返回结果。

从流程图可以看出,传统的 IO 流程包括 4 次上下文切换,4 次数据拷贝(2CPU 拷贝和 2DMA拷贝)。

内核空间和用户空间

服务器上运行的应用程序需要通过操作系统来执行一些特殊的操作,如磁盘文件的读写、内存的读写等。

因为这些都是比较危险的操作,应用程序不能乱来,只能交给底层操作系统来完成。

因此,操作系统为用户应用分配了两种内存空间:用户空间和内核空间。

  • 内核空间:主要提供进程调度、内存分配、硬件资源连接等功能;
  • 用户空间:提供给每个程序进程的空间,它没有访问内核空间资源的权限。如果应用程序需要使用内核空间的资源,需要经过操作系统进行调用。进程从用户空间切换到内核空间,完成相关操作后,再从内核空间切换回用户空间。

用户态和内核态

  • 如果进程运行在内核空间,则称为进程的内核态。
  • 如果进程运行在用户空间,则称为进程的用户态。

DMA

DMA 表示直接内存访问。它本质上是主板上一个独立的芯片,它允许外围设备和内存存储之间直接进行 IO 数据传输,并且这个过程不需要 CPU 的参与。

简单来说就是帮助 CPU 转发 IO 请求,进行拷贝数据。为什么需要它呢?主要是为了提升效率,它帮助 CPU 做一些事情,这段时间 CPU 可以空闲下来做其它事情,这就提高了 CPU 的利用效率。

我们来看一下这个过程:

  • 应用程序调用 read 函数,向操作系统发起 IO 请求,同时进入阻塞状态等待数据返回;
  • CPU 收到指令后,向 DMA 控制器发起指令调度;
  • DMA 收到请求后,向磁盘发送请求;
  • 磁盘将数据读入磁盘控制器缓冲区,并通知 DMA
  • DMA 将数据从磁盘控制器缓冲区拷贝到内核缓冲区;
  • DMACPU 发送一个读取数据的信号,CPU 负责将数据从内核缓冲区复制到用户缓冲区;
  • 用户应用进程从内核态切换到用户态并解除阻塞状态。

如何实现零拷贝

我们已经理解了 DMA 的工作原理,下面我们来讨论一下如何实现零拷贝。首先,零拷贝并不意味着不进行数据拷贝,而是减少用户态和内核态上下文切换次数和 CPU 拷贝次数;有两种常见的方式实现零拷贝

  • 方案一mmap + write
  • 方案二sendfile

mmap + write

mmap 的函数定义如下:

1
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

通过 mmap + write 实现零拷贝处理流程如下图所示:

read() 函数调用相比,这里的主要区别是用户进程通过调用 mmap 方法向操作系统内核发起 IO 调用,上下文从用户态切换到内核态,然后 CPU 使用 DMA 控制器将数据从硬盘复制到内核缓冲区。主要步骤是:

  • 用户进程通过调用 mmap 方法向操作系统内核发起 IO 调用,上下文从用户态切换到内核态;
  • CPU 通过 DMA 控制器将数据从磁盘拷贝到内核缓冲区;
  • 上下文从内核态切换到用户态,mmap 方法调用返回结果;
  • 用户进程通过调用 write 方法再次对操作系统内核进行 IO 调用,上下文从用户态切换到内核状态,最终写入到 socket 缓冲区;
  • CPU 通过 DMA 控制器将数据从 socket 缓冲区拷贝到网卡设备,上下文从内核态切换到用户态,最后 write 方法返回结果。

我们发现通过 mmap + write 实现的零拷贝技术发生了 4 次上下文切换和 3 次拷贝(2DMA 拷贝和 1CPU 拷贝)。

sendfile()

sendfileLinux 2.1 版本后内核引入的系统函数,函数定义如下:

1
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

sendfile 是指在两个文件描述符之间传递数据,它是在操作系统内核中完成的,因此避免了内核缓冲区和用户缓冲区数据拷贝过程,可以用来实现零拷贝技术。

流程如下图所示:

  • 用户进程发起 sendfile 系统调用,上下文从用户态切换到内核态;
  • DMA 控制器从磁盘拷贝数据到内核缓冲区;
  • CPU 将读缓冲区中的数据复制到 socket 缓冲区;
  • DMA 控制器将 socket 缓冲区中的数据异步复制到网卡设备;
  • 上下文从内核态切换到用户态,sendfile 函数调用返回;

我们发现通过 sendfile 实现的零拷贝技术只发生了 2 次上下文切换和 3 次拷贝(2DMA 拷贝和 1CPU 拷贝)。

扩展资料

感谢您的阅读,本文由 董宗磊的博客 版权所有。如若转载,请注明出处:董宗磊的博客(https://dongzl.github.io/2023/03/05/04-Linux-Zero-Copy/
深入探究 MySQL 数据库 Performance Schema
表不存在:MySQL 中 lower_case_table_names 问题探究