传统I/O的过程

当一个进程发起I/O操作(例如读取文件、网络请求等)时,需要通过系统调用(如 read、write、recv、send 等)与操作系统内核进行交互。读取文件时

  1. 数据首先从磁盘读取到内核缓冲区
  2. 数据从内核缓冲区复制到用户空间的缓冲区
  3. 如果数据需要通过网络传输,数据会从用户缓冲区复制到内核空间的网络缓冲区。

虽然I/O操作消耗CPU资源,但相比于CPU密集型任务(如计算密集型任务),I/O操作对CPU的占用通常较低。现代操作系统和硬件(如DMA,直接内存访问)已经尽量优化了这些操作,以减少CPU的负担。

零拷贝

“零拷贝”(Zero Copy)是一种用于减少数据在计算机内存中的复制次数,以提高 I/O 操作的效率。通过零拷贝技术,数据可以直接在 I/O 设备和用户空间之间传输,而不需要通过多次拷贝,从而减少了 CPU 的负担和内存带宽的使用。

零拷贝技术旨在减少这些数据复制过程,具体实现可能有所不同,但大致有以下几种方式:

sendfile 系统调用

直接将文件描述符的数据传输到网络套接字

  1. 数据从磁盘读取到内核缓冲区
  2. 数据直接从内核缓冲区传输到网络缓冲区,避免了内核空间和用户空间之间的复制

sendfile 通常用于高效地传输大块数据,而在用户空间中无法读取和处理数据。例如:文件服务器将文件内容传输给客户端。

mmap 系统调用

允许将文件直接映射到进程的地址空间,使得进程可以直接访问文件的数据

  1. 数据从磁盘读取到内核缓冲区
  2. 数据通过内存映射直接在用户空间访问,不需要额外的复制

用户程序是否可以修改这些数据取决于 mmap 的使用模式和权限设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>

int main() {
int fd = open("example.txt", O_RDWR); // 以读写方式打开文件
if (fd < 0) {
perror("Failed to open file");
return 1;
}

struct stat file_stat;
fstat(fd, &file_stat);

// 将文件映射到内存
char *data = mmap(NULL, file_stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
perror("mmap failed");
close(fd);
return 1;
}

// 修改数据
for (size_t i = 0; i < file_stat.st_size; ++i) {
if (data[i] == 'a') {
data[i] = 'A'; // 将所有 'a' 替换为 'A'
}
}

// 同步修改回文件
if (msync(data, file_stat.st_size, MS_SYNC) == -1) {
perror("msync failed");
}

munmap(data, file_stat.st_size);
close(fd);

return 0;
}
  1. 内存映射权限
    1. PROT_READ:映射区域可读
    2. PROT_WRITE:映射区域可写
    3. PROT_EXEC:映射区域可执行
  2. 映射类型
    1. MAP_SHARED:进程对映射区域的修改会同步到文件
    2. MAP_PRIVATE:进程对映射区域的修改不会影响文件,内核会创建一个写时拷贝(copy-on-write)
  3. 同步修改
    1. 使用 msync 系统调用将修改同步回文件,确保数据的一致性

尽管 mmap 允许修改映射的数据,但这些修改的范围和效果受到一些限制,某些文件系统或设备可能不支持对内存映射区域的写入操作。内核会保护某些内存区域不被修改,进程尝试修改这些区域可能会导致段错误(segmentation fault)

使用场景

  1. mmap 提供了一个简单的编程模型,程序员可以像操作内存一样操作文件数据,无需显式的 I/O 操作
  2. 通过 mmap,多个进程可以共享同一内存区域,实现进程间通信。
  3. 对于非常大的文件,可以使用 mmap 逐步将文件映射到内存,而不必一次性加载整个文件,节省内存。
  4. 如果需要随机访问文件中的数据,而不是顺序读取,mmap 提供了很大的灵活性。

直接内存访问(DMA)

允许 I/O 设备(如磁盘控制器、网络卡等)直接将数据传输到内存,而不需要 CPU 的干预

  1. 数据从磁盘通过 DMA 直接传输到用户空间缓冲区,避免了内核缓冲区的复制过程。

在使用DMA的过程中,程序是无法直接控制和修改正在传输的数据的,因为DMA操作通常在硬件级别完成。在DMA传输过程中,数据在内存中的一致性是由硬件保证的,而不是由用户进程进行控制。

但在DMA传输完成后对数据进行操作

使用场景

  1. DMA 最大的优点是高效的数据传输,减少 CPU 的负担,适合于需要快速传输大量数据的场景。
  2. 在实时系统中,CPU 资源宝贵,需要尽可能减少 CPU 的负担,DMA 是一个很好的选择。
  3. 如果数据传输模式是固定的(例如,从设备到内存,或从内存到设备),DMA 可以很好地处理。

比较和选择

  1. CPU 使用
    1. mmap 会消耗一些 CPU 资源,特别是在处理页错误和内存管理时
    2. DMA 则极大地减少了 CPU 的参与,可以将 CPU 资源用于其他任务
  2. 数据处理
    1. 使用 mmap 时,程序可以直接访问和修改映射的数据,非常灵活
    2. 使用 DMA 时,数据传输后可以在用户空间进行处理,虽然不能在传输过程中修改数据,但可以在传输完成后自由操作数据
  3. 编程复杂度
    1. mmap 的使用较为简单,适合快速开发和实现
    2. DMA 的使用通常需要更复杂的设置和硬件支持,可能需要编写驱动程序或使用特定的库