前言
I/O模型大体分为阻塞式I/O 模型和非阻塞式I/O模型。其中,前者最大的问题在于,每一次IO操作,都会有一个线程从发起读指令后一直等待直到本次IO的数据到达用户态或者出现错误。
这样BIO会存在两个问题:1在本地IO事件中,会启动过多的线程,占用太多的内存,也给cpu的切换带来很大的负担。2 在Linux的网络IO中,由于线程的阻塞,会导致不能获取到内核态已经建立好的连接,会导致Accept队列阻塞,进而导致sync队列,最后使得连接无法建立。
因为BIO存在上述问题,所以,出现了NIO。
NIO是什么?
NIO是非阻塞式IO,解决的问题是BIO的等待问题。
NIO 又分为select/poll模型和epoll模型。
select/poll
进程通过将一个或者多个fd传递给select或者poll系统调用,并阻塞在select上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否处于就绪状态,并且大量的套接字传给操作系统让其检查状态是非常浪费资源的(首先是用户态到内核态的大量数据的复制),因此fd的数量树有限的,也就几千个并发连接。因此它的使用受到了限制。
epoll
为了克服 select/poll模型的问题,epoll就出现了。epoll使用事件驱动机制代替顺序扫描机制,因此效率更高一些,当fd的事件就绪时,立即回调rollback函数。
epoll 详解
下图为epoll 原理示意图。
epoll在linux内核空间开启了一个简易的文件系统,把原来的select/poll模型分为三部分:调用epoll_create建立一个epoll对象、调用epoll_ctl向epoll对象中添加众多的socket套接字、调用epoll_wait收集发生事件的连接。这样,只需要在进程启动的时候创建一个eppll对象,并在需要的时候添加和删除连接就可以了,因此在世纪收集事件时,epoll_wait对并没有向内核传递这众多的连接,内核也不需要遍历全部的连接了。因此epoll_wait的效率是非常高的。
linux中epoll的具体实现
当一个进程调用epoll_create方法时,linux会创建一个eventpoll结构体,如上图所示。这个结构体中的rbr红黑树结构和rdllist双向链表结构非常重要。
每一个epoll对象都有一个独立的eventpoll结构体,event_ctl方法会把连接封装成事件信息epitem添加到rbr红黑树红黑树中,这样就能够高效的识别出重复添加的事件。并且每一个连接(事件)都会和设备(网卡)创建程序的回调关系,也就是说,相应的事件发生时,会调用这里的ep_poll_callback函数,该函数会把事件放在rdllist结构中。
当调用epoll_wait函数检查是否有事件连接发生时,只需要检查eventpoll结构体中的rbllist双向链表即可。如果rbllist不为空,则把这里的事件复制到用户态的内存中,同时将事件数量返回给用户。因此,epoll_wait的效率是非常高的。epoll_ctl在向epoll对象添加、修改、删除对象时,从rbr红黑树中查找事件是非常快的,也就是说,epoll是非常高效的。他可以轻易处理几百分的并发连接。
总结
NIO 克服了BIO的阻塞的优势,NIO中的select/poll模型又有用户态和内核态复制的压力和循环遍历的压力,epoll又克服了这些问题。epoll被分成三块,每一块效率都很高,进而可以说epoll下的NIO效率很高。