IO 地图

点一个框看它做什么、在哪篇讲过;选一条路径,一步一步看一次 read、一次 recv、一次 sendfile 怎么走。蓝字可以直接点开对应的文章或地图。

IO 地图:数据怎么在设备、内核和你的缓冲区之间走 上面是用户态,中间是内核,下面是设备和内存。左边一列是文件,中间一列是网络。数字都是量级。 橙线 = CPU 拷贝;蓝色虚线 = DMA(设备直接读写内存,不占 CPU);绿色虚线 = 映射,不拷贝;灰线 = 调用或通知。 蓝字 = 在哪里讲过,进程地图、硬件地图会打开对应的那张图。 用户态:你的进程 系统调用:同一个线程切到内核态,一次来回约 100 ns 到几百 ns 内核态 设备和内存 你的进程 read / write:读写文件 send / recv:收发网络数据 mmap:把文件映射进地址空间 每一个都是一次系统调用 进程地图 用户缓冲区 buf 你自己分配的内存:栈上、堆上 read / recv:内核把数据拷进来 write / send:内核从这里拷走 进程地图:栈、堆 mmap 映射区 页缓存里的页直接映射进来 读写就是普通的访存,不拷贝 第一次碰一页要缺页 进程地图:mmap 区 IO 模型:等数据的时候,线程在干什么 阻塞:数据没好,线程睡在 read / recv 里 非阻塞:数据没好,立刻返回 EAGAIN,过会儿再试 IO 多路复用(select / poll / epoll):一个线程等很多 fd,读还是自己读 异步(io_uring):把“读到 buf 里”交给内核,读完了再通知你 前三种都是同步 IO:把数据拷进 buf 这一步,是你的线程在等 epoll:红黑树、就绪链表和回调 VFS(虚拟文件系统) 按 fd 找到打开的文件 再交给具体的文件系统(ext4、xfs……) 页缓存(page cache) 读过、写过的文件内容,按页缓存在内存里 read 命中就直接拷,不碰磁盘 write 写到这里、标成脏页就返回 free 命令里的 buff/cache 主要就是它 回写线程(writeback) 脏页过一会写回磁盘 默认脏了约 30 s 必写,每 5 s 查一次 fsync:等这个文件写完才返回 块设备层 + 磁盘驱动 把要读写的页组成请求,排队、合并 交给 NVMe 驱动,发给磁盘 socket 缓冲区 接收缓冲:收到了、还没被 recv 的数据 发送缓冲:send 拷进来、对方还没确认的 满了:send 阻塞,或返回 EAGAIN epoll:就绪通知 socket 收到数据,回调把它挂上 epoll 的就绪链表, 再叫醒睡在 epoll_wait 里的线程 只通知“可以读了”,数据还得你自己 recv epoll:红黑树、就绪链表和回调 TCP/IP 协议栈 收:校验、去包头,按 IP 和端口找到 socket 发:分段、等确认、丢了重传 网卡驱动 + 收发环 收:软中断(NAPI)一次取一批包 发:把包挂到发送环,通知网卡 零拷贝:把文件发到网络上 read + write: 磁盘 → 页缓存(DMA)→ buf(CPU)→ socket 缓冲(CPU)→ 网卡(DMA) 4 次拷贝,4 次用户态、内核态切换 mmap + write:页缓存直接映射进来,省掉拷进 buf 那一次 sendfile:数据不出内核,页缓存 → 网卡 网卡支持分散收集 DMA 时,CPU 一次都不拷,只剩 2 次 DMA、2 次切换 splice:在两个 fd 之间搬,中间经过一个管道,也不拷进用户态 磁盘(NVMe SSD) 一次随机读约 10–100 µs 机械硬盘要几 ms 做完用 DMA 写进内存,再发中断 硬件地图:外部设备 内存(DRAM) 页缓存、socket 缓冲、收发环 其实都是内存里的页 DMA:设备直接读写内存,不占 CPU 硬件地图:内存控制器 → 通道 网卡(NIC) 收:DMA 写进收包环,再发中断 发:DMA 从发送环读出来发走 同一机房往返约几十 µs 硬件地图:网卡 → 中断 → 某个核 read / write send / recv CPU 拷贝 CPU 拷贝 映射,不拷贝 读写请求 DMA DMA 中断 回调 查 没命中 图随文章更新:讲到 io_uring、管道和信号时再往上补。