zoukankan      html  css  js  c++  java
  • epoll惊群原因分析

    考虑如下情况(实际一般不会做,这里只是举个例子):
    1. 在主线程中创建一个socket、绑定到本地端口并监听
    2. 在主线程中创建一个epoll实例(epoll_create(2))
    3. 将监听socket添加到epoll中(epoll_ctl(2))
    4. 创建多个子线程,每个子线程都共享步骤2里创建的同一个epoll文件描述符,然后调用epoll_wait(2)等待事件到来accept(2)
    5. 请求到来,新连接建立

    这里的问题就是,在第5步的时候,会有多少个线程被唤醒而从epoll_wait()调用返回?答案是不一定,可能只有一个,也可能有部分,也可能是全部。当然在多个线程都唤醒的情况下,只会有一个线程accept()调用会成功。

    为何如此?从内核代码分析,原因如下:

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

    因为__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++;
    	}
    而在水平触发方式下,从就绪链表中移出来的文件描述符,如果当前仍有事件就绪(可读、可写等),会在复制到用户空间后被再次添加到就绪链表中:
    if (epi->event.events & EPOLLONESHOT)
    	epi->event.events &= EP_PRIVATE_BITS;
    else if (!(epi->event.events & EPOLLET)) {
    	/*
    	 * 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);
    }
    因此在水平触发模式下,被唤醒的进程又会去唤醒其他进程,除非当前事件已经被处理完或者所有进程都已经被唤醒(被唤醒的进程会从epoll等待队列上移除)。
     
  • 相关阅读:
    设计模式-代理模式
    设计模式-桥接模式
    设计模式-组合模式
    设计模式-享元模式
    设计模式-适配器模式
    设计模式-装饰器模式
    设计模式-外观模式
    redis日志格式
    Linux下的文件切割和文件合并
    Windows server 服务器的端口突然远程连不上了,但是可以远程连接,怎么回事?
  • 原文地址:https://www.cnblogs.com/sduzh/p/6810469.html
Copyright © 2011-2022 走看看