epoll源码剖析

epoll的实现主要依赖于一个虚拟文件系统:eventpollfs。epoll是个module,module的入口eventpoll_init,通过eventpoll_init初始化。在初始化的过程中,eventpollfs create两个slub分别是:epitem和eppoll_entry。
epoll使用过程中有几个基本的函数分别是epoll_create,epoll_ctl,epoll_wait。涉及到四个重要的数据结构: struct eventpoll , struct epitem, struct epoll_event ,struct eppoll_entry。

1、epoll_create和epoll_ctl

其中eventpoll是通过epoll_create生成,epoll_create传入一个size参数,size参数只要>0即可,没有任何意义。epoll_create调用函数sys_epoll_create1实现eventpoll的初始化。sys_epoll_create1通过ep_alloc生成一个eventpoll对象,并初始化eventpoll的三个等待队列,wait,poll_wait以及rdlist (ready的fd list)。同时还会初始化被监视fs的rbtree 根节点。

epollcreate在调用ep_alloc通过anon_inode_getfd创建一个名字为“[eventpoll]”的eventpollfs文件描述符号并将file->private_data指定为指向前面生成的eventpoll。这样就将eventpoll和文件id关联。最后返回文件描述符id。

通过epoll_create生成一个eventpoll后,可以通过epoll_ctl提供的相关操作对eventpoll进行ADD,MOD,DEL操作。epoll_ctl有四个参数,分别是:int epfd(需要操作的eventpoll), int op(操作类型), int fd(需要被监视的文件), struct epoll_event *event(被监视文件的相关event)。epoll_ctl首先通过epfd的private_data域获取需要操作的eventpoll,然后通过ep_find确认需要操作的fd是否已经在被监视的红黑树中(eventpoll->rbr)。然后根据op的类型分别作ADD(ep_insert),MOD(ep_modify),DEL(ep_remove)操作。

首先分析ep_insert,ep_insert有四个参数分别为: struct eventpoll *ep(需要操作的eventpoll), struct epoll_event *event(epoll_create传入的event参数,当然得从user空间拷贝过来), struct file *tfile(被监视的文件描述符), int fd(被监视的文件id)。ep_insert首先从slub中分配一个epitem的对象epi。并初始化epitem的三个list头指针,rdllink(指向eventpoll的rdlist),fllist指向(struct file的f_ep_links),pwqlist(指向包含此epitem的所有poll wait queue)。并将epitem的ep指针,指向传入的eventpoll,并通过传入参数event对ep内部变量event赋值。然后通过ep_set_ffd将目标文件和epitem关联。这样epitem本身就完成了和eventpoll以及被监视文件的关联。下面还需要做两个动作:将epitem插入目标文件的polllist并注册回调函数;将epitem插入eventpoll的rbtree。

为了完成第一个动作,还需要一个数据结构ep_pqueue帮忙,ep_pqueue主要包含两个变量一个是epitem还有一个是callback函数(ep_ptable_queue_proc)相关的一个数据结构poll_table,ep_pqueue主要完成epitem和callback函数的关联。然后通过目标文件的poll函数调用callback函数ep_ptable_queue_proc。Poll函数一般由设备驱动提供,以网络设备为例,他的poll函数为sock_poll然后根据sock类型调用不同的poll函数如:packet_poll。packet_poll在通过datagram_poll调用sock_poll_wait,最后在poll_wait实际调用callback函数(ep_ptable_queue_proc)。

ep_ptable_queue_proc函数完成epitem加入到特定文件的wait队列任务。ep_ptable_queue_proc有三个参数:struct file *file(目标文件), wait_queue_head_t *whead(目标文件的waitlist), poll_table*pt(前面生成的poll_table)。在函数中,引入了另外一个非常重要的数据结构eppoll_entry。eppoll_entry主要完成epitem和epitem事件发生时的callback(ep_poll_callback)函数之间的关联,并将上述两个数据结构包装成一个链表节点,挂载到目标文件file的waithead中。

这里还得完成两个动作,首先将eppoll_entry的whead指向目标文件的waitlist(传入的参数2),然后初始化base变量指向epitem,最后通过add_wait_queue将epoll_entry挂载到目标文件的waitlist。完成这个动作后,epoll_entry已经被挂载到waitlist,然后还有一个动作必须完成,就是将eppoll_entry挂载到epitem的pwqlist上面。现在还剩下一个动作,就是将epitem的fllink链接到目标文件的f_ep_links上,这部分工作将在poll函数返回后在ep_insert中完成。当然ep_insert除了完成这个动作外,还会完成前面提到的第二步,epitem插入eventpoll的rbtree。完成以上动作后,将还会判断当前插入的event是否刚好发生,如果是,那么做一个ready动作,将epitem加入到rdlist中,并对epoll上的wait队列调用wakeup。

