epoll服务器开发详解

epoll基础知识:

epoll比select poll最大的好处在于它不会随着监听fd数目的增长而降低效率,它属于异步I/O方式。

epoll的接口非常简单,一共就三个函数:
1. int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll
的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fdepfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events
可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
4、关于ETLT两种工作模式:
可以得出这样的结论
:
ET
模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;LT模式是只要有数据没有处理就会一直通知下去的.

网上流传的epoll解决方案:http://blog.163.com/pandalove@126/blog/static/9800324520122633515612/

【寻找办法】

看了网上N多帖子和网页,阅读多款优秀开源程序的源代码,再结合自己的实验测试,总结如下:

1.实际情况中,在发生惊群时,并非全部子进程都会被唤醒,而是一部分子进程被唤醒。但被唤醒的进程仍然只有1个成功accept,其他皆失败。

2.所有基于linux epoll机制的服务器程序在多进程时都受惊群问题的困扰,包括 lighttpd nginx 等程序,各家程序的处理办法也不一样。

3.lighttpd的解决思路:无视惊群。采用Watcher/Workers模式,具体措施有优化fork()epoll_create()的位置(让每个子进程自己去epoll_create()epoll_wait()),捕获accept()抛出来的错误并忽视等。这样子一来,当有新accept时仍将有多个lighttpd子进程被唤醒。

4.nginx的解决思路:避免惊群。具体措施有使用全局互斥锁,每个子进程在epoll_wait()之前先去申请锁,申请到则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量。

5一款国内的优秀商业MTA服务器程序(不便透露名称):采用Leader/Followers线程模式,各个线程地位平等,轮流做Leader来响应请求。

对比lighttpdnginx两套方案,前者实现方便,逻辑简单,但那部分无谓的进程唤醒带来的资源浪费的代价如何仍待商榷(有网友测试认为这部分开销不大 http://www.iteye.com/topic/382107)。后者逻辑较复杂,引入互斥锁和负载均衡算分也带来了更多的程序开销。所以这两款程序在解决问题的同时,都有其他一部分计算开销,只是哪一个开销更大,未有数据对比。

坊间也流传Linux 2.6.x之后的内核,就已经解决了accept的惊群问题,论文地址 http://static.usenix.org/event/usenix2000/freenix/full_papers/molloy/molloy.pdf

但其实不然,这篇论文里提到的改进并未能彻底解决实际生产环境中的惊群问题,因为大多数多进程服务器程序都是在fork()之后,再对epoll_wait(listen_fd,...)的事件,这样子当listen_fd有新的accept请求时,进程们还是会被唤醒。论文的改进主要是在内核级别让accept()成为原子操作,避免被多个进程都调用了。 

【采用方案】

多方考量,最后选择参考lighttpdWatcher/Workers模型,实现了我需要的那款多进程epoll程序,核心流程如下:

1.主进程先监听端口, listen_fd = socket(...); setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,...)setnonblocking(listen_fd)listen(listen_fd,...)

2.开始fork(),到达子进程数上限(建议根据服务器实际的CPU核数来配置)后,主进程变成一个Watcher,只做子进程维护和信号处理等全局性工作。

3.每一个子进程(Worker)中,都创建属于自己的epollepoll_fd = epoll_create(...);,接着将listen_fd加入epoll_fd中,然后进入大循环,epoll_wait()等待并处理事件。千万注意, epoll_create()这一步一定要在fork()之后。

4.大胆设想(未实现):每个Worker进程采用多线程方式来提高大循环的socket fd处理速度,必要时考虑加入互斥锁来做同步,但也担心这样子得不偿失(进程+线程频繁切换带来的额外操作系统开销),这一步尚未实现和测试,但看到nginx源码中貌似有此逻辑。

 

根据上面那个作者的想法,我在linux平台测试了下,发现

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值