五种IO模型I/O(输入/输出) 全名为 input/output, 比如我们平时在访问外设(比如网卡)时, 在进行 I/O 操作(如 recv 或 scanf )时, 大部分时间可能会花在等待数据到达或等待 I/O 设备就绪,而真正的数据传输(拷贝)只占用其中的一小部分时间,所以IO的本质是等待 + 拷贝我们常说IO效率比较低, 但是什么叫高效IO?IO中无论是从外设-内存, 还是内存-内存(用户缓冲区-内核缓冲区), IO工作本质都是数据拷贝.如果我们每时每刻都在拷贝, 那IO就是效率最高的, 因此高效IO的高效就在于减少单位时间内IO等待的比重,提高IO效率就需要减少单位时间内IO等待的比重.拿钓鱼这件事举例, 钓鱼的过程可以简化为: 等待 + 钓鱼我们假设鱼塘是OS, 鱼是数据, 鱼漂是就绪条件, 钓鱼者是进程(准确来说是接口), 现在有这样五个人在钓鱼:张三: 永远盯着鱼漂, 谁叫我都不理李四: 看一次鱼漂, 如果没有鱼那我就去看书或者看手机; 等我下一次看鱼漂, 如果上钩了我就收杆.王五: 铃铛绑在了鱼竿的顶部, 我还是去看手机, 但是我不会再去看鱼漂, 直到等着铃铛响了我才去收杆.赵六: 我还是在永远盯着鱼漂, 但是我这时同时掌管100个鱼竿, 只要至少有一个鱼竿有动静, 我就钓.田七: 我其实就是想吃鱼, 我不钓鱼, 我让助理去钓鱼, 助理钓完鱼后告诉我一声叫我去拿就行, 我坐享其成.线程(钓鱼者)调用recv()后,会一直等待数据(盯着鱼漂),直到数据到达(鱼漂动了),才会继续执行。线程不会一直等待数据,而是调用recv()发现没数据后就做其他事(看看书、玩手机),然后过一会儿再检查数据是否到达。这五个例子对应了五种IO模型, 先笼统认识一下:阻塞IO: 在内核将数据准备好之前,系统调用会一直等待. 所有的套接字,默认都是阻塞方式非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).其中还有一个假设鱼竿, 它其实指的是文件描述符, 为什么?进程调用 I/O 操作的本质都是在等待文件描述符(FD, File Descriptor)变得可用,无论是socket、文件、管道、设备等.1. 阻塞式IO阻塞式IO是我们IO模型中最常见的IO模型, 没有之一.比如网络通信中, 以 recv() 为例:1. 用户线程调用 recv() ,通过系统调用进入内核态.内核根据 fd 找到对应的 socket 结构体,检查其中 socket 接收缓冲区:1. 如果接收缓冲区有数据,直接把数据从内核缓冲区拷贝到用户进程缓冲区,随后返回.2. 这里更重要的是, 如果接收缓冲区没有数据可读, 阻塞式IO下,线程进入阻塞态,OS 将其PCB 挂起到该 socket 的等待队列(下图线程A),并让出 CPU, 此时调度器选择其他可运行线程执行.用户进程 │ │ recv(5, buf, 1024, 0) ▼ 进程的文件描述符表 │ └── fd = 5 │ ▼ struct file │ ▼ struct socket │ ├── wq ← Socket 关联的等待队列 │ │ │ └── 线程 A(等待接收数据) │ └── sk → struct sock │ ├── sk_receive_queue - Socket 接收缓冲区(逻辑概念) │ │ │ └── 接收队列(实际数据结构) │ │ │ └── 此时无数据包 │ └── sk_wq │ └── 关联上述等待队列2. 等待数据到达:当对端发送数据到这个 socket 后, 网卡接收网络数据包, OS通过网卡驱动和网络协议栈处理数据,并将数据交给对应 Socket 的内核接收缓冲区.网卡-接收缓冲区简略的过程可能是网卡发送中断信号给OS, OS调用网卡驱动程序将数据从网卡(外设)拷贝到内核接收缓冲区(内存)随后 OS 发现有线程A在该 socket (本质是对应的FD) 的阻塞队列中等待数据, 于是准备唤醒一个(或多个)阻塞的进程.3. 进程被唤醒, 状态从 “阻塞” 改为 “就绪”OS唤醒线程: 将线程PCB中状态由阻塞态改为就绪态,允许调度器重新选择它执行.线程等待 CPU 调度执行,调度器决定何时真正运行这个线程.4. 进程恢复执行当 CPU 调度该线程后, 它从先前阻塞的内核执行位置继续运行, 继续执行recv(),重新检查读取条件, 条件满足, 数据从 SEO 优化官网定制响应式建站教育培训建站