1、listen socket水平触发的poll函数调用
以服务器端epoll为例,加入监听、等待并接受连接、再次等待,会有3次检查是否有连接就绪的操作,分别是epoll_ctl、epoll_wait、epoll_wait。
1.1、epoll_wait(第1次调用)
等待就绪链表相关内核看前面发布的文章《linux网络编程 - epoll内核实现代码分析》。这里介绍水平触发下,如果还有等待接受的连接,epoll_wait是如何触发或者说获取到读事件的。
ep_send_events_proc代码:
如上图代码1533行,这行代码将就绪epitem从就绪链表删掉,然后调用ep_item_poll检查获取该epitem的就绪事件,监听的就绪事件保存到revents,如果没有就绪的监听事件,那么revents就为0,1543行就是检查epitem是否有就绪事件,如果没有,那么if分支代码就不会执行(if代码块之后没有代码将epitem再次加入就绪链表),下次再次调用epoll_wait就不会再检查该epitem。
接下来看1543行if分支里面的代码,有就绪的监听事件就调用__put_user拷贝事件到用户态,最后判断触发方式,ep_send_events_proc代码如下所示:
1554行检查是否是边沿触发EPOLLET,如果不是边沿触发,就执行if里面的代码,调用list_add_tail将epitem再次加入到就绪链表里面,也就是水平触发模式,epoll_wait获取到就绪事件之后并不会将就绪事件删除,用户程序获取到就绪事件之后,不管是接受所有连接还是接受部分连接,listen的socket还是"可读"的。
1.2、epoll_wait(第2次调用)
epoll_wait调用返回之后,用户程序可以不处理事件,也可以只接受部分连接,然后再次调用epoll_wait:
- 如果用户程序接受所有连接之后再次调用epoll_wait,此时epitem虽然还在就绪链表里面,但是实际上epitem是不可读的,因为epitem还在就绪链表里面,所以内核还会再次调用ep_item_poll对socket事件再次进行检查,正如前面介绍的内核会先将epitem从就绪链表删除,因为当前epitem实际上不可读,所以不会返回就绪事件,也就是revents为0,那么epitem就不会被再次加入就绪链表,也就是水平触发的epoll_wait多做了一次无用的检查;
- 如果用户程序没有接受连接或者接受部分连接,那么再次调用epoll_wait的时候epitem自然是就绪的,也就是前面为什么获取到就绪事件之后,还要再次将epitem加入到就绪链表的原因。
2、listen socket边沿触发的poll函数调用
边沿触发在前面实际已经介绍到了,水平触发epitem从就绪链表删除之后还会再添加到就绪链表,但是边沿触发就不会,如果epoll_wait获取事件之后不读取连接或者读取部分连接,那么再次调用epoll_wait,此时epitem已经不再就绪链表里面了,那么就检查不到就绪事件(虽然此时有连接等待接受),从前面的文章《linux网络编程 - epoll内核实现代码分析》可以看到,如果有新的连接,那么会调用sock_def_readable,sock_def_readable一级级调用下去就会检查epitem是否在就绪链表里面,如果不在,那么就会添加到就绪链表并唤醒阻塞线程,
ep_poll_callback添加就绪链表唤醒阻塞线程代码如下:
函数调用栈如下: