IO模型

对于一次IO访问,会分为两个阶段,首先等待数据准备好后被拷贝到操作系统内核的缓冲区中,然后再从操作系统内核的缓冲区拷贝到用户空间。IO模型可以分为以下五种类型:

  • 阻塞式I/O(Blocking I/O)
  • 非阻塞式I/O(Non-blocking I/O)
  • 多路复用I/O(Multiplexing I/O)
  • 信号驱动I/O(Signal-driven I/O)
  • 异步I/O(Asynchronous I/O)

其中信号驱动式IO并不常用,所以重点关注另外四种IO模型。

阻塞式I/O

在这个IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞,不能处理别的IO。

阻塞IO的特点就是能够及时的返回数据,但是在IO执行的两个阶段都被阻塞了,只有当数据从内核复制到了用户空间中,进程才能继续往下执行,因此对性能有所牺牲。

非阻塞式I/O

应用进程执行系统调用之后,内核会立即返回一个错误码,但IO操作还没完成。此时应用进程并没有被阻塞,可以继续执行,但是需要不断的执行系统调用来获知IO操作是否完成,这种方式称为轮询。

这种模型的CPU利用率比较低,并且因为每过一段时间才去轮询一次,所以存在一个响应延迟。还需要注意的是,拷贝数据的整个过程,进程仍然是属于阻塞的状态。

I/O多路复用

使用select或者poll对多个IO端口进行监听,只要多个套接字中的任何一个数据准备好了,就能返回可读,之后应用进程再执行recvfrom系统调用把数据从内核复制到进程中。

I/O复用模型让单个进程具有处理多个I/O事件的能力,因此相比多进程和多线程技术,它的系统开销小了许多。但是selectpollepoll函数依然会阻塞应用进程,并且由于多路复用可以处理多个IO,那么多个IO之间的顺序就变得不确定了。

信号驱动I/O

应用进程使用sigaction系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送SIGIO信号,应用进程收到之后在信号处理程序中调用recvfrom将数据从内核复制到应用进程中。

相比于非阻塞式I/O的轮询方式,信号驱动I/O的CPU利用率更高。

异步非阻塞I/O

用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户进程就可以去做别的事情。等到数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。这个IO模式的两个阶段,进程都是非阻塞的。

五种I/O模型对比

  • 阻塞式I/O:同步阻塞
  • 非阻塞式I/O:同步非阻塞(轮询)
  • I/O多路复用:同步阻塞(可以监听多个IO)
  • 信号驱动I/O:同步非阻塞(收到SIGIO信号后才执行recvfrom并阻塞)
  • 异步I/O:异步非阻塞(两个阶段都不会阻塞)

I/O多路复用中的select、poll、epoll

select,poll,epoll都是IO多路复用的机制,select出现的最早,之后是poll,再是epoll。I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

  • select:它仅仅知道有I/O事件发生了,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,测试每个流是否有事件发生,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的时间复杂度,同时处理的流越多,无差别轮询时间就越长。
  • poll:poll本质上和select没有区别,需要查询每个fd对应的设备状态,但是它没有最大连接数的限制,因为select的描述符类型使用数组实现,而poll的描述符类型使用链表实现。
  • epoll:epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

参考资料