wiki:html
Epoll优势;node
Epoll工做流程;linux
Epoll实现机制:编程
epollevent;缓存
Epoll源码分析;服务器
Epoll接口:网络
epoll_create;多线程
epoll_ctl;架构
epoll_close;并发
Epoll工做方式:
LT(level-triggered);
ET(edge-triggered);
Epoll应用模式;
Epoll优势:
<1>支持一个进程打开大数目的socket描述符(FD)
select一个进程所打开的FD是有必定限制的,由FD_SETSIZE设置,默认值是2048。能够选择修改这个宏而后从新编译内核,不过资料也同时指出这样会带来网络效率的降低,二是能够选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面建立进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,因此也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大能够打开文件的数目,这个数字通常远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目能够cat /proc/sys/fs/file-max察看,通常来讲这个数目和系统内存关系很大。
<2>IO效率不随FD数目增长而线性降低
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之上了。同时对于监听的fd不少,可是活跃的fd不多的状况下epoll相比select也有很高的效率。
<3>使用mmap加速内核与用户空间的消息传递。
不管是select,poll仍是epoll都须要内核把FD消息通知给用户空间,如何避免没必要要的内存拷贝就很重要,在这点上,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网 卡驱动架构。
<5>与select相比,不复用监听的文件描述集合来传递结果
这样不须要每次等待前对文件描述符集合从新赋值。
Epoll工做流程:
Epoll实现机制:
epoll fd有一个私有的struct eventpoll,它记录哪个fd注册到了epfd上。eventpoll 一样有一个等待队列,记录全部等待的线程。还有一个预备好的fd列表,这些fd能够进行读或写。相关内核实现代码fs/eventpoll.c,判断是否tcp有激活事件吗:net/ipv4/tcp.c:tcp_poll函数;
struct eventpoll {
/* Protect the access to this structure */
spinlock_t lock;
/*
* This mutex is used to ensure that files are not removed
* while epoll is using them. This is held during the event
* collection loop, the file cleanup path, the epoll file exit
* code and the ctl operations.
*/
struct mutex mtx;
/* Wait queue used by sys_epoll_wait() */
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
wait_queue_head_t poll_wait;
/* List of ready file descriptors */
struct list_head rdllist;//调用epoll_wait的时候,将readylist中的epitem出列,将触发的事件拷贝到用户空间.以后判断epitem是否需要从新添加回readylist.
/* RB tree root used to store monitored fd structs */
struct rb_root rbr;//红黑树的根,一个fd被添加到epoll中以后(EPOLL_ADD),内核会为它生成一个对应的epitem结构对象.epitem被添加到rbr中。该结构保存了epoll监视的文件描述符。
/*
* This is a single linked list that chains all the "struct epitem" that
* happened while transferring ready events to userspace w/out
* holding ->lock.
*/
struct epitem *ovflist;
/* The user that created the eventpoll descriptor */
struct user_struct *user;
};
epitem从新添加到readylist必须知足下列条件:
1) epitem上有用户关注的事件触发.
2) epitem被设置为水平触发模式(若是一个epitem被设置为边界触发则这个epitem不会被从新添加到readylist
注意,若是epitem被设置为EPOLLONESHOT模式,则当这个epitem上的事件拷贝到用户空间以后,会将
这个epitem上的关注事件清空(只是关注事件被清空,并无从epoll中删除,要删除必须对那个描述符调用
EPOLL_DEL),也就是说即便这个epitem上有触发事件,可是由于没有用户关注的事件因此不会被从新添加到
readylist中.
epitem被添加到readylist中的各类状况(当一个epitem被添加到readylist若是有线程阻塞在epoll_wait中,那
个线程会被唤醒):
1)对一个fd调用EPOLL_ADD,若是这个fd上有用户关注的激活事件,则这个fd会被添加到readylist.
2)对一个fd调用EPOLL_MOD改变关注的事件,若是新增长了一个关注事件且对应的fd上有相应的事件激活,
则这个fd会被添加到readylist.
3)当一个fd上有事件触发时(例如一个socket上有外来的数据)会调用ep_poll_callback(见eventpoll::ep_ptable_queue_proc),
若是触发的事件是用户关注的事件,则这个fd会被添加到readylist中.
了解了epoll的执行过程以后,能够回答一个在使用边界触发时常见的疑问.在一个fd被设置为边界触发的状况下,
调用read/write,如何正确的判断那个fd已经没有数据可读/再也不可写.epoll文档中的建议是直到触发EAGAIN
错误.而实际上只要你请求字节数小于read/write的返回值就能够肯定那个fd上已经没有数据可读/再也不可写.
最后用一个epollfd监听另外一个epollfd也是合法的,epoll经过调用eventpoll::ep_eventpoll_poll来判断一个
epollfd上是否有触发的事件(只能是读事件).
Epoll源码分析:
涉及linux模块的编写;
<<Epoll源码分析.doc>>
Epoll module:
static int __init eventpoll_init(void){
//模块初始化函数
}
eventpoll_init函数源码
static int __init eventpoll_init(void)
{
int error;
init_MUTEX(&epsem);
/* Initialize the structure used to perform safe poll wait head wake ups */
ep_poll_safewake_init(&psw);
/* Allocates slab cache used to allocate "struct epitem" items */
epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem),
0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,
NULL, NULL);
/* Allocates slab cache used to allocate "struct eppoll_entry" */
pwq_cache = kmem_cache_create("eventpoll_pwq",
sizeof(struct eppoll_entry), 0,
EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);
/*
* Register the virtual file system that will be the source of inodes
* for the eventpoll files
*/
/*注册了一个新的文件系统,叫"eventpollfs"(在eventpoll_fs_type结构里),而后挂载此文件系统*/
error = register_filesystem(&eventpoll_fs_type);
if (error)
goto epanic;
/* Mount the above commented virtual file system */
eventpoll_mnt = kern_mount(&eventpoll_fs_type);
error = PTR_ERR(eventpoll_mnt);
if (IS_ERR(eventpoll_mnt))
goto epanic;
DNPRINTK(3, (KERN_INFO "[%p] eventpoll: successfully initialized.\n",
current));
return 0;
epanic:
panic("eventpoll_init() failed\n");
}
epoll是个module,因此先看看module的入口eventpoll_init。这个module在初始化时注册了一个新的文件系统,叫"eventpollfs"(在eventpoll_fs_type结构里),而后挂载此文件系统。另外建立两个内核cache(在内核编程中,若是须要频繁分配小块内存,应该建立kmem_cahe来作“内存池”),分别用于存放struct epitem和eppoll_entry。
Epoll的接口:
epoll是Linux内核为处理大批句柄而做改进的poll,是Linux下多路复用IO接口select/poll的加强版本,它能显著的减小程序在大量并发链接中只有少许活跃的状况下的系统CPU利用率。由于它会复用文件描述符集合来传递结果而不是迫使开发者每次等待事件以前都必须从新准备要被侦听的文件描述符集合,另外一个缘由就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就好了。epoll除了提供select\poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减小epoll_wait/epoll_pwait的调用,提供应用程序的效率。
1.工做函数
1>.int epoll_create(int size);
建立一个epoll的句柄,size用来告诉内核这个监听的数目fd+1,每一个epoll都会占用一个fd值,能够在/proc/进程id/fd/查看。记得close()。
2>.int epoll_ctl(int epfd,int op,int fd ,struct epoll_event *event);
epoll的事件注册函数,epoll的控制函数;
这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动做,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是须要监听的fd,第四个参数是告诉内核须要监听什么事,struct epoll_event结构以下:
typedef union epoll_data { void *ptr;//数据指针 int fd;/*descriptor*/ __uint32_t u32; __uint64_t u64; } epoll_data_t;
struct epoll_event { __uint32_t events; /* Epoll events type */ epoll_data_t data; /* User data variable */ };
epoll_event->data涵盖了调用epoll_ctl增长或者修改某指定句柄时写入的信息,epoll_event->event,则包含了返回事件的位域。 |
events能够是如下几个宏的集合:
EPOLLIN :表示对应的文件描述符能够读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符能够写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来讲的。
EPOLLONESHOT:只监听一次事件,当监听完此次事件以后,若是还须要继续监听这个socket的话,须要再次把这个socket加入到EPOLL队列里
enum EPOLL_EVENTS
{
EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN
EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT
EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
EPOLLWAKEUP = 1u << 29,
#define EPOLLWAKEUP EPOLLWAKEUP
EPOLLONESHOT = 1u << 30,
#define EPOLLONESHOT EPOLLONESHOT
EPOLLET = 1u << 31
#define EPOLLET EPOLLET
};
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表示已超时。
工做方式:
LT/ET:
LT(level triggered):水平触发,缺省方式,同时支持block和no-block socket,在这种作法中,内核告诉咱们一个文件描述符是否被就绪了,若是就绪了,你就能够对这个就绪的fd进行IO操做。若是你不做任何操做,内核仍是会继续通知你的,因此,这种模式编程出错的可能性较小。传统的select\poll都是这种模型的表明。
ET(edge-triggered):边沿触发,高速工做方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪状态时,内核经过epoll告诉你。而后它会假设你知道文件描述符已经就绪,而且不会再为那个描述符发送更多的就绪通知,直到你作了某些操做致使那个文件描述符再也不为就绪状态了(好比:你在发送、接受或者接受请求,或者发送接受的数据少于必定量时致使了一个EWOULDBLOCK错误)。可是请注意,若是一直不对这个fs作IO操做(从而致使它再次变成未就绪状态),内核不会发送更多的通知。
应用模式:
那么究竟如何来使用epoll呢?其实很是简单。
经过在包含一个头文件#include <sys/epoll.h> 以及几个简单的API将能够大大的提升你的网络服务器的支持人数。
首先经过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_wait范围以后应该是一个循环,遍利全部的事件。
几乎全部的epoll程序都使用下面的框架(尤为是socket):
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd) //有新的链接;咱们能够注册多个FD,若是内核发现事件,就会载入events,若是有咱们要的描述符也就是listenfd,说明某某套接字监听描述符所对应的事件发生了变化。每次最多监测20个fd数。
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个链接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;//LT
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
}
else if( events[i].events&EPOLLIN ) //接收到数据,读socket,数据可读标志EPOLLIN
{
n = read(sockfd, line, MAXLINE)) < 0 //读
ev.data.ptr = md; //md为自定义类型,添加数据
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
}
else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
{
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
}
else
{
//其余的处理
}
}
}
来自 <http://blog.csdn.net/susubuhui/article/details/37906287>
来自 <http://www.cnblogs.com/iTsihang/archive/2013/05/23/3095775.html>