Linux五种I/O模型
什么是I/OI/O Input/Output输入/输出。本质上就是数据的流动输入数据从外部设备 → 进入内存比如读文件、收网络包、键盘输入输出数据从内存 → 到外部设备比如写文件、发网络包、屏幕显示常见的I/O设备设备输入输出磁盘读文件写文件网卡接收数据包发送数据包键盘按键输入-显示器-显示内容鼠标点击/移动-I/O在计算机中的位置┌─────────────────────────────────────┐ │ 应用程序 │ │ (用户空间) │ └─────────────┬───────────────────────┘ │ 系统调用 ↓ ┌─────────────────────────────────────┐ │ 操作系统内核 │ │ (内核空间) │ │ ┌─────────────────────────┐ │ │ │ I/O子系统 │ │ │ │ (文件系统、网络协议栈) │ │ │ └──────────┬──────────────┘ │ └───────────────┼─────────────────────┘ │ 设备驱动 ↓ ┌─────────────────────────────────────┐ │ 硬件设备 │ │ (磁盘、网卡、显卡...) │ └─────────────────────────────────────┘为什么I/O慢I/O是计算机中最慢的操作原因操作速度量级对比CPU指令纳秒级内存访问~100nsSSD读取~100μs机械硬盘~10ms网络传输1-100msCPU太快I/O太慢差距在10⁵-10⁸倍所以I/O模型的核心问题就是怎么让CPU不等I/OI/O的两个核心动作对于一次读操作1. 等待数据准备好磁盘/网卡 → 内核缓冲区2. 拷贝数据内核缓冲区 → 用户缓冲区应用程序内存所有I/O模型的差异都在于如何处理这两个阶段。内核态 vs 用户态这是理解I/O的关键用户态应用程序运行的权限级别不能直接访问硬件内核态操作系统运行的权限级别可以访问一切一次I/O必定涉及用户态↔内核态切换切换开销保存/恢复寄存器切换页表刷新缓存频繁的系统调用是性能杀手缓冲区的作用内核缓冲区操作系统维护的缓冲区用户缓冲区应用程序自己的缓冲区写入流程用户缓冲区 → 内核缓冲区 → 磁盘/网卡读取流程磁盘/网卡 → 内核缓冲区 → 用户缓冲区为什么有缓冲区减少实际I/O次数合并小请求解耦速度差异生产者-消费者模式零拷贝技术传统I/O需要多次拷贝磁盘 → 内核缓冲区 → 用户缓冲区 → 内核缓冲区 → 网卡 (DMA拷贝) (CPU拷贝) (CPU拷贝) (DMA拷贝)零拷贝技术sendfile、mmap可以减少拷贝次数磁盘 → 内核缓冲区 → 网卡 (DMA拷贝) (DMA拷贝)应用场景Kafka、Nginx等高性能服务器概念核心理解I/O本质数据在内存与外部设备间流动I/O之痛速度慢CPU要等I/O模型解决怎么等的问题核心矛盾CPU太快I/O太慢优化方向减少等待、减少拷贝、减少切换Linux的五种I/O模型是理解网络编程和高并发服务器的核心。对于一次I/O操作比如read会经历两个阶段等待数据准备数据从网卡/磁盘读取到内核缓冲区将数据从内核拷贝到用户空间从内核缓冲区拷贝到应用程序缓冲区五种I/O模型的区别主要在这两个阶段如何处理。1. 阻塞I/OBlocking I/O最简单的模型默认行为。应用进程调用recvfrom ↓ 阻塞等待... ↓ 数据准备好 ↓ 内核拷贝数据到用户空间 ↓ 返回成功特点调用后进程挂起直到数据准备好并拷贝完成简单直观代码容易理解缺点一个线程只能处理一个连接效率极低典型场景简单的客户端程序2. 非阻塞I/ONon-blocking I/O不断轮询不等待。应用进程调用recvfrom ↓ 数据未准备好 → 立即返回EAGAIN错误 ↓ 再次调用recvfrom ↓ 再次返回EAGAIN... ↓ 循环轮询 ↓ 数据准备好了 → 拷贝数据 → 返回成功特点设置socket为NONBLOCK调用立即返回数据未准备好返回错误EAGAIN/EWOULDBLOCK缺点CPU空转严重轮询消耗大量资源典型场景很少单独使用通常配合I/O多路复用3. I/O多路复用用select/poll/epoll同时监控多个连接多个的进程的IO可以注册到一个复用器(select)上然后用一个进程调用该select,select会监听所有注册进来的IO。如果select监听的IO在内核缓冲区都没有可读数据select调用进程会被阻塞而当任一IO在内核缓冲区中有可读数据时select调用就会返回而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO读取内核中准备好的数据。Linux中IO复用的实现方式主要有SelectPoll和EpollSelect注册IO、阻塞扫描监听的IO最大连接数不能多于FD_ SIZE1024。Poll原理和Select相似没有数量限制但IO数量大扫描线性性能下降。Epoll 事件驱动不阻塞mmap实现内核与用户空间的消息传递数量很大Linux2.6后内核支持。应用进程调用select/epoll_wait ↓ 阻塞等待任意一个socket可读 ↓ 有socket可读了 ↓ 返回就绪的socket列表 ↓ 对就绪socket调用recvfrom ↓ 内核拷贝数据到用户空间 ↓ 返回成功特点一个线程可以监控多个文件描述符select/poll/epoll是系统调用本身也会阻塞优点单线程处理大量连接性能好缺点需要两次系统调用select recvfrom三种实现对比select有fd数量限制1024O(n)遍历poll无数量限制仍需O(n)遍历epoll事件驱动只返回就绪的fdO(1)典型场景Nginx、Redis、Netty等高性能服务器4. 信号驱动I/OSignal-driven I/O数据准备好时发信号通知。应用进程建立SIGIO信号处理函数 ↓ 调用sigaction注册信号 ↓ 进程继续执行其他任务... ↓ 数据准备好 → 内核发送SIGIO信号 ↓ 信号处理函数中调用recvfrom ↓ 内核拷贝数据到用户空间特点等待数据阶段不阻塞数据准备好后通过信号通知缺点信号处理复杂TCP场景信号过多实际很少使用典型场景UDP应用较多TCP很少用5. 异步I/OAsynchronous I/O真正的异步两个阶段都不阻塞。应用进程调用aio_read ↓ 立即返回进程继续执行 ↓ 同时内核等待数据 拷贝数据 ↓ 全部完成后通知应用进程特点两个阶段都不阻塞应用进程只需发起请求完成后会被通知优点真正的异步性能最优缺点Linux的aio支持长期不完善编程模型复杂典型场景Windows的IOCP用得比较多Linux上用较少五种模型对比模型第一阶段等待数据第二阶段拷贝数据特点阻塞I/O阻塞阻塞简单但低效非阻塞I/O非阻塞轮询阻塞CPU空转I/O多路复用阻塞在select上阻塞高并发首选信号驱动I/O非阻塞信号通知阻塞TCP下较少用异步I/O非阻塞非阻塞真正异步同步 vs 异步同步I/O导致请求进程阻塞直到I/O操作完成前四种都是同步异步I/O不导致请求进程阻塞只有AIO是真正的异步实际开发中I/O多路复用epoll是Linux高性能服务器的标准选择Redis、Nginx都用这个。阻塞I/O适合简单场景异步I/O理论上最优但Linux支持不够成熟。