惊群:概念就不解释了。
直接说正题:惊群问题一般出现在那些web服务器上,Linux系统有个经典的accept惊群问题,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理。
/* * The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just * wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve * number) then we wake all the non-exclusive tasks and one exclusive task. * * There are circumstances in which we can try to wake a task which has already * started to run but is not in state TASK_RUNNING. try_to_wake_up() returns * zero in this (rare) case, and we handle it by continuing to scan the queue. */ static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key) { wait_queue_t *curr, *next; list_for_each_entry_safe(curr, next, &q->task_list, task_list) { unsigned flags = curr->flags; if (curr->func(curr, mode, wake_flags, key) && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break; } }
添加了一个WQ_FLAG_EXCLUSIVE标记,告诉内核进行排他性的唤醒,即唤醒一个进程后即退出唤醒的过程,问题得以解决。
多路复用的需求让select,poll,epoll等事件模型更为受到欢迎,所谓的事件模型即阻塞在事件上,
内核仅仅通知发生了某件事,具体发生了什么事,则有处理进程或者线程自己来poll。
如此一来,这个事件模型(无论其实现是select,poll,还是epoll)便可以一次搜集多个事件,从而满足多路复用的需求。
Linux 3.x 中epoll的惊群问题?:https://www.zhihu.com/question/24169490/answers/updated
首先看下惊群的原因:
ep_insert的时候会调用,revents = ep_item_poll(epi, &epq.pt);
//epi代表target file,即被监听的文件,poll()返回就绪事件的掩码,赋给revents.epi->ffd.file->f_op->poll(epi->ffd.file, pt) & epi->event.events;
其实就是调用被监控文件(epoll里叫“target file”)的poll方法, 而这个poll其实就是调用poll_wait(还记得poll_wait吗?每个支持poll的设备驱动程序都要调用的), 最后就是调用ep_ptable_queue_proc。
(注:f_op->poll()一般来说只是个wrapper, 它会调用真正的poll实现, 拿UDP的socket来举例, 这里就是这样的调用流程: f_op->poll(), sock_poll(), udp_poll(), datagram_poll(), sock_poll_wait()。)
这是比较难解的一个调用关系,因为不是语言级的直接调用。
sock_poll_wait(file, sk_sleep(sk), wait); static inline wait_queue_head_t *sk_sleep(struct sock *sk) { BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0); return &rcu_dereference_raw(sk->sk_wq)->wait;// 在sk->sk_wq 上挂载回调函数ep_ptable_queue_proc---》ep_poll_callback }
事件发生,唤醒相关文件句柄睡眠队列的entry,调用其回调
假设一个TCP Listen socket上来了一个连接请求,已经完成了三次握手,内核希望通知epoll_wait返回,然后去取accept。
内核在wakeup这个socket的sk_wq时,最终会调用到ep_poll_callback回调,ep_poll_callback中会调用:
/* * Wake up ( if active ) both the eventpoll wait list and the ->poll() * wait list. */ //如果等待进程队列不为空的话,唤醒在该epoll上的等待进程 if (waitqueue_active(&ep->wq)) { if ((epi->event.events & EPOLLEXCLUSIVE) && !((unsigned long)key & POLLFREE)) { switch ((unsigned long)key & EPOLLINOUT_BITS) { case POLLIN: if (epi->event.events & POLLIN) ewake = 1; break; case POLLOUT: if (epi->event.events & POLLOUT) ewake = 1; break; case 0: ewake = 1; break; } } wake_up_locked(&ep->wq); }
既然“就绪链表”中有了新成员,则唤醒阻塞在epoll_wait系统调用的task去处理。注意,如果本来epi已经在“就绪队列”了,这里依然会唤醒并处理的
但是唤醒epoll睡眠队列的task,搜集并上报数据时会调用ep_send_events
向用户态上报事件,其中调用ep_scan_ready_list:
ep_scan_ready_list 中会调用如下代码:
//如果rdllist链表非空,尝试唤醒ep->wq和ep->poll_wait等待队列 、 if (!list_empty(&ep->rdllist)) { /* * Wake up (if active) both the eventpoll wait list and * the ->poll() wait list (delayed after we release the lock). */ if (waitqueue_active(&ep->wq)) wake_up_locked(&ep->wq); if (waitqueue_active(&ep->poll_wait)) pwake++; }
也就是: 如果“就绪链表”上仍有未处理的epi,且有进程阻塞在epoll句柄的睡眠队列,则唤醒它!(这将是LT惊群的根源)
epoll的LT和ET以及相关问题:
- LT水平触发
如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你。 - ET边沿触发
如果事件来了,不管来了几个,你若不处理或者没有处理完,除非下一个事件到来,否则epoll将不会再通知你 - 所以ET 要用非阻塞读取fd 直到读取完毕
一般http server写法: https://blog.csdn.net/rzytc/article/details/50529691
// 否则会阻塞在IO系统调用,导致没有机会再epoll set_socket_nonblocking(fd); epfd = epoll_create(1); event.data.fd = fd; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); while (1) { epoll_wait(epfd, events, 1, xx); ... // 危险区域!如果有共享同一个epfd的进程/线程调用epoll_wait,它们也将会被唤醒! // 这个accept将会有多个进程/线程调用,如果并发请求数很少,那么将仅有几个进程会成功: // 1. 假设accept队列中有n个请求,则仅有n个进程能成功,其它将全部返回EAGAIN (Resource temporarily unavailable) // 2. 如果n很大(即增加请求负载),虽然返回EAGAIN的比率会降低,但这些进程也并不一定取到了epoll_wait返回当下的那个预期的请求。 csd = accept(fd, &in_addr, &in_len); ... }
如https://blog.csdn.net/dog250/article/details/80837278 分析如下:
LT的描述“如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你。”,显然,epoll_wait刚刚取到事件的时候的时候,不可能马上就调用accept去处理,事实上,逻辑在epoll_wait函数调用的ep_poll中还没返回的,这个时候,显然符合“仍然有未处理的事件”这个条件,显然这个时候为了实现这个语义,需要做的就是通知别的同样阻塞在同一个epoll句柄睡眠队列上的进程!在实现上,这个语义由两点来保证:
- 保证1:在LT模式下,“就绪链表”上取出的epi上报完事件后会重新加回“就绪链表”;
- 保证2:如果“就绪链表”不为空,且此时有进程阻塞在同一个epoll句柄的睡眠队列上,则唤醒它。
-
ep_scan_ready_list() { // 遍历“就绪链表” ready_list_for_each() { list_del_init(&epi->rdllink); revents = ep_item_poll(epi, &pt); // 保证1 if (revents) { __put_user(revents, &uevent->events); if (!(epi->event.events & EPOLLET)) { list_add_tail(&epi->rdllink, &ep->rdllist); } } } // 保证2 if (!list_empty(&ep->rdllist)) { if (waitqueue_active(&ep->wq)) wake_up_locked(&ep->wq); }
假设LT模式下有10个进程共享同一个epoll句柄,此时来了一个请求client进入到accept队列,我们发现上述的1和2是一个循环唤醒的过程:
1).假设进程a的epoll_wait首先被ep_poll_callback唤醒,那么满足1和2,则唤醒了进程B;
2).进程B在处理ep_scan_ready_list的时候,发现依然满足1和2,于是唤醒了进程C….
3).上面1)和2)的过程一直到之前某个进程将client取出,此时下一个被唤醒的进程在ep_scan_ready_list中的ep_item_poll调用中将得不到任何事件,此时便不会再将该epi加回“就绪链表”了,LT水平触发结束,结束了这场悲伤的梦!
所用解决惊群方法之一:让不同进程的epoll_waitI调用互斥即可。对于非listen socket 可以这样使用,但是对于文件 I/O fd那就不好说了,有时就是为了多个进程读
ET边沿触发模式的问题以及解决
ET模式不满足上述的“保证1”,所以不会将已经上报事件的epi重新链接回“就绪链表”,也就是说,只要一个“就绪队列”上的epi上的事件被上报了,它就会被删除出“就绪队列”。
由于epi entry的callback即ep_poll_callback所做的事情仅仅是将该epi自身加入到epoll句柄的“就绪链表”,同时唤醒在epoll句柄睡眠队列上的task,所以这里并不对事件的细节进行计数,
比如说,如果ep_poll_callback在将一个epi加入“就绪链表”之前发现它已经在“就绪链表”了,那么就不会再次添加,因此可以说,一个epi可能pending了多个事件,注意到这点非常重要!
一个epi上pending多个事件,这个在LT模式下没有任何问题,因为获取事件的epi总是会被重新添加回“就绪链表”,那么如果还有事件,在下次check的时候总会取到。
然而对于ET模式,仅仅将epi从“就绪链表”删除并将事件本身上报后就返回了,因此如果该epi里还有事件,则只能等待再次发生事件,进而调用ep_poll_callback时将该epi加入“就绪队列”。这意味着什么?
这意味着,应用程序,即epoll_wait的调用进程必须自己在获取事件后将其处理干净后方可再次调用epoll_wait,否则epoll_wait不会返回,而是必须等到下次产生事件的时候方可返回。即,依然以accept为例,必须这样做:
while (1) { epoll_wait(epfd, events, 64, xx); while ((csd = accept(sd, &in_addr, &in_len)) > 0) { do_something(...); } ...
目前有很多是:便出现了create listener+fork这种模型,
fd = create_listen_socket(); for (i = 0; i < N; i++) { if (fork() == 0) { // 继承了父进程的文件描述符 server(fd); }
这种模型在处理同一个socket的时候,必须互斥,同时内核必须防止潜在的惊群效应,因为互斥的要求,有且仅有一个进程可以处理特定的请求。这就对编程造成了极大的干扰。
目前reuseport出现解决此问题。
对于epoll 惊群问题,可以由如下解决方案:
1、类似于accept 解决方式? 是不是方法不对??
因为__wake_up_common()的调用是从wake_up_locked()开始的,__wake_up_common的各个参数值为:
- q: struct eventpoll.wq
- mode: TASK_NORMAL
- nr_exclusive:1
- wake_flags: 0
- key:NULL。
- curr->flags: WQ_FLAG_EXCLUSIVE
- curr->func: default_wake_function
因此__wake_up_common里的if条件会在第一次判断的时候就满足,唤醒一个进程后便返回了,是不是以为只会唤醒一个进程??
当某个等待在epoll实例上的进程被唤醒后,最终会进入到ep_scan_ready_list() 这个函数中,ep_scan_ready_list()会以回调方式调用ep_send_events_proc()来将数据复制到用户空间。而ep_scan_ready_list()函数在返回之前会再次判断epoll的就绪链表rdllist是否为空,如果不为空的话,就会再唤醒其他进程!下面就是ep_scan_ready_list()返回之前的判断操作:
if (!list_empty(&ep->rdllist)) { /* * Wake up (if active) both the eventpoll wait list and * the ->poll() wait list (delayed after we release the lock). */ if (waitqueue_active(&ep->wq)) wake_up_locked(&ep->wq); if (waitqueue_active(&ep->poll_wait)) pwake++; }
在水平触发方式下,从就绪链表中移出来的文件描述符,如果当前仍有事件就绪(可读、可写等),会在复制到用户空间后被再次添加到就绪链表中:
2、linux 3.x 引入的reuseport
-
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网