unix 网络编程中的五种IO模型

  • Blocking IO 阻塞IO
  • NoneBlocking IO 非阻塞IO
  • IO multiplexing IO多路复用
  • signal driven IO 信号驱动IO (不常用)
  • asynchronous IO 异步IO

IO发生时设计到的对象和步骤,对于一个network IO,他会设计到两个系统对象:

  • application 调用这个IO的进程
  • kernel 系统内核

IO交互过程是:

阶段1, wait for data 等待数据准备
阶段2 copy data from kernel to user 将数据从内核拷贝到用户进程中

只所以会有同步、异步、阻塞和非阻塞 这几种说法就是根据程序再这两个阶段的处理方式不同儿产生的。了解这些背景之后,我们就分别针对四种IO模型进行讲解

Blocking IO— 阻塞IO

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概如下图:

阻塞、非阻塞、同步、异步 - 图1
当用户进程调用了recvfrom 这个系统调用,kernel就开始了IO的第一个阶段:准备数据。 对于network IO来说,很多时候数据在一开始还没有到达(比如, 还没有收到一个完整的UDP包), 这个时候kernel就要等待足够的数据到来。 而用户进程这边,整个进程会被阻塞。当kernel一直等这数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel 返回结果,用户进程才解除block的状态,重新运行起来。
所以, blocking IO的特点就是在IO执行的两个阶段都被block了

NoneBlockingIO — 非阻塞IO

linux下,可以通过设置socket使其变为non-blocking。 当对一个non-blocking socket 执行读操作时,流程时这个样子
阻塞、非阻塞、同步、异步 - 图2
从图中可以看出,当用户进程发出recvfrom 这个系统调用后,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个结果(no datagram ready)。 从用户进程角度讲,它发起一个操作后,并没有等待,而是马上就得到了一个结果。 用户进程得知数据还没有准备好,它可以每隔一段时间再次发送recvfrom操作。 一旦kernel中的数据准备好了,并且又再次收到了用户的system call,那么它马上就将数据拷贝到用户内存,然后返回。
所以,用户进程其实时需要不断的主动询问kernel数据好了没有。

IO multiplexing —- IO 多路复用

IO多路复用时网络编程中最常用的模型,像我们最常用的select、epoll都属于这种模型。 以select为例

阻塞、非阻塞、同步、异步 - 图3
看起来它和Blocking IO 很相似,两个都阻塞,但它与blocking IO的一个重要区别就是它可以等待多个数据报就绪(datagram ready), 即可以处理多个连接。 这里的select相当于一个代理,调用select以后进程会被select阻塞,这个时候在内核空间内select会监听指定的多个datagram(如socket连接),如果其中任意一个数据就绪了就返回,此时程序再进行数据读取操作,将数据拷贝至当前进程内。由于select 可以监听多个socket,我们可以用它来处理多个连接。

select模型

在select 模型中,每个socket 一般都设置成non-blocking,虽然等待数据节点仍然时阻塞状态,但是它时被select调用阻塞的,而不是直接被IO阻塞的。select 底层通过轮询机制来判断每个socket读写是否就绪。当然select也有一些确定,比如底层轮询机制会增加开销,支持的文件描述符量过少等。

epoll 模型

1, 每次新建IO句柄(epoll_create) 才复制并注册(epoll_ctl)到内核
2, 内核根据IO事件,把准备好的IO句柄放到就绪队列
3, 应用只要轮询(epoll_wait)就绪队列,然后去读取数据
只需要轮询就绪队列,不存在select的轮询,也没有内核的轮询,不需要多次复制所有的IO句柄。因此可以同事支持的IO句柄数轻松过百万。

asynchronous IO — 异步IO

异步IO在网络编程中几乎用不到, 在File IO中可能会用到:

阻塞、非阻塞、同步、异步 - 图4
读取模型跟阻塞IO有些不同,读取操作aio_read 会通知内核进行读取操作并将数据拷贝值进程中,完事后通知进程整个操作全部完成(绑定一个回调函数处理数据)。读取操作会立刻返回,程序可以进行其他操作,所有的读取、拷贝工作都由内核去做,做完以后通知进程,进程调用绑定的回调函数来处理数据

阻塞与非阻塞

  • 阻塞调用会一直等待远程数据就绪再返回,即上面阶段1会阻塞调用者,知道读取结束
  • 非阻塞无论在什么情况下都会立即返回,虽然非阻塞大部分时间不会被block,但是它仍要求进程不断的去主动询问kernel是否准备好数据,也需要进程主动的再次调用recvfrom来讲数据拷贝到用户内存

同步与异步

  • 同步方法会一直阻塞进程,知道IO操作结束。注意这里相当于上面阶段1、阶段2都会阻塞调用者
  • 异步方法不会阻塞调用者进程,即使时从内核空间的缓冲区将数据拷贝到进程中这一操作也不会阻塞进程,拷贝完毕后,内核会通知进程数据拷贝结束。