前言:

每次谈到IO模型,都会牵扯到同步、异步、阻塞、非阻塞这几个词。从词的表面上看,很多人都觉得很容易理解。但是细细一想,却总会发现有点摸不着头脑。下文就结合具体的实例来讲解一下经典的几个IO模型。

  • IO模型分类:内存IO、磁盘IO、网络IO等,通常的IO指的是后两者

  • 阻塞和非阻塞是函数的实现方式,即在数据就绪之前是立即返回还是等待,就是发起的IO是否被阻塞

  • 以文件IO为例,一个IO读过程是文件数据从磁盘→内核缓冲区→用户内存的过程。同步与异步的区别主要在于数据从内核缓冲区→用户内存这个过程需不需要用户进程等待,即实际的IO读写是否阻塞请求进程。(网络IO把磁盘换做网卡即可)

IO模型

同步阻塞

如果数据没有准备就绪,就一直等待,直到数据准备就绪。

去麦当劳吃饭,点了一个自己最喜欢的套餐,然后一直在排队等待处等到套餐做好,自己端回到餐桌上就餐。这就是典型的同步阻塞。当厨师给你做饭的时候,你需要一直在那里等着。

2019-8-23-0

在网络编程中,读取客户端的数据需要调用recvfrom。在默认情况下,这个调用会一直阻塞直到数据接收完毕,就是一个同步阻塞的IO方式。这也是最简单的IO模型,在通常fd较少、就绪很快的情况下使用是没有问题的。

同步非阻塞

如果没有准备就绪会返回一个标志信息,不需要等待,等到数据准备就绪以后,内核会把数据拷贝到线程中去。但是需要不断询问内核是否已经准备好数据,非阻塞虽然不用等待但是一直占用CPU。

还是在麦当劳点餐,你每次点完饭就在那里等着,突然有一天你发现自己so stupid 。于是,你点完之后,就回桌子那里坐着,然后估计差不多了,就问服务员💁‍♂️饭好了没,如果好了就去端,没好的话就等一会再去问,循环直到饭做好,这就是同步非阻塞。

这种方式在编程中对socket设置O_NONBLOCK即可。但此方式仅仅针对网络IO有效,对磁盘IO并没有作用。因为本地文件IO就没有被认为是阻塞,我们所说的网络IO的阻塞是因为网路IO有无限阻塞的可能,而本地文件除非是被锁住,否则是不可能无限阻塞的,因此只有锁这种情况下,O_NONBLOCK才会有作用。而且,磁盘IO时要么数据在内核缓冲区中直接可以返回,要么需要调用物理设备去读取,这时候进程的其他工作都需要等待。因此,后续的IO复用和信号驱动IO对文件IO也是没有意义的。

2019-8-23-0

IO多路复用模型

多路复用IO,会有一个线程不断地去轮询多个socket的状态,当socket有读写事件的时候才会调用IO 读写操作。

用一个线程管理多个socket,是通过selector.select()查询每个通道是否有事件到达,如果没有事件到达,则会一直阻塞在那里,因此也会带来线程阻塞问题。

还是在麦当劳点餐,你点一份饭然后循环的去问好没好显然有点得不偿失,倒还不如就等在那里直到准备好;但是当你点了好几样饭菜的时候,你每次都去问一下所有饭菜的状态(未做好/已做好)肯定比你每次阻塞在那里等着好多了。当然,你问的时候是需要阻塞的,一直到有准备好的饭菜或者你等的不耐烦(超时)。这就引出了IO复用,也叫多路IO就绪通知。这是一种进程预先告知内核的能力,让内核发现进程指定的一个或多个IO条件就绪了,就通知进程。使得一个进程能在一连串的事件上等待。

可以打个比方:如果我们先前创建的几个进程承载不了目前快速发展的业务的话,是不是还得增加进程数?我们都知道系统创建进程是需要消耗大量资源的,所以这样就会导致系统资源不足的情况。

那么有没有一种方式可以让一个进程同时为多个客户端端提供服务?

select模型:

说的通俗一点就是各个客户端连接的文件描述符也就是套接字,都被放到了一个集合中,调用select函数之后会一直监视这些文件描述符中有哪些可读,如果有可读的描述符那么我们的工作进程就去读取资源。PHP 中有内置的函数来完成 select 系统调用。

poll模型:

poll 和 select 的实现非常类似,本质上的区别就是存放 fd 集合的数据结构不一样。select 在一个进程内可以维持最多 1024 个连接,poll 在此基础上做了加强,可以维持任意数量的连接。
但 select 和 poll 方式有一个很大的问题就是,我们不难看出来 select 是通过轮训的方式来查找是否可读或者可写,打个比方,如果同时有100万个连接都没有断开,而只有一个客户端发送了数据,所以这里它还是需要循环这么多次,造成资源浪费。
所以后来出现了 epoll系统调用。

epoll模型:

epoll 是 select 和 poll 的增强版,epoll 同 poll 一样,文件描述符数量无限制。
epoll是基于内核的反射机制,在有活跃的 socket 时,系统会调用我们提前设置的回调函数。而 poll 和 select 都是遍历。

但是也并不是所有情况下 epoll 都比 select/poll 好,比如在如下场景:
在大多数客户端都很活跃的情况下,系统会把所有的回调函数都唤醒,所以会导致负载较高。既然要处理这么多的连接,那倒不如 select 遍历简单有效。

2019-8-23-0

信号驱动模型

在信号驱动IO模型中,当用户发起一个IO请求操作时,会给对应的socket注册一个信号函数,线程会继续执行;当数据准备就绪的时候会给线程发送一个信号,线程接受到信号时,会在信号函数中进行IO操作。非阻塞IO、多路复用IO、信号驱动IO都不会造成IO操作的第一步,查看数据是否准备就绪而带来的线程阻塞,但是在第二步,对数据进行拷贝都会使线程阻塞。

上文的麦当劳就餐情形,还是需要你每次都去问一下饭菜状况。于是,你再次不耐烦了,就跟老板说,哪个饭菜好了就通知我一声吧。然后就自己坐在桌子那里干自己的事情。或者,你可以把手机号留给老板,自己出门,等饭菜好了直接发条短信给你。这就类似信号驱动的IO模型。

2019-8-23-0

异步非阻塞

异步IO是最理想的IO模型,当线程发出一个IO请求操作时,接着就去做自己的事情了,内核去查看数据是否准备就绪和准备就绪后对数据的拷贝,拷贝完以后内核会给线程发送一个通知说整个IO操作已经完成了,数据可以直接使用了。

之前的就餐方式,到最后总是需要你自己去把饭菜端到餐桌。这下你也觉得不耐烦了,于是就告诉老板,能不能饭好了直接端到你的面前或者送到你的家里(外卖)。这就是异步非阻塞IO了。

2019-8-23-0

异步IO和信号驱动的主要区别,在于:信号驱动由内核告诉我们何时可以开始一个IO操作(数据在内核缓冲区中),而异步IO则由内核通知IO操作何时已经完成(数据已经在用户空间中)。