0%

《操作系统》IO 零拷贝技术

DMA

DMA 的全称叫直接内存存取(Direct Memory Access),是一种允许外围设备(硬件子系统)直接访问系统主内存的机制。

也就是说,基于 DMA 访问方式,系统主内存于硬盘或网卡之间的数据传输可以绕开 CPU 的全程调度。

目前大多数的硬件设备,包括磁盘控制器、网卡、显卡以及声卡等都支持 DMA 技术。

整个数据传输操作在一个 DMA 控制器的控制下进行的。

CPU 除了在数据传输开始和结束时做一点处理外(开始和结束时候要做中断处理),在传输过程中 CPU 可以继续进行其他的工作。

这样在大部分时间里,CPU 计算和 I/O 操作都处于并行操作,使整个计算机系统的效率大大提高。

DMA技术下,read系统调用读取文件的过程

  1. 用户进程向 CPU 发起 read 系统调用读取数据,由用户态切换为内核态,然后一直休眠阻塞等待数据的返回(假设文件描述符被设置为阻塞式)。
  2. CPU 在接收到指令以后对 DMA 磁盘控制器发起调度指令,然后CPU调度到其他进程继续工作
  3. DMA 磁盘控制器对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区,CPU 全程不参与此过程。
  4. 数据读取完成后,DMA 磁盘控制器会接受到磁盘的通知,将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
  5. DMA 磁盘控制器向 CPU 发出数据读完的中断信号,由 CPU 负责将数据从内核缓冲区拷贝到用户缓冲区。
  6. 用户进程由内核态切换回用户态,唤醒进程解除阻塞状态,然后接着执行

内核缓冲区

用户进程在进行文件 IO 操作时,比如读文件,并不是每次都会从磁盘拿数据,而是先从内核缓冲区拿数据,如果内核缓冲区没有,则由内核一次性想磁盘索要一批数据放入内核缓冲区

如果内核缓冲区已经有了,则直接返回,避免了对磁盘的读取(磁盘读写相对于内存读写非常耗时)

写文件也是一样的,先写内核缓冲区,由内核控制什么时机写入磁盘

read、write 系统调用只是将数据读写到内核缓冲区,写入磁盘他们不管

socket 缓冲区

除了针对文件 IO 操作的内核缓冲区,还有网络 IO 的 socket 缓冲区

每个网络 IO 都有两个 socket 缓冲区,一个是读缓冲区,一个是写缓冲区

用户缓冲区

用户缓冲区就是用户态的缓冲区,一般存在于标准库中,比如 Golang 中的 bufio 包

因为用户态进程不能直接访问内核缓冲区,所有必然存在内核缓冲区中数据向用户空间拷贝的动作(其实就发生在 read 调用时,返回的 bytes 就是从内核缓冲区拷贝过来的数据),拷贝过来的数据可以选择放入 bufio 这样的用户缓冲区中,当然也可以不使用用户缓冲区而直接操作数据

零拷贝技术

首先看一个场景:读取一个文件的内容,然后将内容通过网卡发送出去

传统的 read、write 系统调用的步骤

  1. 用户进程通过 read 函数向内核发起系统调用,上下文从用户态切换为内核态
  2. CPU 利用 DMA 控制器将数据从硬盘拷贝到内核缓冲区
  3. CPU 将内核缓冲区中的数据拷贝到用户空间
  4. 上下文从内核态切换回用户态,read 调用执行返回。
  5. 用户进程通过 write 函数向内核发起系统调用,上下文从用户态切换为内核态
  6. CPU 将用户空间的数据拷贝到 socket 写缓冲区中
  7. write 调用返回,回到用户态
  8. CPU 利用 DMA 控制器将 socket 写缓冲区中的数据写入网卡

上面的步骤总共触发了 4 次上下文切换、2 次 CPU 拷贝、2 次 DMA 拷贝

零拷贝技术是指 0 次 CPU 拷贝,有以下实现方式

mmap + write

上面的场景下,大致是下面两行代码

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

mmap 将内核缓冲区直接一一映射到了用户虚拟空间,用户进程对虚拟空间的操作直接作用到内核缓冲区,从而直接作用到了文件中

1
2
3
4
5
mmap 原理:

mmap 执行后其实并没有发生文件到内核缓冲区的拷贝或者内核缓冲区到用户空间的拷贝,没有真正分配物理内存

用户进程在访问 mmap 返回的虚拟空间地址时,会发现并没有对应的物理地址,进而发生缺页中断,触发中断处理程序,内核会检查 mmap 表将对应的文件从磁盘中获取一部分内容到内核缓冲区

mmap 系统调用返回一个用户空间指针,可以通过这个指针直接访问数据,从而避免了内核空间向用户空间的数据拷贝

针对上面场景的步骤如下:

  1. mmap 系统调用,用户态切换到内核态
  2. 内核完成一一映射,mmap 系统调用返回,切换回用户态
  3. 使用 write 系统调用,用户态切换到内核态
  4. 内核缺页中断,DMA 控制器从磁盘获取数据到内核缓冲区
  5. CPU 将数据从内核缓冲区复制到 socket 缓冲区
  6. DMA 控制器将数据从 socket 缓冲区写入网卡

全程发生了 4 次上下文切换、2 次 DMA 复制、1 次 CPU 复制

相对于传统的 read、write,只是减少了一次 CPU 复制,效率有所提升

sendfile

1
sendfile(socket_fd, file_fd, len);

sendfile 就是讲一个 fd 数据拷贝到另一个 fd,完全不经过用户空间

步骤如下:

  1. sendfile 系统调用,用户态切换到内核态
  2. DMA 控制器将数据从硬盘拷贝到内核缓冲区
  3. CPU 将数据从内核缓冲区拷贝到 socket 缓冲区
  4. DMA 控制器将数据从 socket 缓冲区拷贝到网卡

全程发生了 2 次上下文切换、2 次 DMA 复制、1 次 CPU 复制

相对 mmap 又减少了 2 次上下文切换

sendfile + DMA gather copy

Linux 2.4 版本的内核对 sendfile 系统调用进行修改,为 DMA 拷贝引入了 gather 操作。

它将内核缓冲区中对应的数据描述信息(内存地址、地址偏移量)记录到相应的 socket 缓冲区中,由 DMA 根据内存地址、地址偏移量将数据批量地从内核缓冲区拷贝到网卡设备中,这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作

步骤如下:

  1. sendfile 系统调用,用户态切换到内核态
  2. DMA 控制器将数据从硬盘拷贝到内核缓冲区
  3. DMA 控制器将数据从内核缓冲区拷贝到网卡

全程发生了 2 次上下文切换、2 次 DMA 复制、0 次 CPU 复制

splice

sendfile 只适用于将数据从文件拷贝到 socket 套接字上,同时需要硬件的支持,这也限定了它的使用范围。

Linux 在 2.6.17 版本引入 splice 系统调用,不仅不需要硬件支持,还实现了两个文件描述符之间的数据零拷贝

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

步骤如下:

  1. splice 系统调用,用户态切换为内核态
  2. DMA 控制器将数据从硬盘拷贝到内核缓冲区
  3. CPU 在内核缓冲区和 socket 缓冲区之间建立管道
  4. DMA 控制器将数据从 socket 缓冲区拷贝到网卡进行数据传输

全程发生了 2 次上下文切换、2 次 DMA 复制、0 次 CPU 复制




微信关注我,及时接收最新技术文章