IO 模型是网络编程的一个基本话题, 了解IO模型有助于深入学习Nginx、libevent、libev 等诸多优秀的项目,IO模型的发展从最初的单进程顺序处理到 fork 子进程处理再到多线程处理, 后来出现了诸如 select/poll/epoll/kqueue 等多种 IO 复用机制,IO模型的发展从从广义上讲, IO 模型并不仅仅限于网络 IO, 对于磁盘 IO 来说原理都是相同的, 而且该模型对 Linux/Darwin/FreeBSD 等都是通用的。
Blocking IO (阻塞式 IO)
所谓阻塞IO就是当应用程序发起读取数据申请时,在内核数据没有准备好之前,应用程序会一直处于等待数据状态,直到内核把数据准备好提交给应用程序整个过程才算结束。
我们以UDP协议为例, UDP 提供面向 Packet 的无连接、不可靠的服务, 一个典型的 UDP 通信过程是这样的:
- 服务端调用 socket() 创建套接字, 调用 bind() 绑定套接字相关的配置信息(如 IP、Port 等)
- 服务端调用 recvfrom() 等待客户端发来的 Packet, 若没有 Packet 到来, 则 recvfrom() 将会持续阻塞
- 客户端调用 socket() 创建客户端套接字
- 客户端调用 sendto() 将数据以 Packet 的形式发送出去, 然后调用 recvfrom() 等待服务端响应, 若没有 Packet 到来, 则 recvfrom 将会持续阻塞.
- 服务端收到客户端发来的 Packet, 执行一定的逻辑, 调用 sendto() 将回复内容发回给客户端
上述的 4 ~ 5 可能会持续多轮, 当客户端无数据要再发往服务端后, 便调用 close() 关闭套接字
这里我们着重关注 recvfrom() 函数, 无论是对于客户端还是服务端来说, 当调用 recvfrom() 函数后都会进入阻塞状态, 直到有新的数据准备就绪才可以读取, 这种方式便是最简单的 IO 模型, 称之为 Blocking IO, 默认情况下, 所有 socket 读写都是阻塞的, Blocking IO 的流程图如下所示:

Non-Blocking IO (非阻塞式 IO)
所谓非阻塞IO就是当应用程序发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用程序,不会要求应用程序在那里等待。
Non-Blocking IO 的非阻塞体现在 recvfrom() 调用上, 若内核当前未准备好数据则 recvfrom() 将会立即返回 EWOULDBLOCK 错误, 这是在 errno.h 中定义的一个宏, 即当内核数据未就绪时, 应用程序不会因 recvfrom() 调用而阻塞, 应用程序可以根据 recvfrom() 的执行结果判断是否继续轮询, 直到内核的数据准备就绪后, 应用程序再次调用 recvfrom() 将会阻塞, 待数据从内核空间拷贝到用户空间后重新转换为就绪态, Non-Blocking IO 的流程图如下所示:

简要总结:非阻塞IO是在应用程序在调用recvfrom读取数据时,如果该缓冲区没有数据,内核就会直接返回一个EWOULDBLOCK错误,不会让应用程序处在一个一直等待的状态。在应用程序收到EWOULDBLOCK错误后,如果应用程序想要继续读取数据,就需要不断的调用recvfrom请求,直到它读取到所需要的数据。
IO Multiplexing (IO 复用)
IO 复用适用于多并发场景,典型的例子便是 Web Server, 它们在同一时刻可以处理大量的客户端请求,
Unix系统把所有的网络请求以一个fd文件描述符进行标识,一个进程会同时监听多个套接字对象(),当所监听的套接字集合中至少一个准备就绪,就会调用线程的 recvfrom() 去读取数据,这么做的好处就是可以节省出大量的线程资源出来。
IO Multiplexing 有多个实现方法, 如 select/poll/epoll/kqueue函数,具体的流程图如下:
观察上面的图示可以看到, 对于 IO Multiplexing 来说, 两次系统调用都是阻塞的, 与 Blocking-IO 相比, IO Multiplexing 在整个 IO 过程中发起了两次系统调用, 所以如果只操作一个套接字对象的话, IO Multiplexing 的效率甚至比不上 Blocking IO, 而 IO Multiplexing 的优势在于同时处理多个套接字对象
简要总结:进程通过将一个或多个fd传递给select,阻塞在select操作上,select帮我们侦测多个fd是否准备就绪,当有fd准备就绪时,select返回数据可读状态,应用程序再调用recvfrom读取数据。
Signal Driven IO (信号驱动 IO)
IO复用模型解决了一个线程可以监控多个fd的问题,但是select函数采用不断轮询fd的可读状态来判断是否有可读数据,其中一些轮询是无效的,所以这一部分是需要优化的。
信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个fd,Signal Driven IO 的流程图:

简要总结:首先应用程序注册SIGIO信号监听,并通过系统调用sigaction执行一个信号处理函数,此时请求即刻返回,当数据准备就绪时,内核生成对应进程的SIGIO信号,通过信号回调通知应用程序线程调用recvfrom来读取数据。
Async IO (异步 IO)
不管是IO复用还是信号驱动,我们要读取一个数据总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。以上四种 IO 模型,无论是哪一种, IO 操作中都会有阻塞产生, 尤其对于数据从内核空间和用户空间拷贝的时候。对于异步 IO 来说, 应用程序只要通知内核要需要读取的套接字对象, 以及数据的接收地址, 则整个过程都是由内核独立来完成的, 包括数据从内核空间向用户空间的拷贝, 等所有操作都完成之后,内核会发起一个通知告诉应用,异步 IO 的流程图如下所示:

简要总结: 异步IO模型与信号驱动模型的主要区别在于,信号驱动IO需要内核通知应用程序可以开始下一个IO操作,内核空间拷贝数据到用户空间是由应用进程调用 recvfrom() 来实现的。而异步IO模型中数据拷贝到户空间的一切都是由内核独立完成,最后内核再通知应用程序请求的处理结果。
总结
阻塞就是应用程序向内核发起数据读取请求的时,当内核数据还没准备就绪时,看答复是即刻返回,还是需要等待,如果需要等待的话就是阻塞,反之非阻塞。
同步就是应用程序从发起请求到数据拷贝的整个过程都需要自己参与;反之,如果应用程序发送完请求指令后就不再参与其他过程,只需要等待最终完成结果的通知,那么就属于异步。
参考链接:
100%弄明白5种IO模型
Unix IO 模型解析
本文详细解析了五种IO模型:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO。通过具体例子阐述了每种模型的工作原理,重点介绍了它们在处理网络请求和数据传输时的差异,帮助读者深入理解IO模型在Nginx等项目中的应用。
4万+

被折叠的 条评论
为什么被折叠?



