Epoll模型详解

2021年09月15日 阅读数:2
这篇文章主要向大家介绍Epoll模型详解,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

转自http://blog.163.com/huchengsz@126/blog/static/73483745201181824629285/ html

Linux 2.6内核中提升网络I/O性能的新方法-epoll I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。

一、为何select落后
首先,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每一个FD_SET的句柄个数,在 我用的2.6.15-25-386内核中,该值是1024,搜索内核源代码获得:
include/linux/posix_types.h:
#define __FD_SETSIZE 1024
也就是说,若是想要同时检测1025个句柄的可读状态是不可能用select实现的。或者 同时检测1025个句柄的可写状态也是不可能的。其次,内核中实现 select是用轮询方法,即每次检测都会遍历全部FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。固然,在前文中我并无说起poll方法,事实上用select的朋友必定也试过poll,我我的以为 select和poll大同小异,我的偏好于用select而已。

二、内核中提升I/O性能的新方法epoll
epoll是什么?按照man手册的说法:是为处理大批量句柄而做了改进的poll。要使用epoll只须要这三个系统调 用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。
固然,这不是2.6内核才有的,它是在 2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)

Linux2.6 内核epoll介绍
先介绍2本书《The Linux Networking Architecture--Design and Implementation of Network Protocols in the Linux Kernel》,以2.4内核讲解Linux TCP/IP实现,至关不错.做为一个现实世界中的实现,不少时候你必须做不少权衡,这时候参考一个久经考验的系统更有实际意义。举个例子,linux内 核中sk_buff结构为了追求速度和安全,牺牲了部份内存,因此在发送TCP包的时候,不管应用层数据多大,sk_buff最小也有272的字节.其实 对于socket应用层程序来讲,另一本书《UNIX Network Programming Volume 1》意义更大一点.2003年的时候,这本书出了最新的第3版本,不过主要仍是修订第2版本。其中第6章《I/O Multiplexing》是最重要的。Stevens给出了网络IO的基本模型。在这里最重要的莫过于select模型和Asynchronous I/O模型.从理论上说,AIO彷佛是最高效的,你的IO操做能够当即返回,而后等待os告诉你IO操做完成。可是一直以来,如何实现就没有一个完美的方 案。最著名的windows完成端口实现的AIO,实际上也是内部用线程池实现的罢了,最后的结果是IO有个线程池,你应用也须要一个线程池...... 不少文档其实已经指出了这带来的线程context-switch带来的代价。在linux 平台上,关于网络AIO一直是改动最多的地方,2.4的年代就有不少AIO内核patch,最著名的应该算是SGI那个。可是一直到2.6内核发布,网络 模块的AIO一直没有进入稳定内核版本(大部分都是使用用户线程模拟方法,在使用了NPTL的linux上面其实和windows的完成端口基本上差很少 了)。2.6内核所支持的AIO特指磁盘的AIO---支持io_submit(),io_getevents()以及对Direct IO的支持(就是绕过VFS系统buffer直接写硬盘,对于流服务器在内存平稳性上有至关帮助)。
因此,剩下的select模型基本上就是咱们在linux上面的惟一选择,其实,若是加上no-block socket的配置,能够完成一个"伪"AIO的实现,只不过推进力在于你而不是os而已。不过传统的select/poll函数有着一些没法忍受的缺 点,因此改进一直是2.4-2.5开发版本内核的任务,包括/dev/poll,realtime signal等等。最终,Davide Libenzi开发的epoll进入2.6内核成为正式的解决方案

三、epoll的优势
<1>支持一个进程打开大数 目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有必定限制的,由FD_SETSIZE设置,默认值是2048。对于那些须要支持的上万链接数目的IM服务器来讲显 然太少了。这时候你一是能够选择修改这个宏而后从新编译内核,不过资料也同时指出这样会带来网络效率的降低,二是能够选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面建立进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,因此也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大能够打开文件的数目,这个数字通常远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目能够cat /proc/sys/fs/file-max察看,通常来讲这个数目和系统内存关系很大。

<2>IO 效率不随FD数目增长而线性降低
传统的select/poll另外一个致命弱点就是当你拥有一个很大的socket集合,不过因为网络延时,任一时间只有部分的socket是"活跃"的, 可是select/poll每次调用都会线性扫描所有的集合,致使效率呈现线性降低。可是epoll不存在这个问题,它只会对"活跃"的socket进行 操做---这是由于在内核实现中epoll是根据每一个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其余idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,由于这时候推进力在os内核。在一些 benchmark中,若是全部的socket基本上都是活跃的---好比一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,若是过多使用epoll_ctl,效率相比还有稍微的降低。可是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

