一、I/O模型

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

  • 等待数据准备好
  • 从内核向检查复制数据

对于一个套接字上的操作,第一步通常涉及等待数据从网络中到达。当所等数据到达时,它被复制到内核中的某个缓冲区,第二步数据从内核缓冲区复制到应用进程缓冲区。

Unix有五种I/O模型

  • 阻塞式I/O
  • 非阻塞式I/O
  • I/O服用(select、poll、epoll)
  • 信号渠道式I/O(SIGIO)
  • 异步I/O(AIO)

阻塞式I/O

应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区才返回。

在阻塞的的过程中,其他应用进程孩可以进行,因此阻塞不意味整个操作系统阻塞。因为其他应用进程还可以执行,所以不消耗CPU时间,这种模型的CPU利用率会比较高。
soket - 图1

非阻塞式I/O

应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知I/O是否完成,这种方式称为轮询(polling)

由于CPU要处理很多的系统调用,因此这种模型的CPU利用率比较低
soket - 图2

I/O复用

使用select获知poll等待数据,并且可以等待多个套接字中的任何一个变成可用。这一过程会被阻塞,当某个套接字可读时返回,之后在使用recvfrom把数据从内核复制到进程中。

它可以让单个进程具有处理多个I/O时间的能力。又被称为Event Driven I/O

soket - 图3

如果一个web服务器没有I/O复用,那么每个socket链接都需要创建一个线程去处理如果同时有几万个链接,那么就需要创建相同数量的线程。相比于多进程和多线程,I/O复用不需要进程线程创建和切换的开销,系统开销更小。

信号驱动I/O

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

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

异步I/O

应用进程执行aio_read系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核汇总所有操作完成之后向应用进程发送信号。异步I/O与信号驱动I/O的区别在于,异步I/O的信号是通知应用进程I/O完成,而信号驱动I/O的信号是通知应用进程可以开始I/O。
soket - 图5

五大I/O模型比较

  • 同步I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。
  • 异步I/O:第二阶段应用进程不会阻塞

同步I/O包括阻塞式I/O、非阻塞式I/O、I/O复用和信号驱动I/O,主要区别在第一阶段。
非阻塞式I/O、信号驱动I/O和异步I/O在第一阶段不会阻塞。

soket - 图6

二、I/O复用

select、poll、epoll都是I/O多路复用的实现,select出现的最早,后面是poll、再是epoll。

select

select允许应用监视一组文件描述符,等待一个或多个描述符成为就绪状态,从而完成I/O操作。

  1. int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

fd_set使用数组实现,数组大小使用FD_SETSIZE定义,只能监听少于FD_SESIZE数量的描述符

poll

  1. int poll(struct pollfd *fds, unsigned int nfds, int timeout);

pol的功能与select类似,也是等待一组描述符中的一个成为就绪状态。
poll中的描述符是poll类型的数组,pollfd的定义如下

  1. struct pollfd {
  2. int fd; /* file descriptor */
  3. short events; /* requested events */
  4. short revents; /* returned events */
  5. };

比较

1、功能

select和poll的功能基本相同,不过在一些实现细节上有所不同。

  • select会修改描述符,而poll不会;
  • select的描述符类型使用数组实现,FD_SESSIZE大小默认是1024,因此默认只能监听少于1024个描述符。如果要监听更多描述符的话,需要修改FD_SETSIZE之后重新编译;而poll没有描述符数量的限制;
  • poll提供了更多的事件类型,并且对描述符的重复利用上比select高。
  • 如果一个线程对某个描述符调用 select或poll,另一个线程关闭了该描述符,会导致调用结果不确定。

    2、速度

    select和poll速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。

    3、可移植性

    几乎所有的系统都支持select,但是只有比较新的系统支持poll。

epoll

  1. int epoll_create(int size);
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_ctl()用于向内核注册新的描述符获知是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将I/O准备好的描述符加入到一个链表中管理,进程调用epoll_wat()便可以得到事件完成的描述符。

从上面的描述可以看出,epoll只需要将描述符冲进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。

epoll仅适用于linux os。
epoll比select和poll更加灵活而且没有描述符数量限制。
epoll对多线程编程更加友好,一个线程调用了epoll_wait()另一个线程关闭了同一个描述符也不会产生像select和poll的不确定情况

工作模式

epoll的描述符事件有两种出发模式:LT(level trigger)和ET(edge trigger)。

1、LT模式

当epoll_wait()检查到描述符事件到达时,将此事件通知进程,进程可以不理解处理该事件,下次调用epoll_wait()会再次通知进程。是默认的一种模式,并且同事支持Blocking和No-Blocking。

2、ET模式

和LT模式不同的是,通知之后进程必须立即处理事件,下次再调用epoll_wait()时不会再得到事件到达的通知。
很大程度监视了epol事件被重复出发的次数,因此效率比Lt模式高,只支持No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

应用场景

1、select应用场景

select的timeout参数精度为微妙,而poll和epoll为毫秒,因此select更加适用于实时性要求比较高的场景,比如核反应堆的控制。select可移植性更好,几乎所有主流平台都支持

2、poll应用场景

poll没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应用使用poll而不是select。

3、epoll应用场景

只需要运行在Linux平台,有大量的描述符需要同事轮询,并且这些链接最后是长链接。
需要同时监控小于1000个描述符,就没有必要使用epoll,因为这个应用场景并不能体现epoll的优势。

需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用epoll。因为epoll中的所有描述符都存在内存中,造成每次需要对描述符的状态改变都需要通过epoll_ctl()进行系统调用,频繁系统调用降低效率。并且epoll的描述符存在内核,不容易调试。

select和epoll区别

1、select句柄数目受限,最多支持1024个,epoll的上限取决于系统。
2、epoll不会随着fd的数目增加而降低效率,select是数组,epoll是用的队列。epoll只会对活跃的socket进行操作,只有活跃的socket才会主动去调用callback函数(把句柄加入队列)。如果绝大部分I/O都是活跃的,epoll效率不一定比select高。
3、使用mmap加速内核与用户空间的消息传递。无论select,poll还是epoll都需要内核把fd消息通知给用户空间。epoll是通过内核于用户空间mmap同一块内存实现的。