全部博文(142)
分类: LINUX
2011-10-15 16:39:39
[EYUALUO]epoll技术本人在许多年前使用过,但是一直没有总结归纳。虽然网上epoll的文章数不胜数,为了尊重作者的辛苦劳动还是把版权属上。重新排了一下版,废话少说,开讲。
==========================================================================================
1、为什么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而已。
2、2.6内核中提高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)
以下文章转自滕昱的Web Log http://mechgouki.spaces.msn.com/blog/PersonalSpace.aspx
/*********************************引用开始******************************/
Linux2.6内核epoll介绍---我的blog 2005/3/30
[作者]:滕昱,2005/3/30,0.1版本
[版权声明]:此文档遵循GNU自由文档许可证(GNU Free Documentation License).任何人可以自由复制,分发,修改,不过如果方便,请注明出处和作者:)
(1)导言:
首先,我强烈建议大家阅读Richard Stevens著作《TCP/IP Illustracted Volume 1,2,3》和《UNIX Network Programming Volume 1,2》。虽然他离开我们大家已经5年多了,但是他的书依然是进入网络编程的最直接的道路。其中的3卷的《TCP/IP Illustracted》卷1是必读-如果你不了解tcp协议各个选项的详细定义,你就失去了优化程序重要的一个手段。卷2,3可以选读一下。比如卷2 讲解的是4.4BSD内核TCP/IP协议栈实现----这个版本的协议栈几乎影响了现在所有的主流os,但是因为年代久远,内容不一定那么vogue. 在这里我多推荐一本《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,reltime signal等等。最终,Davide Libenzi开发的epoll进入2.6内核成为正式的解决方案
(2)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网卡驱动架构。
(3)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个系统调用,具体用法请参考 ,
在也有一个完整的例子,大家一看就知道如何使用了
(4)Leader/follower模式线程pool实现,以及和epoll的配合
.....未完成,主要是要避免过多的epoll_ctl调用,以及尝试使用EPOLLONESHOT加速......
(5)benchmark
.......未完成
/*********************************引用结束******************************/
3、epoll的使用方法
这是epoll的man手册提供的一个例子,这段代码假设一个非阻塞的socket监听listener被建立并且一个epoll句柄kdpfd已经提前用epoll_create建立了:
struct epoll_event ev, *events; for(;;) { for(n = 0; n < nfds; ++n) { |
4、epoll使用方法示意代码
以下代码由chinaunix.net上BBS用户safedead()提供:
static int s_epfd; //epoll描述字 {//初始化epoll //设置epoll {//这个过程可以循环以便加入多个LISTEN套接字进入epoll事件集合 //加入epoll事件集合 {//epoll事件处理 |
对照safedead和前面的一份代码,我想大家一定是明白了的。
5、参考文档
Improving (network) I/O performance ...
别人的概括
1)epoll返回时已经明确的知道哪个sokcet fd发生了事件,不用再一个个比对。这样就提高了效率。
2)select的FD_SETSIZE是有限止的,而epoll是没有限止的只与系统资源有关。
3)单个epoll并不能解决所有问题,特别是你的每个操作都比较费时的时候,因为epoll是串行处理的。 所以你有还是必要建立线程池来发挥更大的效能。
4)如果下一个循环你还要关注这个socket fd的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型,这一步非常重要。
======================================================================
理解epoll()的实现
epoll_create() 系统调用
首先,我们注意epoll_create()返回的是一个文件描述符,这是epoll()与select()/poll()族系
统调用的一点显著不同。有文件描述符就一定有对应的文件系统,这个特殊文件系统,在RHEL5.4内核
(主要基于2.6.18,混合了部分2.6.2x的代码)里叫eventpollfs。不过,这个用来借壳“充数”的文件
系统在更新的内核里已经看不到了,因为新内核(至少2.6.32以后是没有了)提供了更轻量级的方法实现
“借壳”。
“借壳”本身与epoll()的核心逻辑关系不大,我们略过不表。无论有没有这个特殊文件系统,这个打
开文件的对应的file_operations都实现了两个方法:一个是release,一个poll。前一个我不用介绍
了,从名字上猜含义不难。后一个就比较有趣了,这意味着epoll_create()返回文件描述符本身也是可以
select()/poll()/epoll()的,我们可以复合epoll!
epoll_create()创建完对应的inode()之后,就是做一些常规的VFS粘合代码,诸如将filp与
fd关联,不说了。
另外,值得一提的,就是那个size参数,其实是棒锤,什么也不顶。
epoll_ctl() 系统调用
前面做参数合法检查和退出时打扫战场的代码我就忽略了。这个函数的骨干逻辑如下:
epi = ep_find(ep, tfile, fd);
error = -EINVAL;
switch (op) {
case EPOLL_CTL_ADD:
if (!epi) {
epds.events |= POLLERR | POLLHUP;
error = ep_insert(ep, &epds, tfile, fd);
} else
error = -EEXIST;
break;
case EPOLL_CTL_DEL:
if (epi)
error = ep_remove(ep, epi);
else
error = -ENOENT;
break;
case EPOLL_CTL_MOD:
if (epi) {
epds.events |= POLLERR | POLLHUP;
error = ep_modify(ep, epi, &epds);
} else
error = -ENOENT;
break;
}
这是ep_find()调用的唯一地方,它的作用是在在一棵红黑树查找满足已经添加过的“事件结构”,
毫无疑问,这是为DEL和MOD操作的预备动作。而我们将主要关心ADD,所以忽略它吧。此外一个值
得注意的地方,从以上代码可以看到,对于ADD/MOD操作, POLLERR | POLLHUP都是必定
会通知用户空间程序的,即使你不指定它们。
OK,对于ADD操作,就剩下ep_insert(ep, &epds, tfile, fd) 需要解释了。这个函数首先
分配一个epi(event poll item)结构,然后初始化上面的几个链表什么的,没什么大不了是不是?但
最关键的代码就隐藏在这个函数里面,其中最重要的代码是这么几行:
struct ep_pqueue epq;
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
revents = tfile->f_op->poll(tfile, &epq.pt);
(当然,这个函数也会把epi插入到之前提到的红黑树中。)
init_poll_funcptr()倒是很简单:只有一行代码,它的意思其实就是:
epq.pt.qproc = ep_ptable_queue_proc;
稍微解释一下这里的数据结构, ep_pqueue只是用来粘合epoll()与Linux 多路复用
(multiplexing)机制的一个脚手架,epq.epi是脚本架的epoll()这一端,epq.pt是Linux 多路复
用(multiplexing)机制的另一端。 ep_ptable_queue_proc,是一个函数,稍后介绍。tfile,是
要需要监听的那个fd的对应的filp(内核里描述打开文件的结构体)。这个结构体内的f_op->poll()方
法是实现Linux 多路复用(multiplexing)机制的关键点。因为监听的文件可能是socket,可能是一
个裸设备,也可能是某个文件,所以,只要想支持多路复用,协议栈、设备驱动、文件系统都必须支持这个
方法。以TCP为例,它的poll实现是tcp_poll函数:
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
unsigned int mask;
struct sock *sk = sock->sk;
struct tcp_sock *tp = tcp_sk(sk);
poll_wait(file, sk->sk_sleep, wait);
if (sk->sk_state == TCP_LISTEN)
return inet_csk_listen_poll(sk);
/* …... */
if ((tp->rcv_nxt != tp->copied_seq) &&
(tp->urg_seq != tp->copied_seq ||
tp->rcv_nxt != tp->copied_seq + 1 ||
sock_flag(sk, SOCK_URGINLINE) || !tp->urg_data))
mask |= POLLIN | POLLRDNORM;
/* …... */
return mask;
}
你可能注意到了,上面的tcp_poll()与tfile->f_op->poll()的参数不同,这是因为tcp_poll()
是通过socket层包装过的,只是参数不同,但本质没有区别。这个函数的代码按功能分成两部分,前面的
poll_wait()用于支持多路复用,file和wait分别是前面的tfile和&epq.pt,sk->sk_sleep是
TCP的等待队列,如果TCP有需要通知的事件(例如收到数据包了)就通知在这个等待队列上面的等待者;
后面是TCP自己判断是否产生了有效事件(POLLIN等)的逻辑。poll_wait()的逻辑也是非常简单的,
相当于:
if (&epq.pt && sk->sk_sleep)
&epq.qt->qproc(tfile, sk->sk_sleep, &epq.qt);
也就是说,会调用 ep_ptable_queue_proc(),这个函数做了什么呢?去掉错误处理:
pwq = kmem_cache_alloc(pwq_cache, SLAB_KERNEL))) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead;
pwq->base = epi;
add_wait_queue(whead, &pwq->wait);
这是啥意思?其实意图是很简单的,向sk->sk_sleep这个等待队列里放置一个“等待者”。通常意
义上的唤醒等待者,会唤醒对应的某个任务,然后在下次发生中断时进行任务切换。但这里的唤醒只会导致
运行 ep_poll_callback,这个函数的核心代码是:
list_add_tail(&epi->rdllink, &ep->rdllist);
if (waitqueue_active(&ep->wq))
__wake_up_locked(&ep->wq, TASK_UNINTERRUPTIBLE |
TASK_INTERRUPTIBLE);
首先把这个epi(event poll item)添加到这个事件池的“Ready Event Linked List”的尾
部中,然后唤醒等待在这个事件池的任务(如果有的话),也就是调用了epoll_wait()的用户空间线程。
epoll_wait() 系统调用
明白了上述过程,epoll_wait()就格外简单了。首先,它让当前线程休眠在等待队列 ep->wq上。
直到因为”Ready Event Linked List“里面有数据有个唤醒它。然后,它会调用
ep_events_transfer(ep, events, maxevents)将等到的按顺序事件复制到用户指定的参数中。这
就不解释其余的代码了。
其他epoll_XXX() 系统调用
在RHEL6的内核(主要基于2.6.32)里,还有两个与epoll有关的系统调用:
epoll_pwait(),epoll_create1()。这些只是上面三个系统调用的包装而已。pwait是做了与进程信
号有关的工作,create1则是用来补漏的。
epoll over epoll
上面讲到epoll_create()返回的本身就是文件描述符,而eventpollfs则是一个支持多路复用的
特殊文件系统。这样,我们就是可以对epoll_create()的结果做epoll了。为了避免可能的无穷递归,
epoll的实现对此也做了相应的处理。这就是ep->poll_wait成员和ep_poll_safewake()函数的用
意所在了,这块的代码就不粘了,点到即止吧。
OK,为什么要epoll over epoll呢?试想我们有几千个socket需要处理时,而我们不希望平
等对待这几千个socket,要将其分成128个优先级。上面已经看到,对于一个epoll文件
(epoll_create()返回的那个fd)中的事件,内核是平等对待的,就是一个简单的FIFO队列。使用复
合epoll,就可以快速地分辨出哪些优先级上的socket有数据包来临,而不需要逐个判断每个就绪连接的
优先级了,从而可以优先处理那高优先级的socket,嗯,再改吧改吧,似乎是个很好的QoS机制的基础。