Linux 高级 IO:五种 IO 模型与非阻塞实现
一、前置知识
在正式学习高级 IO 之前,需要打好基础。基础 IO 包括文件接口、文件系统调用(open, write, close)、文件 fd 等概念。
IO 对应的系统调用可以是 read 和 write。read 将数据从内核缓冲区拷贝到用户层,write 将数据从用户层拷贝到内核缓冲区。如果 tcp 接收缓冲区中没有数据,read 会阻塞;如果 tcp 发送缓冲区满了,write 会阻塞。因此 IO = 等待 + 拷贝。高效的 IO 意味着单位时间内等待的比重越小。
网络通信的本质也是 IO,输入对应 read,输出对应 write。
二、五种 IO 模型
IO 分为五种模型,可以通过钓鱼的例子来理解:
- 阻塞式 IO:张三盯着鱼漂,直到浮动了才拉杆。进程在系统调用中一直等待,直到数据准备好。
- 非阻塞式 IO:李四每隔一会看一眼鱼漂,没动静就看书。进程轮询检查状态,不满足条件立即返回。
- 信号驱动式 IO:王五带铃铛,响了才拉杆。内核通过 SIGIO 信号通知应用程序。
- 多路转接(多路复用):赵六用 100 根鱼竿,遍历检查。可以并发等待多个文件描述符,select 专门用于此。
- 异步 IO:田七让司机小王去钓鱼,钓好了再通知。操作系统负责等待和拷贝,完成后通知应用。
同步 IO(前四种)都需要参与 IO 过程(等待),异步 IO 不参与等待过程,只发起请求并获取结果。
其中多路复用效率最高,因为可以让等待的时间重合,减少等待比重。
三、深入认识五种 IO 模型
阻塞 IO
进程运行,用户在应用层调用 recvfrom 从内核的 tcp 接收缓冲区中读数据。如果此时 tcp 接收缓冲区中没有数据,就会阻塞在系统调用中,进程被挂起。当内核准备好数据时,进程被唤醒,recvfrom 将数据拷贝到用户层缓冲区后返回。
阻塞 IO:在内核将数据准备好之前,系统调用会一直等待,所有套接字默认都是阻塞方式。

非阻塞 IO
通常默认系统调用接口是阻塞式调用。如果想要以非阻塞方式 IO,需要进行设置,最常用的是使用 fcntl 将文件描述符设置为非阻塞。
通过参数以非阻塞的形式调用 recvfrom,搭配循环,就可以实现非阻塞 IO。内核中的数据没有准备好,recvfrom 直接返回,错误码 errno 设置为 EWOULDBLOCK。返回上层后,可以去做其它的事情,然后再继续以非阻塞的形式调用 recvfrom,以此循环。
非阻塞 IO 需要程序员以循环的方式反复尝试读取文件描述符,这个过程称为轮询,非阻塞 IO 也称为非阻塞轮询。

信号驱动 IO
进程先调用 sigaction 当进程收到 SIGIO 信号的时候执行信号处理函数,信号处理函数中应该设置调用 recvfrom 将数据读取上来。当内核的 tcp 接收缓冲区有数据了,内核就会给进程发送 SIGIO 信号。
信号驱动 IO,当内核将数据准备好的时候,通过 SIGIO 信号通知应用程序进行 IO 操作。





