本文参考董浩博客 http://donghao.org/uii/
(1)内核为epoll做准备工作
这个模块在内核初始化时(操作系统启动)注册了一个新的文件系统,叫"eventpollfs"(在eventpoll_fs_type结构里),然后挂载此文件系统。另外还创建两个内核cache(在内核编程中,如果需要频繁分配小块内存,应该创建kmem_cahe来做“内存池”),分别用于存放struct epitem和eppoll_entry。这个内核高速cache区,就是建立连续的物理内存页,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的内存。
现在想想epoll_create为什么会返回一个新的fd?
因为它就是在这个叫做"eventpollfs"的文件系统里创建了一个新文件!返回的就是这个文件的fd索引。完美地遵行了Linux一切皆文件的特色。
(2)epoll_create(int size)
epoll_ create时,内核除了帮我们在epoll文件系统里建了新的文件结点,将该节点返回给用户。还在内核cache里建立一个红黑树(实际上是每一个epoll对应的eventpoll)用于存储以后epoll_ctl传来的需要监听文件fd外,这些fd会以红黑树节点的形式保存在内核cache里,以便支持快速的查找、插入、删除操作。
(3)epoll_ctl
int epoll_ctl(int epfd, intop, int fd, struct epoll_event *event);
在这个实现时,将用户空间epoll_event拷贝到内核中,后续可以将其转化为epitem作为节点存入红黑树中,从eventpoll的红黑树中查找fd所对应的epitem实例(二分搜索),根据传入的op参数行为进行switch判断,对红黑树进行不同的操作。对于ep_insert,首先设置了对应的回调函数,然后调用被监控文件的poll方法(每个支持poll的设备驱动程序都要调用),其实就是在poll里调用了回调函数,这个回调函数实际上不是真正的回调函数,真正的回调函数(ep_poll_callback)在该函数内调用,这个回调函数只是创建了struct eppoll_entry,将真正回调函数和epitem关联起来,之后将其加入设备等待队列。当设备就绪,唤醒等待队列上的等待者,调用对应的真正的回调函数,这个回调函数实际上就是将红黑树上收到event的epitem插入到它的就绪队列中并唤醒调用epoll_wait进程。
在ep_insert中还将epitem插入到eventpoll中的红黑树上,然后还会去判断当前插入的event是否是刚好发生,如果是直接将其加入就绪队列,然后唤醒epoll_wait
(4)epoll_wait
int epoll_wait(int epfd,struct epoll_event *events,int maxevents, int timeout);
在epoll_wait主要是调用了ep_poll,在ep_poll里直接判断就绪链表有无数据,有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。当有数据时,还需要将内核就绪事件拷贝到传入参数的events中的用户空间,就绪链表中的数据一旦拷贝就没有了,所以这里要区分LT和ET,如果是LT有可能会将后续的重新放入就绪链表。
ps:我们在调用ep_send_events_proc()将就绪队列中的事件拷贝给用户的期间,新就绪的events被挂载到eventpoll.ovflist所以我们需要遍历eventpoll.ovflist将所有已就绪的epitem 重新挂载到就绪队列中,等待下一次epoll_wait()进行交付...
如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。
1.执行epoll_create时,创建了红黑树(eventpoll结构)
2.执行epoll_ctl时,创建就绪list链表,如果增加fd添加到红黑树上,然后向内核注册有事件到来时的回调函数,当设备上的中断事件来临时这个回调函数会向list链表中插入就绪的fd并唤醒epoll_wait进程。
3.执行epoll_wait时立刻返回准备就绪链表里的数据即可
1)支持一个进程打开大数目的socket描述符(FD)
select最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于1024,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
2)IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的,但是select/poll每次调用都会线性轮询扫描全部的fd集合,导致效率呈现线性下降。epoll不存在这个问题,它只会对”活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的回调函数实现的。那么,只有”活跃”的socket才会主动的去调用callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在Linux内核。3)两种触发模式,ET模式减少epoll_wait()的调用次数
EPOLL ET 边沿触发:只触发一次,无论缓冲区中是否还有剩余数据,直到有新的数据到达才会被触发,再去读取缓冲区里面的数据。EPOLL LT 水平触发(默认): LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket,每次缓冲区都有数据都要触发。Epoll可以监控管道文件,任意文件,不仅仅是socket文件
4)使用mmap加速内核与用户空间的消息传递
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核与用户空间mmap同一块内存实现的。
客户端给我写的数据带有自己设计的协议头,而我只需要读取客户端数据的协议头,判断是否需要继续往下读取。如果不需要则不继续读取剩下的数据,增加程序运行的效率。
epoll工作在ET模式的时候,必须使用非阻塞套文件读写,以避免由于一个文件句柄的阻塞读/阻塞写操作容易阻塞在read函数时,因为没有读取到需要的字节数,而服务器又不能脱离read的阻塞状态去调用epoll函数接收客户端的数据造成死锁。
解决方法
1、非阻塞读取用fcntl 修改文件描述符的非阻塞读属性。
2、在open时指定非阻塞打开属性
Epoll-ET的非阻塞IO模型::read非阻塞轮询+边沿触发
去读取文件描述符的数据,直到为0,最效率的方法。减少epoll_wait的调用次数提高程序的效率。
支持ET模式的原理,可以看上图的源码过程或下面分析!
当一个fd上有事件发生时,内核会把该fd插入上面所说的准备就绪rdlist链表,这时我们调用epoll_wait,会把准备就绪的fd拷贝到用户态内存,然后清空就绪list链表。
最后,epoll_wait检查这些fd,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪rdlist链表了。所以,非ET的句柄,只要它上面还有事件,epoll_ wait每次都会返回。而ET模式的句柄,除非有新中断到,即使fd上的事件没有处理完,也是不会次次从epoll_wait返回的
处理EPOLLONESHOT事件
当我们使用ET模式,一个socket上的某个事件还是可能被触发多次,这在并发程序中就会引起一个问题。比如在线程池或者进程池模型中,某一个线程或进程正在处理一个有事件的socket文件描述符时,在这个时候这个socket文件描述符又有新的数据可以读取,此时另外一个线程被唤醒来读取这些新的数据,于是就出现了两个线程或者进程同时操作一个文件描述符的情况,这个时候就容易发生错误,因为某个线程调度的时间和顺序是不能确定的,很有可能一个线程或者进程已经把数据读取完成,而且另外的一个进程或者线程还在读取这个文件描述符。 这是我们所不希望的,我们希望在任意时刻都只有一个线程或者进程在操作一个文件描述符,关于这一点要求epoll提供了一个叫做EPOLLONESHOT事件的实现。对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其注册的一个可读可写或者异常事件,且只触发一次。除非我们使用epoll_ctl()函数重置该文件描述符上注册的EPOLLONESHOT事件。反过来思考的话,注册EPOLLONESHOT事件的文件描述符一旦被某个线程或者进程处理完成后,该线程或进程就应该立即重置这个sock文件描述符上的EPOLLONESHOT事件,以确保这个sockfd文件描述符下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个sockfd文件描述符.。
(1)I/O密集型,链接的人数非常多,活跃的人数相对来说少,CPU利用率提升非常明显,这是epoll的优势所在。
多链接多活跃的情况下效率的提升并不明显,三种多路IO转接都差别不大。如果活跃量很大的话回调函数反复调用反而影响效率。
这个时候设置的最大打开文件描述符可以通过更改到硬件水平,软件水平一般都是65535
epoll需要突破系统资源设置65535,在配置文件里加入硬件限制和软件限制设置。$ulimit –n 90000
通过命令$cat /pro/sys/fs/fs/file-max查看最大的文件打开数量。
(2)并发支持完美,不会随着socket的增加而降低效率,也不用在内核空间和用户空间之间做无效的copy操作。
1、因为selete和poll只有一个函数,epoll是一组函数,每次调用select和poll的时候都需要将监控的fd和需要监控的事件从用户空间拷贝到内核空间,非常影响效率。而epoll就是自己保存用户空间拷入的fd和需要监控的事件,只需在调用epoll_ctl的时候就把所有的fd和需要监控的事件只进行一次从用户空间到内核空间的拷贝
2、select和poll在内核中都是采用线性轮询的方式检查整个数组(poll是将事件维护了一个链表poll_list)里的活跃fd,对于许多没有数据的fd来说这浪费了不必要的时间。如果我们不再检查活跃的fd,而是活跃的fd自动调用一个回调函数,把自己挂到就绪队列里。那不是简单的多么?EPOLL是通过回调函数自动把就绪的文件描述符放入到一个就绪链表中而不需要遍历文件描述符。通过epoll_wait()函数将就绪的文件描述符返回给用户
epoll是在epoll_ctl中调用fd对应的设备驱动程序的poll,进行注册回调函数,等设备驱动程序有数据时调用回调函数,即epoll只是调用了一次设备驱动程序的poll加入等待队列;而poll它是每次调用poll都会去对每一个fd轮流将其进程加入fd对应的设备等待队列,调用次数很多,效率低。(即epoll只在epoll_ctl时把进程挂一遍(这第一遍是免不了的),但是poll每次调用都会将每一个fd的进程挂到fd对应的设备等待队列上)
①从上面的调用方式就可以看出epoll比select/poll的一个优势:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。
② 此外,内核使用了slab机制,为epoll提供了快速的数据结构:
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
③ epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。