漫谈五种网络IO模型

1. 举个通俗的栗子

  1. 阻塞IO, : 给女神发一条短信, 说我来找你了, 然后就默默的一直等着女神下楼, 这个期间除了等待,你不会做其他事情, 属于备胎做法.
  2. 非阻塞IO:给女神发短信, 如果不回, 接着再发, 一直发到女神下楼, 这个期间,你除了发短信等待不会做其他事情, 属于专一做法.
  3. IO多路复用: 是找一个宿管大妈来帮你监视下楼的女生, 这个期间你可以些其他的事情. 例如可以顺便看看其他妹子,玩玩王者荣耀, 上个厕所等等. IO复用又包括 select, poll, epoll 模式. 那么它们的区别是什么?
    • select大妈 每一个女生下楼, select大妈都不知道这个是不是你的女神, 她需要一个一个询问, 并且select大妈能力还有限, 最多一次帮你监视1024个妹子
    • poll大妈不限制盯着女生的数量, 只要是经过宿舍楼门口的女生, 都会帮你去问是不是你女神
    • epoll大妈不限制盯着女生的数量, 并且也不需要一个一个去问. 那么如何做呢? epoll大妈会为每个进宿舍楼的女生脸上贴上一个大字条,上面写上女生自己的名字, 只要女生下楼了, epoll大妈就知道这个是不是你女神了, 然后大妈再通知你.
  4. 上面这些同步IO有一个共同点就是, 当女神走出宿舍门口的时候, 你已经站在宿舍门口等着女神的, 此时你属于阻塞状态
  5. 接下来是异步IO的情况
    你告诉女神我来了, 然后你就去王者荣耀了, 一直到女神下楼了, 发现找不见你了, 女神再给你打电话通知你, 说我下楼了, 你在哪呢? 这时候你才来到宿舍门口. 此时属于逆袭做法

2. Block I/O模型(阻塞I/O)

阻塞I/O模型示意图:

以Read为例:

  1. 最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
  2. 当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。
  3. 当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态.

典型的阻塞IO模型的例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在read方法.

😄 也就是说,内核准备数据数据从内核拷贝到进程内存地址这两个过程都是阻塞的。

3. Non-Block(非阻塞I/O模型)

可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

  1. 当用户进程发出read操作时,如果kernel中的数据还没有准备好;
  2. 那么它并不会block用户进程,而是立刻返回一个error,从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果;
  3. 用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call;
  4. 那么它马上就将数据拷贝到了用户内存,然后返回。

😄所以,nonblocking IO的特点是用户进程内核准备数据的阶段需要不断的主动询问数据好了没有

4. Multiplexing I/O (多路复用)

I/O多路复用实际上就是用select, poll, epoll监听多个io对象,当io对象有变化(有数据)的时候就通知用户进程。好处就是单个进程可以处理多个socket。当然具体区别我们后面再讨论,现在先来看下I/O多路复用的流程:

  1. 当用户进程调用了select,那么整个进程会被block;
  2. 而同时,kernel会“监视”所有select负责的socket;
  3. 当任何一个socket中的数据准备好了,select就会返回;
  4. 这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回

这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用多线程 + 阻塞 IO的web server性能更好,可能延迟还更大。

select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

5. Signal-driven I/O(信号驱动)

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

6. Asynchronous I/O(异步 I/O)

真正的异步I/O很牛逼,流程大概如下:

  1. 用户进程发起read操作之后,立刻就可以开始去做其它的事。
  2. 而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。
  3. 然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

6. 小结

6.1 blocking和non-blocking的区别

调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。

6.2 同步IO和异步 IO的区别

阻塞、非阻塞、IO复用、信号驱动都是同步IO模型。因为在发起操作数据的系统调用(如本文的read())过程中是被阻塞的。这里要注意,虽然在加载数据到kernel buffer的数据准备过程中可能阻塞、可能不阻塞,但kernel buffer才是read()函数的操作对象,同步的意思是让kernel buffer和app buffer数据同步。显然,在保持kernel buffer和app buffer同步的过程中,进程必须被阻塞,否则read()就变成异步的read()。

只有异步IO模型才是异步的,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

6.3 非阻塞 IO和异步 IO的区别

可以发现non-blocking IO和asynchronous IO的区别还是很明显的。

  • 非阻塞IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存
  • 异步IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。