论epoll的使用 - 高调coding,低调做人 - C++博客
peakflys
论epoll的使用
前几天回答一个问题,是关于我们项目中使用的epoll模式的,因为记不大清了,感觉应该使用的就是epoll的高速模式,也就是ET(edge-trigger)模式。这两天闲暇的时候,打开代码又看了一下,在epoll事件注册时并未标记ET模式,看来实际使用的是epoll默认的LT(level-trigger )模式,为什么呢?使用LT意味着 只要 fd 处于 readable/writable 状态,每次 epoll_wait 时都会返回该 fd,系统开销不说,自己处理时每次都要把这些fd轮询一遍,如果fd很多的话,不管这些fd有没有事件发生,epoll_wait 都会触发这些fd的轮询判断。
查阅了一些资料,才知道常用的事件处理库很多都选择了 LT 模式,包括大家熟知的libevent和boost::asio等,为什么选择LT呢?那就不得不从ET的弊端的弊端说起。
ET模式下,当有事件发生时,系统只会通知你一次,也就是调用epoll_wait 返回fd后,不管事件你处理与否,或者处理完全与否,再调用epoll_wait 时,都不会再返回该fd,这样programmer要自己保证在事件发生时及时有效的处理完。比如此时fd发生了EPOLLIN事件,在调用epoll_wait 后发现此事件,programmer要保证在本次轮询中对此fd进行了读操作,并且还要循环调用recv操作,一直读到recv的返回值小于请求值,或者遇到EAGAIN错误,不然下次轮询时,如果此fd没有再次触发事件,你就没有机会知道这个fd需要你的处理。这样无形中就增加了programmer的负担和出错的机会。
ET模式的短处正是LT模式的长处,无论此fd是否有事件发生,或者有事件未处理完,每次epoll_wait 时总会得到此fd供你处理。显而易见,OS在LT模式下维护的 ready list 的大小肯定比ET模式下长,而且你自己轮询所有的fd时也要比ET下要多,这种消耗和ET模式下循环调用处理函数(如recv和send等),还要逻辑处理是否处理完毕,理论上应该是LT更大一些,不过个人感觉应该差别不会太大。但是LT模式下带来的逻辑处理的方便性和不易出错性,让我们有理由把它作为首选。我想这可能也是为什么epoll后来在ET的基础上又增加了LT,并且将其作为默认模式的原因吧。
peakflys 上述观点,欢迎 志同道合或志同道不合的朋友拍砖。
PS:文中一味写LT的好处,没有说LT 极易引起的写触发 频繁通知的问题,具体大家可以参考评论部分,再次感谢大家的指教。
posted on 2012-08-26 18:33 peakflys 阅读(1948) 评论(14) 编辑 收藏 引用 所属分类: 服务器
评论
# re: 论epoll的使用[未登录] 2012-08-27 10:33 春秋十二月
LT模式下,只要空间可写,则写事件不断被触发,CPU占用较高,如果不转为ET模式,怎么解决这一问题? 回复 更多评论
# re: 论epoll的使用[未登录] 2012-08-27 16:36 春秋十二月
@peakflys
fd写事件被触发,是因为sock底层缓冲区有大于某个阈值的空闲空间,和应用层有无数据待写没有关系吧 回复 更多评论
# re: 论epoll的使用 2012-08-28 10:51 peakflys
@春秋十二月
谢谢春秋仁兄的指教,我是这么认为的。send buffer不满时触发的写事件,应该不至于引起CPU的占用过高(OS里本身也有很多纳秒级的死循环),如果过高说明轮询时的处理函数太耗CPU了,应该是可以优化的,另外轮询时间也可以设置的长一些,当然有些应用需要这么准确、及时。如果这样的话,我认为可以这样改进:在一次网络主循环里调用两次epoll_wait,第一次是及时的(例如1ms)用于处理读和错误事件,第二次是稍微长的(例如30~50ms,视情况定)用于处理读、写等事件。为了达到这种效果,我们可以 封装两种send方式,一种是使用epoll触发的写,另外一种是紧急的立即写(当然写时可以调用poll等检测一下是否可写)。这样效率应该跟得上了,复杂度和出错成都也没有ET模式高。 回复 更多评论
# re: 论epoll的使用 2012-08-29 17:07 唐诗
楼主没有说到重点,需要注意的是写事件。
a. 对于et来说,应用层向tcp缓冲区写,有可能应用层数据写完了,但是tcp缓冲没有写到EAGAIN事件,那么此时需要在应用层做个标记,表明tcp缓冲区是可写的,否则,由于et是只触发一次,应用层就再也不会被通知缓冲区可写了。
b. 对于lt来说,应用层确实会每次通知可写事件,问题在于,如果应用层没数据需要往Tcp缓冲区写的话,epoll还是会不停的通知你可写,这时候需要把描述符移出epoll,避免多次无效的通知
http://www.cnblogs.com/egametang/archive/2012/07/30/2615808.html 回复 更多评论
# re: 论epoll的使用 2012-08-29 17:08 唐诗
事实上et要比lt简单的多 回复 更多评论
# re: 论epoll的使用 2012-08-30 07:34 peakflys
谢谢唐诗的回复和指教,你指出的
a问题:在文中写ET模式时已经说了一部分,不过没有说写事件处理完之后,send buffer仍然可写时怎么处理,因为这个本身就是我认为的ET模式弊端之一,因为挺麻烦也挺易出错。
b问题:当时写这篇短文时确实没有特别考虑,不过在评论里面 春秋十二月仁兄指出了这个问题,唐诗兄给出的方法是每次把数据写完之后把它移出epoll监听队列,以后有新的写数据时再加入写事件到队列,不过个人感觉这种方法不是很理想,除了自己写着难受之外,因为从2.6.10内核之后 epoll内部队列的数据结构变成了RB_TREE,游戏中写数据很频繁(尤其是大规模玩家在线时),这样频繁的调整RB_TREE,性能损耗应该会不小。我在给春秋十二月仁兄的回复中给出了我的大致解决方法(参看上面评论),有什么不完整或者不对的地方,还请唐诗兄指教或者邮件交流 peakflys@gmail.com
至于唐诗兄说的et要比lt简单,这个可能是用et用的多了,很多细节错误已经有了自己固定成熟的解决方案了才说出这样的结论。ET如果保证每次触发的事件都可以及时有效的处理完全(当然 个人认为不容易,有时候还要自己处理一些本该TCP处理的东西)ET模式还是可以作为首选的,否则会表现出通讯过程中应用上层各种诡异的问题…… @唐诗 回复 更多评论
# re: 论epoll的使用 2012-08-30 11:09 唐诗
正因为觉得移出epoll队列不好,但是不移除也不好,所以et是比较好的方式
代码其实相当简单。
write_list_是应用层缓冲区,在epoll写事件来的时候,应用层缓冲区为空的话
设置socket 可写。下次往应用层缓冲区写数据时,检查socket是否可写,如果可写则调用HandleWrite即可。缓冲区写满的时候设置socket不可写就行了。
HandleWrite有两个调用途径,一个是写事件触发,一个是应用层触发(socket有is_writable标记)。
void HandleWrite()
while (true) {
// 应用层缓冲区全部写到TCP缓冲区了, 此时TCP缓冲区还是可写
// et模式下不会再通知应用层, 所以设置下socket writable状态
// 下次应用层数据来的时候检查该状态
if (write_list_.TotalSize() == 0) {
socket_.set_is_writable(true);
return;
}
int n = write(fd, write_list_.ReadPoint(), write_list_.readable_size());
const int error_no = errno;
if (n == -1) { // 写异常
if (error_no == EINTR) {
continue;
}
// 缓冲区已写满, 需要等写事件
if (error_no == EAGAIN) {
socket_.set_is_writable(false);
return;
} else {
HandleError(error_no);
return;
}
} else { // 写正常
write_list_.ReadAdvance(n);
}
} 回复 更多评论
# re: 论epoll的使用 2012-08-30 14:11 peakflys
恩,唐诗兄在socket上加入标记位的办法是可以很好解决ET模式的写问题(上述代码中唐诗兄应该加上write之后 0==n 的情况,及时断掉正常中断的socket,而不是认为写正常,马上调整发送缓存)
谢谢唐诗兄的指教,不过如果使用LT模式,唐诗兄会发现更简单,呵呵。不知道你们一个网络主线程挂载多少socket? @唐诗
回复 更多评论
# re: 论epoll的使用 2012-08-30 14:53 唐诗
@peakflys
单线程,压力测试流量很大,3K都没有问题,这时已经受到带宽限制了。流量小点10K都可以。我们设计只需要1K连接就行了,不够可以加多个网关服,所以绰绰有余了 回复 更多评论
# re: 论epoll的使用 2012-08-30 15:48 peakflys
恩,看来服务器性能还不错,我们单网关设计是5K连接,不过使用的是线程池,单个线程挂512个socket,在加上网络主循环有相应优化,所以LT模式影响不大,留给上层逻辑很大的扩展空间 @唐诗
回复 更多评论
# re: 论epoll的使用[未登录] 2012-08-31 01:04 春秋十二月
唐工的方法,和我实践中基本是一样的,但没有加标记,而是直接发,如果碰到EAGAIN,则入队;如果发了一部分,则剩下的部分入队,留在下次写事件中发。
简言之,ET模式的读写,需要不断读或写直到遇到EAGAIN或出错,也就是达到边缘状态(空间空或满),如果后来空间非空或非满(原因是网络收到数据或写出数据),则读或写事件,就被触发一次了。 回复 更多评论
# re: 论epoll的使用 2012-09-01 15:49 zuhd
一直用的是LT,最大链接4K,并且没有把 “没有写需求的socket”移出epoll,目前也没有发现效率问题。在思考是轮询的代价大还是移除的代价大? 回复 更多评论
# re: 论epoll的使用 2012-09-02 11:36 peakflys
@zuhd
移出的代价必然大于轮询的代价,但是如果LT模式不做写事件优化的话,是在一定程度上影响效率的(影响的程度和挂载的socket数量有关),这种影响首先表现在轮询的次数上,其次(也应该说主要)是你的发送函数上调用上,因为不管有没有消息需要发送,只要send buffer不满,写事件都会触发,你所封装的发送函数都会调用。 回复 更多评论