IO多路复用之epoll模型

1.前言

epoll是Linux在2.6内核版本中提出的,是之前select和poll的增强版本.相对于select和poll来说,epoll做了更细致的分解,包含了三个方法,使用上更加灵活分别为epoll_createepoll_ctlepoll_wait

2.epoll_create函数

int epoll_create(int size):
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的参数,并不会限制epoll所能监听的描述符的最大个数,只是对内核储时分配内部数据结构的一个建议。当创建号epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能会导致fd被耗尽

这个函数可以理解为对应NIO编程里的Selector.open()

3.epoll_ctl函数

int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event)
该函数是对指定描述符fd执行op操作,其中
epfd:是epoll_create的返回值
op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件
fd:是需要监听的fd(文件描述符)
epoll_event:是告诉内核需要监听什么事件,有具体的宏可以使用,比如EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);EPOLLOUT:表示对应的文件描述符可以写;

这个函数可以理解为NIO编程里的socketChannel.register()

4.epoll_wait函数

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
epfd:等待epfd上的io事件,最多返回maxevents个事件,
events:用来从内核得到事件的集合,
maxevents:告诉内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size
timeout:超时时间(毫秒, 0表示会立即返回, -1将不确定,也有说话是永久阻塞)。该函数返回需要处理的时间数目,如返回0表示已超时

这个函数可以理解为NIO编程里面的selector.select();

5.epoll原理和流程

当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象(也就是程序中的epfd所代表的对象)eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列.
创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。以添加socket为例,如下图,如果通过epoll_ctl添加socket1、socket2、socket3的监视,内核会将这三个socket添加到eventpoll的等待队列中
IO多路复用之epoll模型_第1张图片
当socket受到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。中断程序会给eventpoll的“就绪列表”添加socket引用。如下图展示的时socket2和socket3收到数据后,中断程序让rdllist引用这两个socket
IO多路复用之epoll模型_第2张图片
eventpoll对象相当于时socket和进程之间的中介,socket的数据并不直接影响进程,而是通过改变evetpoll的就绪列表(rdllist)来改变进程状态.
当程序执行到epoll_wait时,如果rdllist已经引用了socket,那么epoll_wait直接返回,如果rdllist为空,阻塞进程
假设计算机中正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。如下图,内核会将进程A放入到eventpoll的等待队列中,阻塞进程
IO多路复用之epoll模型_第3张图片
当socket接收到数据,终端程序一方面修改rdllist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态.也因为rdllist的存在,进程A可以知道哪些socket发生了变化

6.epoll的实现细节

IO多路复用之epoll模型_第4张图片
Linux源码fs目录下的eventpoll.h如上图所示,它的就绪列表rdllist引用着就绪的socket,所以它应能够快速地插入数据
程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被快速移除
所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll使用双线双向链表来实现就绪队列
既然epoll将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的socket。至少要方便的添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),还可以通过常数操作实现平衡,效率较好。epoll使用了红黑树作为索引结构也就是上面源码当中的struct rb_root_cached rbr

7.epoll的设计思路

epoll是在select出现N多年后才被发明的,是select和poll的增强版本。epoll通过以下一些措施来改进效率
措施一:功能分离
select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。每次调用select都需要这两部操作,然而大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改,epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。显而易见的,效率就能得到提升
相比select,epoll拆分了功能
epoll的用法,如下方代码,先用epoll_create创建一个epoll对象的epfd,再通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据

	int epfd = epoll_create();
	// 将所有需要监听的socket添加到epfd中
	epoll_ctl(epfd,....)
	
	while(1) {
		int n = epoll_wait(...)
		for(接收到数据的socket) {
			// 处理
		}
	}

功能分离,使得epoll有了优化的可能

措施二:就绪列表
select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历,如果内核维护一个"就绪列表",引用收到数据的socket,久能避免遍历

8.总结

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表用于存储准备就绪的时间,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可,有数据就返回,没有数据就sleep,等待timeout时间到后即时链表没数据也返回。
同时,所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法,这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中
当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rldlist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户,因此epoll_wait效率非常高,可以请意地处理百万级别地并发连接

这里还牵扯到了IO事件的触发模式
epoll除了提工select/poll那种IO事件的水平触发外,还提供了边缘触发,这就使得用户空间有可能缓存IO状态,减少epoll_wait的调用,提高应用程序效率

水平触发
只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知,epoll默认模式是水平触发

边缘触发
当文件描述符关联的读内核缓冲区非空时,则发出可读信号进行通知,写缓冲区不满时,则发出可写信号进行通知,
边缘触发只会通知一次

个人理解:
当有数据可读时,水平触发会一直通知epoll对象告诉我们有数据可以读,触发epoll_wait系统调用,如果我们上一次的socket还没处理完毕,则会造成该系统调用的阻塞,消耗CPU,但是如果我们先让数据留存在缓冲区,等这次epoll_wait处理完毕之后,再去读,此时也有可能不止一个socket中有数据,我们可以一次性读取多个socket,效率比较高

你可能感兴趣的:(网络IO,java,开发语言)