(应该是讲的最易懂的一篇)关于epoll的一切 ,看完这篇你就彻底懂了

欢迎交流 
QQ :2431173627 
Wechat: ccc17862701790
 

 

 

引入原因

工作原理

epoll_create(),

epoll_ctl()

epoll_wait() 

ET

LT

总结 

特点分析 

特点分析

 


 


引入原因

先可以回顾一下select和poll是怎么工作 
看一看我写的另外两篇介绍select和poll的文章
(推荐!)一个小例子彻底搞懂select函数
IO复用大揭秘------彻底搞懂poll函数
先来总结一些他们的工作流程 这两个函数的工作流程都是类似的 都类似这样

--->创建监听套接字
--->创建的套接字加入select的监管列表
--->进入while循环
--->select在有限时间内阻塞的轮询每个监听的套接字 最后返回就绪套接字的个数
--->看这些就绪的套接字里面是否有自己想要读写的套接字 如果有就调用read/write/accept读写 否则跳过
---->进入下一个while循环 重复以上步骤

存在的问题



select
(1)单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,
(2)select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
(3)在轮询期间,select需要复制大量的句柄数据结构到内核空间,产生巨大的开销;
(4)select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
(5)select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

poll
相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他缺点依然存在。


拿select模型为例,假设我们的服务器需要支持100万的并发连接,
则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。
除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。


下面将介绍"大杀器" epoll
由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在!!! 



工作原理

epoll的工作原理就我的理解可以概括为:三个函数,两种模式

三个函数是 epoll_create(), epoll_ctl(),  epoll_wait()
两种模式是  ET LT

epoll_create(),

(1)
调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,
这个结构体中有两个成员与epoll的使用方式密切相关。
eventpoll结构体如下所示:
struct eventpoll{
    ....
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
    ....
};
每一个epoll对象都有一个独立的eventpoll结构体,



(2)
对于每一个事件,都会建立一个epitem结构体,
如下所示:
struct epitem{
    struct rb_node  rbn;//红黑树节点
    struct list_head    rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep;    //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}



(3)
 int  epoll_create(int size);
    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

epoll_ctl()

1)
调用epoll_ctl向epoll对象中添加这100万个连接的套接字
用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。
这些事件都会挂载在红黑树中,
如此,重复添加的事件就可以通过红黑树而高效的识别出来
(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,
也就是说,当相应的事件发生时会调用这个回调方法。
这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
	

(2)  
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
 epoll的事件注册函数,它不同与select()是在监听事件时(epoll使用epoll_wait监听)告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是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;
    __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队列里
		

epoll_wait() 

3)调用epoll_wait收集发生的事件的连接
当调用epoll_wait检查是否有事件发生时,
只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。
如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。



(2)
 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表示已超时。
		

ET



 

LT(level triggered)是缺省的工作方式:
并且同时支持block和no-block socket.
在这种做法中,内核告诉你一个文件描述符是否就绪了,
然后你可以对这个就绪的fd进行IO操作。
如果你不作任何操作,内核还是会继续通知你的,
所以,这种模式编程出错误可能性要小一点。
传统的select/poll都是这种模型的代表.

LT

 ET (edge-triggered)是高速工作方式:
只支持no-block socket。
在这种模式下,当描述符从未就绪变为就绪时,
内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,
并且不会再为那个文件描述符发送更多的就绪通知,
直到你做了某些操作导致那个文件描述符不再为就绪状态了
(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致 了一个EWOULDBLOCK错误)。
但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),
内核不会发送更多的通知(only once)。


 

总结 

 

特点分析 

 



 

 

 


 


 

 

 


 



 

 


 

特点分析

3. 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网卡驱动架构。

 

发布了61 篇原创文章 · 获赞 17 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/vjhghjghj/article/details/100985674