linux网络编程 - epoll内核实现代码分析

1、linux内核epoll相关数据结构

1.1、epoll相关数据结构类图

1.2、关键数据结构说明

        socket_wq结构体包含一个__wait_queue_head成员,__wait_queue_head用于连接wait_queue_t链表,对于epoll而言就是连接eppoll_entry;

        eppoll_entry包含一个epitem,epitem包含一个epoll_event、eventpoll以及ffd,epoll_event也就是监听的事件以及用户态传递过来的一个额外数据,eventpoll包含等待链表以及就绪的epitem链表,阻塞线程挂在eventpoll的等待链表,ffd包含socket相关数据,事件就绪之后通过ffd获取到socket并调用tcp_poll检查socket是否可读写。

2、epoll代码实现

2.1、eppoll_entry加入到socket_wq等待链表(epoll_ctl)

        epoll_ctl系统调用为SYSC_epoll_ctl,SYSC_epoll_ctl调用ep_ptable_queue_proc创建并初始化eppoll_entry,然后加入到socket_wq。

         SYSC_epoll_ctl系统调用栈:

 2.2、阻塞任务添加到eventpoll等待链表(epoll_wait)

        epoll_wait系统调用为SyS_epoll_wait,SyS_epoll_wait调用ep_poll,ep_poll调用ep_events_available检查是否有就绪的事件,如果没有就绪的事件,则调用__add_wait_queue_exclusive将当前线程加到eventpoll的等待队列。

2.3、任务切换(schedule) 

        当前线程加到eventpoll的等待队列之后,"循环"等待事件,等待过程可能被其他操作唤醒,所以唤醒之后不一定有就绪事件,所以for循环调用ep_events_available再次检查是否有就绪事件,并检查timed_out是否超时;在任务进入睡眠之前调用signal_pending检查是否有挂起的信号要处理,如果有信号要处理,那么不能进入睡眠状态,需要先处理挂起的事件;如果没有就绪事件也没有超时,另外也没有挂起的事件,那么调用schedule_hrtimeout_range进行任务切换。

         epoll_wait内核调用栈:

         schedule_hrtimeout_range检查是否有传递超时时间,如果没有超时时间,那么不需要启动超时定时器直接调用schedule,schedule保存当前阻塞任务的上下文,然后选择下一个就绪任务执行。

         调用switch_to切换任务上下文:

        切换调用栈:

3、socket可读事件处理

3.1、tcp收到数据(sock_def_readable)

        tcp协议栈收到数据之后调用sock_def_readable检查是否有等待任务;sock_def_readable调用skwq_has_sleeper检查socket等待链表是否不为空(这里的等待链表不是阻塞的任务),如果等待链表不为空,调用wake_up_interruptible_sync_poll唤醒等待链表。

3.2、监听事件检查 

        等待链表的回调函数为ep_poll_callback,sock_def_readable最终调用ep_poll_callback,ep_poll_callback检查epitem的event.events是否有poll任何事件,如果没有poll任务事件,那么不需要处理。

        检查是否有监听当前的事件,如果没有监听那么也不需要处理。

         检查epitem是否已经在rdllink就绪链表里面,如果已经在就绪链表里面,不需要再次加入就是链表,如果不在,那么调用list_add_tail将epitem添加到eventpoll的就绪链表rdllist。

 3.3、唤醒阻塞任务

        将就绪的epitem添加到就绪链表之后,调用epitem检查eventpoll的等待链表是否有阻塞的任务。

         eventpoll的等待链表有阻塞任务,调用wake_up_locked唤醒eventpoll的等待链表任务。

         wake_up_locked最终调用try_to_wake_up唤醒阻塞的任务,try_to_wake_up将阻塞任务的状态更新为TASK_WAKING。

         调用task_waking_fair更新任务的时间片相关时间值,任务睡眠期间没有随系统更新。

        选择唤醒任务运行的线程,调用ttwu_queue将任务添加到目标cpu的就绪任务队列。

         任务放入就绪队列之后,下次被调度将恢复阻塞是的上下文也就是恢复到epoll_wait切换出去的地方继续执行。

4、epoll_wait返回

         sock_def_readable唤醒epoll_wait阻塞的线程之后,阻塞的线程回到epoll_wait继续执行。

4.1、阻塞任务被唤醒继续执行

        阻塞任务唤醒之后,继续执行上下文切换处的代码,schedule_hrtimeout_range内部检查是否是定时器超时导致的唤醒并返回是否超时,如果超时,则设置timed_out为1,否则timed_out为0,如果超时了,后面肯定不能继续阻塞等待事件了,如果因为别的事件导致的任务被唤醒,那么需要继续等待事件。

4.2、检查就绪事件及超时

         再次检查是否有就绪事件或者是否超时,如果有就是事件或者超时,那么break退出循环,不再阻塞等待。

 4.3、将当前任务从等待队列移除

        有就绪事件或者等待超时,将当前任务从eventpoll等待队列删除并更新当前任务的状态为TASK_RUNNING。

 4.4、检查获取就绪事件链表

调用ep_events_available检查rdllist是否有就绪epitem,如果有就绪事件,调用ep_send_events获取的事件链表。

        ep_send_events调用ep_scan_ready_list将就绪事件拷贝到用户态的结果里面。

         函数调用栈:

4.3、listen socket可读检查

        (从代码上看,rdllist应该保存了可读写的item,所以epoll_wait还得调用对应的poll函数再次具体检查是否可读写,返回给用户具体的读写事件... 具体细节暂时略过)

        如下,listen的socket检查可读的方法是检查icsk_accept_queue队列是否为空,是否有等待accept的链接,如果有就返回POLLIN | POLLRDNORM,否则返回0,返回非0的时候,上一级函数会把返回的事件拷贝到用户态就绪事件数组里面。

         函数调用栈:

        ep_send_events_proc调用__put_user将inet_csk_listen_poll的事件拷贝到用户态就绪事件数组(在ARM32上,__put_user大概就是改变页表的DOMAIN,将用户DOMAIN改成内核DOMAIN,这样就可以直接访问用户态内存,拷贝之后再改回原来的DOMAIN):

 4.4、socket数据读写检查

        tcp_poll socket关闭检查,检查SHUTDOWN_MASK(关闭了读/写),检查是否是TCP_CLOSE状态(调用close关闭了socket),检查是否关闭了读,如果关闭了读要返回POLLIN事件,各种事件具体可以参考https://www.man7.org/linux/man-pages/man2/poll.2.html:

        检查socket是否有数据待读取,如果有则需要返回POLLIN事件:

        调用sk_stream_is_writeable检查是否有发送缓存,socket是否可写,如可写,设置POLLOUT事件:

         注意,tcp_poll虽然返回了socket的所有就绪事件,但是上一级函数ep_item_poll会把没有监听的事件清0,所以用户态获取到的是监听的事件以及不可屏蔽的事件(close/shutdown等异常事件),系统调用epoll_ctl的EPOLL_CTL_ADD会强制加上这些不可屏蔽事件的监听。

        函数调用栈:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值