传统IO优化方案
传统IO
背景:传统IO场景下,服务器把磁盘文件网络IO发送给客户端Socket
1
2
read(file_fd, user_buf, ...); // 读文件到用户缓冲区
write(sock_fd, user_buf, ...); // 从用户缓冲区写到网络
这个过程中会发生很多次内核用户态切换:
read()
- 磁盘→内核缓存(DMA读入)
DMA:Direct Memory Access 直接内存访问,让硬件自己把数据搬进/搬出内存,CPU 不用一字节一字节地搬。将CPU从繁琐的搬运工作中解放出来,主动承揽内存搬运这种苦力活。
- 内核缓冲→用户缓存(一次拷贝
write()
-
用户缓冲→内核socket缓冲(再次拷贝)
-
内核 socket 缓冲区 → 网卡(DMA 发送)
上述read()和write()都是系统调用,每次都会触发至少一次用户态↔内核态的上下文切换(进入内核/返回用户态),切换多了也耗 CPU。
所以上述传统IO最核心的开销有两种:
- 多次数据拷贝(尤其是“内核↔用户”这两次)
- 多次上下文切换(系统调用 + 内核处理)
针对传统IO的优化
针对传统IO场景,存在以下优化方式。
mmap
mmap:内存映射,让用户态进程除了堆、栈、共享内存区这些可以直接操作的内存区域外,多出了mmap 出来的用户映射区。
mmap是系统调用,用户态申请内核,内核在进程页表中建立映射,让一段用户虚拟地址 指向某些 物理页,并且通过页表权限位保证用户态只能按规则访问被指向的物理页。读文件时可能少一次显式 read 拷贝(不用
read()把数据再拷到用户 buffer)
sendfile
sendfile : 典型的下载加速。是内核提供的“把一个文件送到另一个socket”的系统调用。用户应用发系统调用请求:“把这个文件发到这个 socket”,涉及到的数据流程在内核:页缓存 → socket → 网卡,典型用于:静态文件下载、CDN/对象存储的下行、Nginx 这类场景
方案对比
| 方案 | 用户态是否拿到文件内容 | 内核↔用户拷贝次数 | 适合场景 |
|---|---|---|---|
| read + write | 是 | 多 | 通用、实现简单 |
| mmap + write | 是(像访问内存) | 较少/取决于用法 | 需要在用户态处理数据 |
| sendfile | 否 | 最少 | 纯转发下载(不改内容) |
千万注意:sendfile 的零拷贝是“文件→socket”路径的优化,不是“内存→socket”的通用优化。mmap 映射出来的是给用户态读写的内存视图,不是 sendfile 的输入类型,所以“先 mmap 写、再 sendfile 发”本质是在绕路把内存伪装成文件,通常不划算;更现实的方案是 mmap + write(或直接 write)。