惊群:概念就不解释了。

直接说正题:惊群问题一般出现在那些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. 保证1:在LT模式下,“就绪链表”上取出的epi上报完事件后会重新加回“就绪链表”;
  2. 保证2:如果“就绪链表”不为空,且此时有进程阻塞在同一个epoll句柄的睡眠队列上,则唤醒它。
  3. 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 解决方式? 是不是方法不对??

在调用epoll_wait(2)的时候,设置的epoll的等待队列回调函数是default_wake_function,添加队列的时候调用的是__add_wait_queue_exclusive()。
ep_poll_callback()中唤醒操作调用的是wake_up_locked(&ep->wq),最终会调用__wake_up_common,后者会判断exclusive标志:

因为__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的值可以通过epoll_wait()的源码得到,具体为:
  • curr->flags: WQ_FLAG_EXCLUSIVE
  • curr->func: default_wake_function
default_wake_function调用的是try_to_wake_up。而try_to_wake_up只有在要唤醒的进程状态不是TASK_NORMAL时才会返回0,TASK_NORMAL的定义是(TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)。

因此__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

-

惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理,这是通过以下的代码来实现的:
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理,这是通过以下的代码来实现的:
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理,这是通过以下的代码来实现的:
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网
惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理
作者:紫衣仙女
链接:https://www.imooc.com/article/40708
来源:慕课网

最新文章

  1. [NHibernate]立即加载
  2. 利用Javascript判断操作系统的类型实现不同操作系统下的兼容性
  3. Html 之div+css布局之css基础
  4. 判断一个类到底是从哪个jar包中调用的工具类
  5. Redis学习笔记(2) Redis基础类型及命令之一
  6. ArrayList&amp;LinkedList&amp;Map&amp;Arrays
  7. android之AlertDialog 点击其它区域自己主动消失
  8. 桥牌笔记 Skill Level 4 C7 小心将吃
  9. Adaboost原理及目标检测中的应用
  10. bzoj1027
  11. 关于GC的意见
  12. virtualbox中新版本Ubuntu安装软件增强包后重启无限登录界面的解决办法
  13. Codeforces Round #551 (Div. 2) D. Serval and Rooted Tree (树形dp)
  14. 类别不平衡问题和Softmax回归
  15. WinForm界面设计优化过程
  16. day 07 数据类型,集合,深浅copy
  17. 颜色空间之CIE2000色差公式
  18. http协议、web服务器、并发服务器(下)
  19. nginx负载均衡优化配置
  20. JS基础---Dom的基本操作

热门文章

  1. 多测师讲解自动化测试 _RF连接数据库_高级讲师肖sir
  2. 习题3-5 谜题(Puzzle, ACM/ICPC World Finals 1993, UVa227)
  3. linux(centos8):用systemctl管理war包形式的jenkins(java 14 / jenkins 2.257)
  4. centos8平台使用lscpu查看cpu信息
  5. python操作excel xlwt (转)
  6. pycharm2018.1下载激活(mac平台)
  7. 第九章 nginx基础之搭建小游戏
  8. codeforces#426(div1) B - The Bakery (线段树 + dp)
  9. hystrix线程池隔离的原理与验证
  10. 标签平滑(Label Smoothing)详解