紧接着,我们看下ep_poll函数:
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout) { ...... retry: // 获取spinlock spin_lock_irqsave(&ep->lock, flags); // 将当前task_struct写入到waitqueue中以便唤醒 // wq_entry->func = default_wake_function; init_waitqueue_entry(&wait, current); // WQ_FLAG_EXCLUSIVE,排他性唤醒,配合SO_REUSEPORT从而解决accept惊群问题 wait.flags |= WQ_FLAG_EXCLUSIVE; // 链入到ep的waitqueue中 __add_wait_queue(&ep->wq, &wait); for (;;) { // 设置当前进程状态为可打断 set_current_state(TASK_INTERRUPTIBLE); // 检查当前线程是否有信号要处理,有则返回-EINTR if (signal_pending(current)) { res = -EINTR; break; } spin_unlock_irqrestore(&ep->lock, flags); // schedule调度,让出CPU jtimeout = schedule_timeout(jtimeout); spin_lock_irqsave(&ep->lock, flags); } // 到这里,表明超时或者有事件触发等动作导致进程重新调度 __remove_wait_queue(&ep->wq, &wait); // 设置进程状态为running set_current_state(TASK_RUNNING); ...... // 检查是否有可用事件 eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR; ...... // 向用户空间拷贝就绪事件 ep_send_events(ep, events, maxevents) }上述逻辑如下图所示:
ep_send_events函数主要就是调用了ep_scan_ready_list,顾名思义ep_scan_ready_list就是扫描就绪列表:
static int ep_scan_ready_list(struct eventpoll *ep, int (*sproc)(struct eventpoll *, struct list_head *, void *), void *priv, int depth) { ... // 将epfd的rdllist链入到txlist list_splice_init(&ep->rdllist, &txlist); ... /* sproc = ep_send_events_proc */ error = (*sproc)(ep, &txlist, priv); ... // 处理ovflist,即在上面sproc过程中又到来的事件 ... }其主要调用了ep_send_events_proc:
static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head, void *priv) { for (eventcnt = 0, uevent = esed->events; !list_empty(head) && eventcnt < esed->maxevents;) { // 遍历ready list epi = list_first_entry(head, struct epitem, rdllink); list_del_init(&epi->rdllink); // readylist只是表明当前epi有事件,具体的事件信息还是得调用对应file的poll // 这边的poll即是tcp_poll,根据tcp本身的信息设置掩码(mask)等信息 & 上兴趣事件掩码,则可以得知当前事件是否是epoll_wait感兴趣的事件 revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) & epi->event.events; if(revents){ /* 将event放入到用户空间 */ /* 处理ONESHOT逻辑 */ // 如果不是边缘触发,则将当前的epi重新加回到可用列表中,这样就可以下一次继续触发poll,如果下一次poll的revents不为0,那么用户空间依旧能感知 */ else if (!(epi->event.events & EPOLLET)){ list_add_tail(&epi->rdllink, &ep->rdllist); } /* 如果是边缘触发,那么就不加回可用列表,因此只能等到下一个可用事件触发的时候才会将对应的epi放到可用列表里面*/ eventcnt++ } /* 如poll出来的revents事件epoll_wait不感兴趣(或者本来就没有事件),那么也不会加回到可用列表 */ ...... } return eventcnt; }上述代码逻辑如下所示:
经过上述章节的详述之后,我们终于可以阐述,tcp在数据到来时是怎么加入到epoll的就绪队列的了。
可读事件到来首先我们看下tcp数据包从网卡驱动到kernel内部tcp协议处理调用链:
step1:网络分组到来的内核路径,网卡发起中断后调用netif_rx将事件挂入CPU的等待队列,并唤起软中断(soft_irq),再通过linux的软中断机制调用net_rx_action,如下图所示:
注:上图来自PLKA(<<深入Linux内核架构>>) step2:
紧接着跟踪next_rx_action
next_rx_action |-process_backlog ...... |->packet_type->func 在这里我们考虑ip_rcv |->ipprot->handler 在这里ipprot重载为tcp_protocol (handler 即为tcp_v4_rcv)我们再看下对应的tcp_v4_rcv
tcp_v4_rcv |->tcp_v4_do_rcv |->tcp_rcv_state_process |->tcp_data_queue |-> sk->sk_data_ready(sock_def_readable) |->wake_up_interruptible_sync_poll(sk->sleep,...) |->__wake_up |->__wake_up_common |->curr->func /* 这里已经被ep_insert添加为ep_poll_callback,而且设定了排它标识WQ_FLAG_EXCLUSIVE*/ |->ep_poll_callback