LINUX编程专题-I/O复用:epoll原理和源码解析(资料的收集和整理,不算原创)
1.什么是epoll和epoll的用法
此问题普通linux书籍都有介绍,随便找一本看看即可,也可直接man查看,此篇文章的重点不在于此
epoll接口介绍可以参见我的另一篇博文
epoll接口介绍
2.poll机制解析(了解poll是为了和epoll做比较)
参见我的另一篇博文poll机制详解
3.epoll原理讲解(重点部分,以通俗易懂的方式讲解了原理,此部分为转载的,具体出处不明)
开发高性能网络程序时,windows开发者们言必称iocp,Linux开发者们则言必称epoll。大家都明白epoll是一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄,比起以前的select和poll效率高大发了。我们用起epoll来都感觉挺爽,确实快,那么,它到底为什么可以高速处理这么多并发连接呢?
先简单回顾下如何使用C库封装的3个epoll系统调用吧。
- int epoll_create(int size);
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
使用起来很清晰,首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。
epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。
epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。
从上面的调用方式就可以看到epoll比select/poll的优越之处:因为后者每次调用时都要传递你所要监控的所有socket给select/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
- static int __init eventpoll_init(void)
- {
- ... ...
- /* 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);
- ... ...
epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!
那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。
这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。
4.代码解析(以下代码转载自http://www.cnblogs.com/sduzh/p/6793879.html)
二、epoll_create()
系统调用epoll_create()会创建一个epoll实例并返回该实例对应的文件描述符fd。在内核中,每个epoll实例会和一个struct eventpoll类型的对象一一对应,该对象是epoll的核心,其声明在fs/eventpoll.c文件中.
epoll_create的接口定义在这里,主要源码分析如下:
首先创建一个struct eventpoll对象:
1
2
3
4
5
|
struct
eventpoll *ep = NULL;
... error = ep_alloc(&ep); if
(error < 0)
return
error;
|
然后分配一个未使用的文件描述符:
1
2
3
4
5
|
fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC)); if
(fd < 0) {
error = fd;
goto
out_free_ep;
} |
然后创建一个struct file对象,将file中的struct file_operations *f_op设置为全局变量eventpoll_fops,将void *private指向刚创建的eventpoll对象ep:
1
2
3
4
5
6
7
|
struct
file *file;
... file = anon_inode_getfile( "[eventpoll]" , &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
if
(IS_ERR(file)) {
error = PTR_ERR(file);
goto
out_free_fd;
} |
然后设置eventpoll中的file指针:
1
|
ep->file = file; |
最后将文件描述符添加到当前进程的文件描述符表中,并返回给用户
1
|
fd_install(fd, file);<br> return
fd;
|
操作结束后主要结构关系如下图:
三、epoll_ctl()
系统调用epoll_ctl()在内核中的定义如下,各个参数的含义可参见epoll_ctl的man手册
1
|
SYSCALL_DEFINE4(epoll_ctl,
int , epfd,
int , op, int , fd,
struct epoll_event __user *, event)
|
epoll_ctl()首先判断op是不是删除操作,如果不是则将event参数从用户空间拷贝到内核中:
1
2
3
4
5
|
struct
epoll_event epds;
... if
(ep_op_has_event(op) &&
copy_from_user(&epds, event,
sizeof ( struct
epoll_event)))
goto
error_return;
|
ep_op_has_event()实际就是判断op是不是删除操作:
1
2
3
4
|
static
inline int
ep_op_has_event( int
op)
{ return
op != EPOLL_CTL_DEL;
} |
接下来判断用户是否设置了EPOLLEXCLUSIVE标志,这个标志是4.5版本内核才有的,主要是为了解决同一个文件描述符同时被添加到多个epoll实例中造成的“惊群”问题,详细描述可以看这里。 这个标志的设置有一些限制条件,比如只能是在EPOLL_CTL_ADD操作中设置,而且对应的文件描述符本身不能是一个epoll实例,下面代码就是对这些限制的检查:
1
2
3
4
5
6
7
8
9
10
11
12
|
/* *epoll adds to the wakeup queue at EPOLL_CTL_ADD time only,
* so EPOLLEXCLUSIVE is not allowed for a EPOLL_CTL_MOD operation.
* Also, we do not currently supported nested exclusive wakeups.
*/
if
(epds.events & EPOLLEXCLUSIVE) {
if
(op == EPOLL_CTL_MOD)
goto
error_tgt_fput;
if
(op == EPOLL_CTL_ADD && (is_file_epoll(tf.file) ||
(epds.events & ~EPOLLEXCLUSIVE_OK_BITS)))
goto
error_tgt_fput;
} |
接下来从传入的文件描述符开始,一步步获得struct file对象,再从struct file中的private_data字段获得struct eventpoll对象:
1
2
3
4
5
6
7
8
|
struct
fd f, tf;
struct
eventpoll *ep;
... f = fdget(epfd);
... tf = fdget(fd);
... ep = f.file->private_data; |
如果要添加的文件描述符本身也代表一个epoll实例,那么有可能会造成死循环,内核对此情况做了检查,如果存在死循环则返回错误。这部分的代码目前我还没细看,这里不再贴出。
接下来会从epoll实例的红黑树里寻找和被监控文件对应的epollitem对象,如果不存在,也就是之前没有添加过该文件,返回的会是NULL。
1
2
3
|
struct
epitem *epi;
... epi = ep_find(ep, tf.file, fd); |
ep_find()函数本质是一个红黑树查找过程,红黑树查找和插入使用的比较函数是ep_cmp_ffd(),先比较struct file对象的地址大小,相同的话再比较文件描述符大小。struct file对象地址相同的一种情况是通过dup()系统调用将不同的文件描述符指向同一个struct file对象。
1
2
3
4
5
|
static
inline int
ep_cmp_ffd( struct
epoll_filefd *p1, <br>
struct epoll_filefd *p2)
{ return
(p1->file > p2->file ? +1:
(p1->file < p2->file ? -1 : p1->fd - p2->fd));
} |
接下来会根据操作符op的不同做不同的处理,这里我们只看op等于EPOLL_CTL_ADD时的添加操作。首先会判断上一步操作中返回的epollitem对象地址是否为NULL,不是NULL说明该文件已经添加过了,返回错误,否则调用ep_insert()函数进行真正的添加操作。在添加文件之前内核会自动为该文件增加POLLERR和POLLHUP事件。
1
2
3
4
5
6
7
|
if
(!epi) {
epds.events |= POLLERR | POLLHUP;
error = ep_insert(ep, &epds, tf.file, fd, full_check);
} else
error = -EEXIST;
if
(full_check)
clear_tfile_check_list();
|
ep_insert()返回之后会判断full_check标志,该标志和上文提到的死循环检测相关,这里也略去。
四、ep_insert()
ep_insert()函数中,首先判断epoll实例中监视的文件数量是否已超过限制,没问题则为待添加的文件创建一个epollitem对象:
1
2
3
4
5
6
7
8
9
10
11
|
int
error, revents, pwake = 0;
unsigned long
flags;
long
user_watches;
struct
epitem *epi;
struct
ep_pqueue epq;
user_watches = atomic_long_read(&ep->user->epoll_watches); if
(unlikely(user_watches >= max_user_watches))
return
-ENOSPC;
if
(!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
return
-ENOMEM;
|
接下来是对epollitem的初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
INIT_LIST_HEAD(&epi->rdllink); INIT_LIST_HEAD(&epi->fllink); INIT_LIST_HEAD(&epi->pwqlist); epi->ep = ep; ep_set_ffd(&epi->ffd, tfile, fd); epi->event = *event; epi->nwait = 0; epi->next = EP_UNACTIVE_PTR; if
(epi->event.events & EPOLLWAKEUP) {
error = ep_create_wakeup_source(epi);
if
(error)
goto
error_create_wakeup_source;
} else
{
RCU_INIT_POINTER(epi->ws, NULL);
} |
接下来是比较重要的操作:将epollitem对象添加到被监视文件的等待队列上去。等待队列实际上就是一个回调函数链表,定义在/include/linux/wait.h文件中。因为不同文件系统的实现不同,无法直接通过struct file对象获取等待队列,因此这里通过struct file的poll操作,以回调的方式返回对象的等待队列,这里设置的回调函数是ep_ptable_queue_proc:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
struct
ep_pqueue epq;
... /* Initialize the poll table using the queue callback */ epq.epi = epi; init_poll_funcptr(&epq.pt, ep_ptable_queue_proc); /* * Attach the item to the poll hooks and get current event bits.
* We can safely use the file* here because its usage count has
* been increased by the caller of this function. Note that after
* this operation completes, the poll callback can start hitting
* the new item.
*/
revents = ep_item_poll(epi, &epq.pt); |
上面代码中结构体ep_queue的作用是能够在poll的回调函数中取得对应的epollitem对象,这种做法在Linux内核里非常常见。
在回调函数ep_ptable_queue_proc中,内核会创建一个struct eppoll_entry对象,然后将等待队列中的回调函数设置为ep_poll_callback()。也就是说,当被监控文件有事件到来时,比如socker收到数据时,ep_poll_callback()会被回调。ep_ptable_queue_proc()代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
static
void ep_ptable_queue_proc( struct
file *file, wait_queue_head_t *whead,
poll_table *pt)
{ struct
epitem *epi = ep_item_from_epqueue(pt);
struct
eppoll_entry *pwq;
if
(epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead;
pwq->base = epi;
if
(epi->event.events & EPOLLEXCLUSIVE)
add_wait_queue_exclusive(whead, &pwq->wait);
else
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
}
else {
/* We have to signal that an error occurred */
epi->nwait = -1;
}
} |
eppoll_entry和epitem等结构关系如下图:
在回到ep_insert()函数中。ep_item_poll()调用完成之后,会将epitem中的fllink字段添加到struct file中的f_ep_links链表中,这样就可以通过struct file找到所有对应的struct epollitem对象,进而通过struct epollitem找到所有的epoll实例对应的struct eventpoll。
1
2
3
|
spin_lock(&tfile->f_lock); list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links); spin_unlock(&tfile->f_lock); |
然后就是将epollitem插入到红黑树中:
1
|
ep_rbtree_insert(ep, epi) |
最后再更新下状态就返回了,插入操作也就完成了。
在返回之前还会判断一次刚才添加的文件是不是当前已经有事件就绪了,如果是就将其加入到epoll的就绪链表中,关于就绪链表放到下一部分中讲,这里略过。
最后是我画的几个结构体之间的结构图。
在通过epoll_ctl(2)向epoll中添加被监视文件描述符时,会将ep_poll_callback()作为回调函数添加被监视文件的等待队列中。下面分析ep_poll_callback()函数
1
2
3
4
5
6
7
|
1004 static
int ep_poll_callback(wait_queue_t *wait, unsigned mode,
int sync,
void *key)
1005 { 1006
int pwake = 0;
1007 unsigned
long flags;
1008
struct epitem *epi = ep_item_from_wait(wait);
1009
struct eventpoll *ep = epi->ep;
1010
int ewake = 0;
|
1008行首先调用ep_item_from_wait()来获取到与被监视文件描述符相关联的结构体struct epitem,获取方法就是利用container_of宏。
1009行再根据struct epitem的ep字段获取到代表epoll对象实例的结构体struct eventpoll。
1
2
3
4
5
6
7
8
9
10
|
1012
if ((unsigned
long )key & POLLFREE) {
1013 ep_pwq_from_wait(wait)->whead = NULL; 1014
/*
1015 * whead = NULL above can race with ep_remove_wait_queue() 1016 * which can do another remove_wait_queue() after us, so we 1017 * can't use __remove_wait_queue(). whead->lock is held by 1018 * the caller. 1019 */ 1020 list_del_init(&wait->task_list); 1021 } |
判断返回的事件掩码里是否设置了标志位POLLFREE(什么时候会设置该标志?),如果是则将当前等待对象从文件描述符的等待队列中删除(疑问:注释是什么意思?为什么不需要加锁?)。
接下来对epoll的实例加锁:
1
|
1023 spin_lock_irqsave(&ep->lock, flags); |
接下来判断epitem中的事件掩码是不是并没有包括任何poll(2)事件,如果是的话,则解锁后直接返回:
1
2
3
4
5
6
7
8
|
1025
/*
1026 * If the event mask does not contain any poll(2) event, we consider the 1027 * descriptor to be disabled. This condition is likely the effect of the 1028 * EPOLLONESHOT bit that disables the descriptor when an event is received, 1029 * until the next EPOLL_CTL_MOD will be issued. 1030 */ 1031
if (!(epi->event.events & ~EP_PRIVATE_BITS))
1032
goto out_unlock;
|
什么时候会出现上述情况呢?注释里也说了,就是在设置了EPOLLONESHOT标志的时候。对EPOLLONESHOT标志的处理是在epoll_wait()的返回过程,调用ep_send_events_proc()的时候,如果设置了EPOLLONESHOT标志则将EP_PRIVATE_BITS以外的标志位全部清0:
1
2
|
1552
if (epi->event.events & EPOLLONESHOT)
1553 epi->event.events &= EP_PRIVATE_BITS; |
接下来判断返回的事件里是否有用户真正感兴趣的事件,没有则解锁后返回,否则继续。
1
2
3
4
5
6
7
8
|
1034
/*
1035 * Check the events coming with the callback. At this stage, not 1036 * every device reports the events in the "key" parameter of the 1037 * callback. We need to be able to handle both cases here, hence the 1038 * test for "key" != NULL before the event match test. 1039 */ 1040
if (key && !((unsigned
long ) key & epi->event.events))
1041
goto out_unlock;
|
如果此时就绪链表rdllist没有被其他进程访问,则直接将当前文件描述符添加到rdllist链表中,否则的话添加到ovflist链表中。ovflist默认值是EP_UNACTIVE_PTR,epoll_wait()遍历rdllist之前会把ovflist设置为NULL,遍历完再恢复为EP_UNACTIVE_PTR,因此通过判断ovflist的值是不是EP_UNACTIVE_PTR可知此时rdllist是不是正在被访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
1049
if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
1050
if (epi->next == EP_UNACTIVE_PTR) {
1051 epi->next = ep->ovflist; 1052 ep->ovflist = epi; 1053
if (epi->ws) {
1054
/*
1055 * Activate ep->ws since epi->ws may get 1056 * deactivated at any time. 1057 */ 1058 __pm_stay_awake(ep->ws); 1059 } 1060 1061 } 1062
goto out_unlock;
1063 } 1064 1065
/* If this file is already in the ready list we exit soon */
1066
if (!ep_is_linked(&epi->rdllink)) {
1067 list_add_tail(&epi->rdllink, &ep->rdllist); 1068 ep_pm_stay_awake_rcu(epi); 1069 } |
如果是描述符是添加到ovflist链表中,说明此时已经有ep_wait()准备返回了,因此不用再唤醒epoll实例的等待队列,因此1062行直接跳到解锁处;否则的话,则唤醒因为调用epoll_wait()而等待在epoll实例等待队列上的进程(这里最多只会唤醒一个进程):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
1075
if (waitqueue_active(&ep->wq)) {
1076
if ((epi->event.events & EPOLLEXCLUSIVE) &&
1077 !((unsigned
long )key & POLLFREE)) {
1078
switch ((unsigned
long )key & EPOLLINOUT_BITS) {
1079
case POLLIN:
1080
if (epi->event.events & POLLIN)
1081 ewake = 1; 1082
break ;
1083
case POLLOUT:
1084
if (epi->event.events & POLLOUT)
1085 ewake = 1; 1086
break ;
1087
case 0:
1088 ewake = 1; 1089
break ;
1090 } 1091 } 1092 wake_up_locked(&ep->wq); 1093 } |
如果epoll实例的poll队列非空,也会唤醒等待在poll队列上的进程,不过是在解锁后才会进行唤醒操作。
1
2
|
1094
if (waitqueue_active(&ep->poll_wait))
1095 pwake++; |
最后解锁并返回:
1
2
3
4
5
6
7
8
9
10
11
|
1097 out_unlock: 1098 spin_unlock_irqrestore(&ep->lock, flags); 1099 1100
/* We have to call this outside the lock */
1101
if (pwake)
1102 ep_poll_safewake(&ep->poll_wait); 1103 1104
if (epi->event.events & EPOLLEXCLUSIVE)
1105
return ewake;
1106 1107
return 1;
|
注意到ep_poll_callback()的返回值和EPOLLEXCLUSIVE标志有关,该标志是用来处理这种情况:当多个进程中的不同epoll实例在监视同一个文件描述符时,如果该文件描述符上有事件发生,则所有的epoll实例所在进程都将被唤醒,这样有可能造成“惊群”(thundering herd)。关于EPOLLEXCLUSIVE可以看这里。