在Linux网络编程中,Linux内核2.6版本之前大多都是用 select() 作为非阻塞的事件触发模型,但是效率低,使用受限已经很明显的暴露了select()(包括poll)的缺陷,为了解决这些缺陷,epoll作为linux新的事件触发模型被创造出来。
一、epoll() 相对于 select() 的优点:
1、支持一个进程socket描述符(FD)的最大数目:
在select() 中,一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置(linux/posix_types.h 中定义:#define __FD_SETSIZE 1024)。表示 select() 最多同时监听1024个fd。对于那些需要支持上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样 会带来网络效率的下降;二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加 上进程间数据同步远比不上线程间同步高效,所以这也不是一种完美的方案。而 epoll() 支持的数目很大,等于系统最大打开的文件描述符数,这个文件描述符数跟内存有一定关系。
2、IO效率不随FD数目增加而线性下降:
select() 对事件的扫描是针对于所有创建的socket描述符进行的,也就是说,有多少个socket描述符,就需要遍历多少个句柄,所以IO效率是随描述符增加线性下降的;而epoll只遍历活跃的 socket 描述符,这是因为在内核实现中 epoll() 是根据每个 fd 上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback 函数,其他 idle 状态 socket 则不会。比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
3、使用 mmap 加速内核与用户空间的消息传递
select() 事件触发后会将信息从内核拷贝到用户空间,这种拷贝就影响了效率。而 mmap 将内核与用户空间的内存映射到一块内存上,内核将消息捕获后放入该内存空间,用户无需拷贝直接可以访问,减少了拷贝次数,提高了效率。(mmap 将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。)
二、epoll() 工作模型:ET、LT
LT(level triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET (edge-triggered) 是高速工作方式,只支持no-block socket。 在这种模式下,当描述符从未就绪变为就绪时,内核就通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的 就绪通知,直到你做了某些操作而导致那个文件描述符不再是就绪状态(比如 你在发送,接收或是接受请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核就不会发送更多的通知(only once)。不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。效率非常高,在并发,大流量的情况下,会比LT少很多 epoll() 的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
三、epoll() 的使用
1、epoll() 用到的所有函数都是在头文件 sys/epoll.h 中声明的
2、epoll 的三大函数:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t;