一、I/O模型
一个输入通常包括两个阶段:
- 等待数据准备好
- 从内核向检查复制数据
对于一个套接字上的操作,第一步通常涉及等待数据从网络中到达。当所等数据到达时,它被复制到内核中的某个缓冲区,第二步数据从内核缓冲区复制到应用进程缓冲区。
Unix有五种I/O模型
- 阻塞式I/O
- 非阻塞式I/O
- I/O服用(select、poll、epoll)
- 信号渠道式I/O(SIGIO)
- 异步I/O(AIO)
阻塞式I/O
应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区才返回。
在阻塞的的过程中,其他应用进程孩可以进行,因此阻塞不意味整个操作系统阻塞。因为其他应用进程还可以执行,所以不消耗CPU时间,这种模型的CPU利用率会比较高。
非阻塞式I/O
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知I/O是否完成,这种方式称为轮询(polling)
由于CPU要处理很多的系统调用,因此这种模型的CPU利用率比较低
I/O复用
使用select获知poll等待数据,并且可以等待多个套接字中的任何一个变成可用。这一过程会被阻塞,当某个套接字可读时返回,之后在使用recvfrom把数据从内核复制到进程中。
它可以让单个进程具有处理多个I/O时间的能力。又被称为Event Driven I/O
如果一个web服务器没有I/O复用,那么每个socket链接都需要创建一个线程去处理如果同时有几万个链接,那么就需要创建相同数量的线程。相比于多进程和多线程,I/O复用不需要进程线程创建和切换的开销,系统开销更小。
信号驱动I/O
应用进程使用sigaction系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送SIGIO信号,应用进程收到之后在信号处理程序中调用recvfrom将数据从内核复制到应用进程中。
相比于非阻塞式I/O的轮询方式,信号驱动I/O的CPU利用率更高。
异步I/O
应用进程执行aio_read系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核汇总所有操作完成之后向应用进程发送信号。异步I/O与信号驱动I/O的区别在于,异步I/O的信号是通知应用进程I/O完成,而信号驱动I/O的信号是通知应用进程可以开始I/O。
五大I/O模型比较
- 同步I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。
- 异步I/O:第二阶段应用进程不会阻塞
同步I/O包括阻塞式I/O、非阻塞式I/O、I/O复用和信号驱动I/O,主要区别在第一阶段。
非阻塞式I/O、信号驱动I/O和异步I/O在第一阶段不会阻塞。
二、I/O复用
select、poll、epoll都是I/O多路复用的实现,select出现的最早,后面是poll、再是epoll。
select
select允许应用监视一组文件描述符,等待一个或多个描述符成为就绪状态,从而完成I/O操作。
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
fd_set使用数组实现,数组大小使用FD_SETSIZE定义,只能监听少于FD_SESIZE数量的描述符
poll
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
pol的功能与select类似,也是等待一组描述符中的一个成为就绪状态。
poll中的描述符是poll类型的数组,pollfd的定义如下
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
比较
1、功能
select和poll的功能基本相同,不过在一些实现细节上有所不同。
- select会修改描述符,而poll不会;
- select的描述符类型使用数组实现,FD_SESSIZE大小默认是1024,因此默认只能监听少于1024个描述符。如果要监听更多描述符的话,需要修改FD_SETSIZE之后重新编译;而poll没有描述符数量的限制;
- poll提供了更多的事件类型,并且对描述符的重复利用上比select高。
- 如果一个线程对某个描述符调用 select或poll,另一个线程关闭了该描述符,会导致调用结果不确定。
2、速度
select和poll速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。3、可移植性
几乎所有的系统都支持select,但是只有比较新的系统支持poll。
epoll
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
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同一块内存实现的。