Linux惊群
1.惊群
惊群即当某一资源可用时,导致多个进程/线程去竞争资源。惊群会导致的问题:
[1]导致n-1个进程/线程做了无效的调度和上下文切换,cpu瞬时增高。
[2]多个进程/线程争取资源同步(加解锁)时造成的系统开销。
当前Linux存在的惊群情况有:accept、epoll、条件变量导致的多线程惊群。
2.accept、epoll、Nginx、条件变量导致的多线程惊群
【1】accept
在2.6内核之前,使用fork多个进程accept同一个fd时,如果有信号会导致所有进程的accept都会惊醒,但只有一个可以accept成功,其他返回EGAIN。
2.6内核之后解决了该问题,由信号时只会唤醒一个进程。
【2】epoll
epoll有LT和ET模式。
epoll的内核操作:加锁遍历“就绪队列”(ep->rdllist),首先把event从“就绪队列”删除,然后调用文件的poll函数检查该文件(fd)是否有事件,如果有则把事件和用户数据拷贝到用户空间,之后如果是EPOLLIN模式则再次加入到“就绪队列”。
因此,当是LT模式时,某个进程的epoll_wait()收到事件后,并且内核再次把事件加到“就绪队列”,进而一直重复此过程,直到这个事件被处理。
结论,使用fork多个进程调用epoll_wait()同一个epoll_fd时:
[1]LT模式时,会导致同一个fd被多个进程收到事件,因为处理这个事件之前,会一直把该事件放到“就绪队列”,导致其他进程收到事件,类似惊群(连锁反应导致的不断触发)。
[2]ET模式时因为不会再次加到“就绪队列”,所以不会导致一个事件被多个进程处理。
【3】条件变量导致的多线程惊群
首先,条件变量中的mutex是对用户的互斥量进行保护的,而不是cond本身,因为cond的操作本来就是原子的。
pthread_cond_signal:这个函数肯定只会唤醒一个线程。
pthread_cond_broadcast:同时唤醒多个线程,只有一个线程的pthread_cond_wait()返回(即当前线程成功加锁了mutex),其他线程还不能返回(等待mutex解锁后才能返回)。即被唤醒的所有线程其实是要通过mutex锁顺序执行。
因此,条件变量的惊群情形是:使用pthread_cond_broadcast同时唤醒多个线程。由于被唤醒的多个线程顺序执行,所以第2个线程的pthread_cond_wait返回时,可能条件已经不满足了,所以需要使用while再次判断条件是否满足:
1 2 3 4 | pthread_mutex_lock(& lock ); while (count == 0) pthread_cond_wait(&cond, & lock ); pthread_mutex_unlock(& lock ); |
3.Nginx的惊群处理
4.epoll内核部分源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | /* * We can loop without lock because we are passed a task private list. * Items cannot vanish during the loop because ep_scan_ready_list() is //表示执行下面循环时前面已经加锁了 * holding "mtx" during this call. */ for (esed->res = 0, uevent = esed->events; //遍历就绪队列,就绪队列表示检查fd状态或有信号的event队列 !list_empty(head) && esed->res < esed->maxevents;) { epi = list_first_entry(head, struct epitem, rdllink); /* * Activate ep->ws before deactivating epi->ws to prevent * triggering auto-suspend here (in case we reactive epi->ws * below). * * This could be rearranged to delay the deactivation of epi->ws * instead, but then epi->ws would temporarily be out of sync * with ep_is_linked(). */ ws = ep_wakeup_source(epi); if (ws) { if (ws->active) __pm_stay_awake(ep->ws); __pm_relax(ws); } list_del_init(&epi->rdllink); //删除event revents = ep_item_poll(epi, &pt, 1); //返回fd的状态,即EPOLLIN等信号 /* * If the event mask intersect the caller-requested one, * deliver the event to userspace. Again, ep_scan_ready_list() * is holding "mtx", so no operations coming from userspace * can change the item. */ if (revents) { //如果有信号,则把信号和用户数据拷贝到用户空间 if (__put_user(revents, &uevent->events) || __put_user(epi-> event .data, &uevent->data)) { list_add(&epi->rdllink, head); ep_pm_stay_awake(epi); if (!esed->res) esed->res = -EFAULT; return 0; } esed->res++; uevent++; if (epi-> event .events & EPOLLONESHOT) epi-> event .events &= EP_PRIVATE_BITS; else if (!(epi-> event .events & EPOLLET)) { //如果不是EPOLLET模式,则再把event加到就绪队列 /* * If this file has been added with Level * Trigger mode, we need to insert back inside * the ready list, so that the next call to * epoll_wait() will check again the events * availability. At this point, no one can insert * into ep->rdllist besides us. The epoll_ctl() * callers are locked out by * ep_scan_ready_list() holding "mtx" and the * poll callback will queue them in ep->ovflist. */ list_add_tail(&epi->rdllink, &ep->rdllist); ep_pm_stay_awake(epi); } } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步