<3>使用mmap加速内核 与用户空间的消息传递。
这点实际上涉及到epoll的具体实现了。不管是select,poll仍是epoll都须要内核把FD消息通知给用户空间,如何避免没必要要的内存拷贝就 很重要,在这点上,epoll是经过内核于用户空间mmap同一块内存实现的。而若是你想我同样从2.5内核就关注epoll的话,必定不会忘记手工 mmap这一步的。

<4>内核微调
这一点其实不算epoll的优势了,而是整个linux平台的优势。也许你能够怀疑 linux平台,可是你没法回避linux平台赋予你微调内核的能力。好比,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么能够在运行时 期动态调整这个内存pool(skb_head_pool)的大小--- 经过echo XXXX>/proc/sys/net/core/hot_list_length完成。再好比listen函数的第2个参数(TCP完成3次握手 的数据包队列长度),也能够根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每一个数据包自己大小却很小的特殊系统上尝试最新的NAPI网 卡驱动架构。

四、epoll的工做模式
使人高兴的是,2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,因此,大部分状况下,强大的东西每每是简单的。惟一有点麻烦 是epoll有2种工做方式:LT和ET。
LT(level triggered)是缺省的工做方式,而且同时支持block和no-block socket.在这种作法中,内核告诉你一个文件描述符是否就绪了,而后你能够对这个就绪的fd进行IO操做。若是你不做任何操做,内核仍是会继续通知你 的,因此,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的表明.
ET (edge-triggered)是高速工做方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核经过epoll告诉你。而后它会假设你知道文件描述符已经就绪,而且不会再为那个文件描述 符发送更多的就绪通知,直到你作了某些操做致使那个文件描述符再也不为就绪状态了(好比,你在发送,接收或者接收请求,或者发送接收的数据少于必定量时致使 了一个EWOULDBLOCK 错误)。可是请注意,若是一直不对这个fd做IO操做(从而致使它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍须要更多的benchmark确认。
epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用,具体用法请参考http://www.xmailserver.org/linux-patches/nio-improve.html ,在http://www.kegel.com/rn/也有一个完整的例子,你们一看就知道如何使用了
Leader/follower模式线程 pool实现,以及和epoll的配合。

五、 epoll的使用方法
首先经过create_epoll(int maxfds)来建立一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,以后的全部操做 将经过这个句柄来进行操做。在用完以后,记得用close()来关闭这个建立出来的epoll句柄。以后在你的网络主循环里面,每一帧的调用 epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询全部的网络接口,看哪个能够读,哪个能够写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create建立以后的句柄,events是一个 epoll_event*的指针,当epoll_wait这个函数操做成功以后,epoll_events里面将储存全部的读写事件。 max_events是当前须要监听的全部socket句柄数。最后一个timeout是 epoll_wait的超时,为0的时候表示立刻返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,若是一直没 有事件,则范围。通常若是网络主循环是单独的线程的话,能够用-1来等,这样能够保证一些效率,若是是和主逻辑在同一个线程的话,则能够用0来保证主循环 的效率。


Epoll模型详解_模型详解
Epoll模型主要负责对大量并发用户的请求进行及时处理,完成服务器与客户端的数据交互。其具体的实现步骤以下:
(a) 使用epoll_create()函数建立文件描述,设定将可管理的最大socket描述符数目。
(b) 建立与epoll关联的接收线程,应用程序能够建立多个接收线程来处理epoll上的读通知事件,线程的数量依赖于程序的具体须要。
(c) 建立一个侦听socket描述符ListenSock;将该描述符设定为非阻塞模式,调用Listen()函数在套接字上侦听有无新的链接请求,在 epoll_event结构中设置要处理的事件类型EPOLLIN,工做方式为 epoll_ET,以提升工做效率,同时使用epoll_ctl()注册事件,最后启动网络监视线程。
(d) 网络监视线程启动循环,epoll_wait()等待epoll事件发生。
(e) 若是epoll事件代表有新的链接请求,则调用accept()函数,将用户socket描述符添加到epoll_data联合体,同时设定该描述符为非 阻塞,并在epoll_event结构中设置要处理的事件类型为读和写,工做方式为epoll_ET.
(f) 若是epoll事件代表socket描述符上有数据可读,则将该socket描述符加入可读队列,通知接收线程读入数据,并将接收到的数据放入到接收数据 的链表中,经逻辑处理后,将反馈的数据包放入到发送数据链表中,等待由发送线程发送。
linux




C++语言: epoll使用方法1
01 //epoll_wait范围以后应该是一个循环,遍利全部的事件:
02 for (n = 0; n < nfds; ++n)
03 {
04 if (events[n].data.fd == listener)
05 { //若是是主socket的事件的话,则表示有新链接进入了,进行新链接的处理。
06 client = accept (listener, ( struct sockaddr *) &local, &addrlen);
07 if (client < 0)
08 {
09 perror ( "accept");
10 continue;
11 }
12 setnonblocking (client); // 将新链接置于非阻塞模式
13 ev.events = EPOLLIN | EPOLLET; // 而且将新链接也加入EPOLL的监听队列。
14 //注意,这里的参数EPOLLIN | EPOLLET并无设置对写socket的监听,
15 //若是有写操做的话,这个时候epoll是不会返回事件的,
16 //若是要对写操做也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET
17 ev.data.fd = client;
18 if (epoll_ctl (kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)
19 {
20 /*
21 设置好event以后,将这个新的event经过epoll_ctl加入到epoll的监听队列里面,
22 这里用EPOLL_CTL_ADD来加一个新的epoll事件,经过EPOLL_CTL_DEL来减小一个epoll事件,通
23 过EPOLL_CTL_MOD来改变一个事件的监听方式.
24 */
25 fprintf (stderr, "epoll set insertion error: fd=%d", client);
26 return - 1;
27 }
28 }
29 else // 若是不是主socket的事件的话,则表明是一个用户socket的事件,
30 do_use_fd (events[n].data.fd); //则来处理这个用户socket的事情,好比说read(fd,xxx)之类的,或者一些其余的处理。
31 }

对,epoll的操做就这么简单,总共不过4个 API:epoll_create, epoll_ctl, epoll_wait和close。
若是您对epoll的效率还不太了解,请参考我 以前关于网络游戏的网络编程等相关的文章。

之前公司的服务器都是使用HTTP链接,可是这样的话,在手机目前的网络状况下不但显得速度较慢,并且不稳定。所以你们一致赞成用 SOCKET来进行链接。虽然使用SOCKET以后,对于用户的费用可能会增长(因为是用了CMNET而非CMWAP),可是,秉着用户体验至上的原则, 相信你们仍是可以接受的(但愿那些玩家月末收到账单不后可以保持克制...)。
此次的服务器设计中,最重要的一个突破,是使用了EPOLL模型, 虽然对之也是只知其一;不知其二,可是既然在各大PC网游中已经通过了如此严酷的考验,相信他不会让咱们失望,使用后的结果,确实也是表现至关不错。在这里,我仍是 主要大体介绍一下这个模型的结构。
六、Linux下EPOll编程实例
EPOLL模型彷佛只有一种格式,因此你们只要参考我下面的代码, 就可以对EPOLL有所了解了,代码的解释都已经在注释中:

C++语言: Codee#11763
01 while (TRUE)
02 {
03 int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT); //等待EPOLL时间的发生,至关于监听,
04 //至于相关的端口,须要在初始化EPOLL的时候绑定。
05 if (nfds <= 0)
06 continue;
07 m_bOnTimeChecking = FALSE;
08 G_CurTime = time ( NULL);
09 for ( int i = 0; i < nfds; i ++)
10 {
11 try
12 {
13 if (m_events[i].data.fd == m_listen_http_fd) //若是新监测到一个HTTP用户链接到绑定的HTTP端口,
14 //创建新的链接。因为咱们新采用了SOCKET链接,因此基本没用。
15 {
16 OnAcceptHttpEpoll ();
17 }
18 else if (m_events[i].data.fd == m_listen_sock_fd) //若是新监测到一个SOCKET用户链接到了绑定的SOCKET端口,
19 //创建新的链接。
20 {
21 OnAcceptSockEpoll ();
22 }
23 else if (m_events[i].events & EPOLLIN) //若是是已经链接的用户,而且收到数据,那么进行读入。
24 {
25 OnReadEpoll (i);
26 }
27
28 OnWriteEpoll (i); //查看当前的活动链接是否有须要写出的数据。
29 }
30 catch ( int)
31 {
32 PRINTF ( "CATCH捕获错误 \n ");
33 continue;
34 }
35 }
36 m_bOnTimeChecking = TRUE;
37 OnTimer (); //进行一些定时的操做,主要就是删除一些短线用户等。
38 }
其实EPOLL的精华,也就是上述的几段短短的代码,看来时代真的不一样了,之前如何接受大量用户链接的问题,如今却被如此轻松的搞定,真是让人不得不感 叹,对哪。