A11 —— 用户和内核的内存边界:零拷贝的技术

金句:数据从磁盘到网络,最快的路径是根本不复制。


用户态和内核态的内存边界

用户程序和内核有各自的地址空间,交互需要通过系统调用或特定的机制。

Bash
用户态和内核态的隔离:

用户地址空间(0x00000000 ~ 0x7FFFFFFFFFFF):
  - 每个进程有独立的页表
  - 只能访问自己的虚拟地址
  - 访问内核地址 → 崩溃

内核地址空间(0xFFFF800000000000 ~):
  - 所有进程共享
  - 内核可以访问任何物理内存
  - 用户态不能直接访问

数据交互的方式:

1. 系统调用参数传递(少量数据):
   read(fd, buf, 1024);  // buf 在用户态,内核写进去

2. mmap(大量数据):
   mmap(NULL, size, PROT, MAP_SHARED, fd, 0);
   // 用户态直接访问文件内容,内核参与 page fault

3. 共享内存(进程间):
   shm_open() / mmap(MAP_SHARED)
   // 用户态和内核共享同一块物理页

传统 I/O 的数据拷贝路径

Bash
传统 read/write 的数据路径:

读取文件到 socket(传统方式):

  [磁盘]                    [Page Cache]                    [用户 Buffer]                    [Socket Buffer]                    [网络]
    │                            │                             │                                │                            │
    ▼                            ▼                             ▼                                ▼                            ▼
  DMA ──────────────→ 物理页(Page Cache) ◄──── 内核读文件    │                                │                            │
                                                              │                                │                            │
                                                           copy ──────────────────────────► │                                │
                                                                                             │                                │
                                                           copy ◄──────────────────────────── │                                │
                                                                                                                    copy ────────────► │
                                                                                                                        │                            │
                                                                                                                         ▼                            ▼
                                                                                                                    socket buffer                DMA
                                                                                                                    to network

复制次数:
  1. 磁盘 → Page Cache(DMA)
  2. Page Cache → 用户 Buffer(memcpy)
  3. 用户 Buffer → Socket Buffer(memcpy)
  → 4 次 DMA/memcpy

用户态参与:
  → CPU 执行 memcpy
  → 用户态和内核态切换(syscall)
  → 高延迟,高 CPU 占用

零拷贝(Zero-Copy)技术

零拷贝的目标是减少甚至消除用户态和内核态之间的数据拷贝。

Bash
sendfile() 系统调用:

#include <sys/sendfile.h>
ssize_t sendfile(out_fd, in_fd, &offset, count);

数据路径:

  [磁盘] → [Page Cache] → [Socket Buffer] → [网络]
              │               │
              │               │
           DMA ──────────► │
                            │
                            copy(Page Cache → Socket Buffer)
                            // 仍然有 1 次 copy

适合场景:
  - 文件到 socket 的传输(静态文件服务)
  - 不需要处理数据,直接转发

splice() 系统调用:

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

  - 在两个文件描述符之间移动数据
  - 不经过用户态
  - 内部使用管道缓冲区作为中转

数据路径:
  [磁盘] → [Page Cache] → [pipe] → [Socket Buffer] → [网络]
              │              │               │
              │              │               │
           DMA              zero-copy       copy(pipe → socket)
                            // 内核内部移动,不需要用户态

Bash
mmap + write(内存映射文件):

1. mmap 把文件映射到用户地址空间
2. write 把数据写入 socket
3. 内核自动把 Page Cache 的数据发送到网络

改进点:
  - 减少一次 User Buffer 的 memcpy
  - 但仍然需要 Page Cache → Socket Buffer copy

data sync:
  msync() / fsync() 确保数据写回磁盘(不用于零拷贝)

零拷贝的完整实现(真正的零 copy):
  - 需要硬件支持:RDMA(Remote Direct Memory Access)
  - 或者:网卡支持 scatter-gather DMA
  - 数据直接从 Page Cache → 网卡,不需要中间 copy

DMA 和零拷贝

Bash
传统 DMA vs RDMA:

传统 DMA:
  - 数据必须经过物理内存
  - CPU 参与设置 DMA 描述符
  - 系统调用有开销

RDMA(Remote Direct Memory Access):
  - 网卡直接读写远程内存
  - 零拷贝:网卡 → 远程内存
  - 无 CPU 参与,无系统调用开销
  - 适用:高性能计算、机器学习、存储

InfiniBand / RoCE / iWARP:
  - RDMA 的网络实现
  - 数据传输绕过内核
  - 延迟 < 1μs(vs TCP 10~100μs)

Bash
网卡 scatter-gather:

支持 SG 的网卡可以直接从多个内存区域读取数据:

  skb(socket buffer)包含多个物理地址:
    - iovec[0]: page 0, offset 0, len 1500
    - iovec[1]: page 1, offset 0, len 1500
  → 网卡直接从这两个页 DMA 数据

Linux 的实现:
  - skb(socket buffer)管理数据
  - page pool:零拷贝的页面管理
  - XDP(Express Data Path):跳过协议栈,直接网卡处理

总结

  • 用户/内核隔离:用户态独立页表,内核全局访问,需要系统调用交互
  • 传统 I/O:磁盘→Page Cache→用户 Buffer→Socket Buffer,4次复制,CPU 参与
  • sendfile():Page Cache→Socket Buffer,2次复制(适合文件到 socket)
  • splice():通过 pipe 中转,内核内部零拷贝,不需要用户态
  • RDMA:网卡直接读写远程内存,完全零拷贝,需要特殊硬件

下篇预告(A12):A 层最后一篇——所有层之间的内存流动:物理内存→页表→buddy→SLAB→用户malloc→mmap→应用→内存泄漏检测,完整总结整个内存系统。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年3月4日

作者

评论

发表评论

您的邮箱地址不会被公开。