2、epoll_wait

epoll_wait有四个参数int epfd(被wait的epoll所关联的epollfs的fd), struct epoll_event __user * events(返回监视到的事件), int maxevents(每次return的events最大值), int timeout(最大wait时间)。epoll_wait首先会检测传入参数的合法性,包括maxevents有没有超过范围events指向的空间是否可写;epfd是否合法等。参数合法性检测都通过后,将通过epfd获取锁依赖的structfile,然后通过file->private_data获取eventpoll。获取epoll后调用ep_poll函数完成真正的epoll_wait工作。ep_poll函数也是四个参数和epoll_wait唯一的差别就是第一参数是前面获取的eventpoll指针。ep_poll首先根据timeout的值判断是否是无限等待,如果不是将timeout(ms)转换为jiffs。然后判断eventpoll的rdlist是否为空,如果为空,那么将current进程通过一个waitquene entry加入eventpoll的waitlist(wq)。并将task的状态改为TASK_INTERRUPTIBLE;并通过schedule_timeout让出处理器。如果rdlist非空,那么通过ep_send_events将event转发到userspace。

ep_send_events通过ep_scan_ready_list对ready_list进行扫描,由于现在在对ready_list进行操作,这个时候必须保证rdlist数据的一致性,如果此时又有新的event ready,那么我们必须提供临时的存储空间,eventpoll提供了一个ovflist用来存储这种event。ep_send_events获取了rdlist后通过ep_send_events_proc完成真正的转发工作。完成转发后,ep_send_events还需要去判断ovflist,如果ovflist中有events,那么还需要将这些events转移到rdlist中。
ep_send_events_proc扫描rdlist从头上面拿出epitem,然后调用epollfs的poll函数(ep_eventpoll_poll),判断拿出来的那个events是否真的已经ready(这部分比较难理解,没怎么看懂)。如果ready,那么将数据封装到uevent里面,同事这里还需要判断epitem的类型是否是Level Triggered如果是,那么还需要把event再次插入队列尾部。

3、ep_poll_callback

以上描述中还缺少关键的一环,就是如何在被监视文件发生event的时候,如何将epitem加入rdlist并唤醒调用epoll_wait进程。这个工作由ep_poll_callback函数完成。前面提到eppoll_entry完成一个epitem和ep_poll_callback的关联,同时eppoll_entry会被插入目标文件file的(private_data)waithead中。以scoket为例,当socket数据ready,中断会调用相应的接口函数比如rawv6_rcv_skb,此函数会调用sock_def_readable然后,通过sk_has_sleeper判断sk_sleep上是否有等待的进程,如果有那么通过wake_up_interruptible_sync_poll函数调用ep_poll_callback。

ep_poll_callback函数首先会判断是否rdlist正在被使用(通过ovflist是否等于EP_UNACTIVE_PTR),如果是那么将epitem插入ovflist。如果不是那么将epitem插入rdlist。然后调用wake_up函数唤醒epitem上wq的进程。这样就可以返回到epoll_wait的调用者,将他唤醒。

4.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函数,当设备就绪,唤醒等待队列上的等待着时,ep_poll_callback就会被调用。每次调用poll系统调用,操作系统都要把current(当前进程)挂到fd对应的所有设备的等待队列上,可以想象,fd多到上千的时候,这样“挂”法很费事;而每次调用epoll_wait则没有这么罗嗦,epoll只在epoll_ctl时把current挂一遍(这第一遍是免不了的)并给每个fd一个命令“好了就调回调函数”,如果设备有事件了,通过回调函数,会把fd放入rdllist,而每次调用epoll_wait就只是收集rdllist里的fd就可以了——epoll巧妙的利用回调函数,实现了更高效的事件驱动模型。

3.使用mmap加速内核与用户空间的消息传递。(错误的)

我见好博客上都说了使用了mmap的零拷贝,但我并没有在源码中发现使用mmap,仔细想想内核确实不可能使用mmap。在用户角度看使用mmap确实可以提高效率。但是有个最大的安全问题就是,内核不可能把内核地址空间肯定不会随便暴露到用户空间,要经过一系列的安全检查,不然很容易被hack。
关于mmap
常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。
总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此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网卡驱动架构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值