一个IO输入操作通常包括两个阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字:

  • 第一步则是等待数据从网络中到达。当数据到达时,它被复制到内核中的某个缓冲区。
  • 第二部就是把数据从内核缓冲区复制到应用进程缓冲区。

Unix有五种I/O模型

  • 阻塞式I/O
  • 非阻塞式I/O
  • IO复用
  • 信号驱动I/O
  • 异步I/O

阻塞式I/O

这是最简单的I/O,意味着我们发起一次I/O操作后一直等待成功或者失败,期间是阻塞的不能做其他事情。
image.png

非阻塞式I/O

通过轮询的方式查看IO数据是否准备完毕,要么数据准备好了、IO操作成功;要么IO数据没准备好,返回错误码。
因为要不停的执行系统调用来查询I/O是否已经准备完毕,所以CPU要处理更多的系统调用,所以这种模型的CPU利用率低。
image.png

I/O复用

使用select或者poll等待数据,等待套接字中的任何一个事件变为可读,这一过程会被阻塞。
当有事件可读时,使用recvfrom把数据从内核复制到进程。

I/O复用让单个进程具有处理多个I/O事件的能力,即事件驱动I/O

当对于连接较多的情况,如果没有I/O复用,那么每一个socket连接都需要创建一个线程进行处理,那么线程切换的开销是很大的,有了I/O复用不需要进程去创建多个线程以及线程切换的开销。
image.png

信号驱动I/O

等待数据阶段同样为非阻塞的。内核在数据到达时向进程发送SIGIO信号,进程收到后在信号处理程序中,调用相关函数完成IO。
信号驱动IO在网络编程时很少用到,因为在网络环境中,和socket相关的读写事件太多,这导致了SIGIO信号种类也太过,以至于我们没有办法去通过信号处理函数去区分不同信号。
所以SIGIO值应该在IO事件单一情况下使用,比如用来监听某个端口的socket。

image.png

异步I/O

异步I/O和信号驱动I/O类似,差别主要在于:信号驱动IO的信号是通知应用进程可以开始IO,而异步IO是通知进程IO的完成。

同步IO和异步IO

第二阶段无阻塞发发生,则为异步IO
image.png

I/O复用

select

select监听一组文件描述符,等待一个或者多个文件描述符成为就绪状态,总而完成IO操作。
select的特点:

  • 文件描述符组采用数组实现,数组大小使用FD_SETSIZE(默认为1024)设定,只能注册并监听少于FD_SETSIZE数量的描述符。
  • 有三种类型的描述符:readset,writeset,exceptset分辨对于读、写、异常。
  • timeout为超时参数,调用select会一直阻塞直到描述符上的事件就绪。
  • 成功调用返回结果大于0,出错返回-1,超时返回0

poll

poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态。

select和poll的主要区别

  • select会修改描述符,而poll不会。
  • select限定了大小默认为1024,改变需要重新编译,而poll没有描述符数量的限制。
  • poll提供了更多的事件类型,对描述符的重复利用比select高

select的好处:

  • select被更多系统支持,只有比较新的系统支持poll
  • select提供精度更高的超时时间,而poll只支持到毫秒的精度。


epoll

epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。
已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理
epoll_wait() 在一个文件描述符中等待某个IO事件完成直至超时。

epoll的工作模式:

  • level trigger:默认模式,当有数据可读时,每次调用epoll_wait都会返回以通知程序可以进行IO操作
  • edge trigger:只有当文件描述符状态发生变化时,调用epoll_wait才会返回,如果第一次调用epoll_wait没有全部读完文件描述符中的数据,并且还没有新的数据写入,再次调用epoll_wait则不会有通知给到程序,因为文件描述符的状态没有变化。

epoll相较于其他IO复用

  • 当需要监听的fd增加时,select和poll的O(N)的复杂度,epoll是O(1),所以高性能的网络服务器都会选择epoll进行IO多路复用。但当fd小于1000时,差距并不会很大。
  • 当需要需要监控的描述符状态变化多,而且十分短暂时,没有必要使用epoll,因为涉及到通过系统调用epoll_ctl()对文件描述符状态的修改,频繁调用会导致效率的降低。