ST源码分析-运行协程 - 弦外之音

/ 0评 / 0

本文 基于 命令./obj/lookupdns www.xianwaizhiyin.net www.baidu.com 做讲解,查询两个个域名。

lookupdns 里面的 do_resolve() 函数前面的执行步奏如下:

本文 主要 讲解 do_resolve() 函数的逻辑,以及它里面使用的 st_recvfrom() 为何不会阻塞另一个协程的 do_resolve() 的运行。

do_resolve() 里面一些 域名相关的 API 函数的调用,本文不过多讲解。do_resolve() 的大致逻辑就是 调 操作系统提供的域名API函数,封装好一个 UDP 包,然后 调 st_sendto(),再调 st_recvfrom() 接受回来的 udp 包,然后解析回来的 udp 包,提取出 域名对应的IP。本文的重点是 st_sendto()st_recvfrom()



do_resolve() 函数的流程图如下:

贴一个数据结构图,方便大家参考:

先讲解一下 st_sendto() 函数的实现,这个函数属于 IO 函数,所以在 io.c 文件里面:

从上图可以看,st_sendto() 函数内部调了 操作系统的函数 sendto() 发送数据,这里一般 n 不会小于 0,所以暂时先不理会 st_netfd_poll()

接着看 st_recvfrom() 函数的实现,如下图:

st_recvfrom() 函数内部 也是 调了操作系统函数 recvfrom() ,因为之前在创建 fd 的时候把 fd 设置成非阻塞的了,所以会跑进去,调用 st_netfd_poll()

接下来仔细讲讲 st_netfd_poll() 的实现,猜测一下,他这个函数内部应该会保存当前协程的上下文,也就是用 MD_SETJMP 来保存寄存器,信号来了,协程就会被激活,然后跳回来这里的 if 判断,然后就会再执行一遍 while

st_netfd_poll() 流程图如下:

_ST_ADD_SLEEPQ() 函数是处理 timeout 超时的,会另起一篇文章讲解,本文不讲。

上面的流程图,有以下重点:

1,用 _st_select_pollset_add()pollfd 套接字加进去全局变量 _st_select_data->fd_read_set 集合

2,把 _st_pollq_t pq加进去 _st_this_vp.io_q 队列,这里注意 _st_pollq_t 是一个新的数据结构。之前没提及过。

这里注意,这个 _ST_ADD_IOQ(pq);pq 是局部变量,把局部链接加进去全局队列有些奇怪,但是不用担心,后面切换上下文的时候,函数还未退出,所以局部变量还没销毁。 为什么要加进去 _st_this_vp.io_q ?是为了后面 select() 之后能找到这个 pq

3,把协程状态 从 _ST_ST_RUNNING 切换到 _ST_ST_IO_WAIT

4,_ST_SWITCH_CONTEXT() ,保存当前协程的上下文,用 _st_vp_schedule() 再次调度,因为本文查了两个域名,调度就会切换到领一个协程来查另一个域名。

所以,第四点 就是协程的精髓,st_recvfrom() 虽然看起来是阻塞,但是实际上不是阻塞,而是被 _ST_SWITCH_CONTEXT() 函数里面的 MD_LONGJMP 跳转到另一个函数去运行了。所以看起来是阻塞了。


注意,上面分析的所有流程,还没开始执行 select() 函数,select() 函数是操作系统的函数,这个函数才真正开始阻塞。我猜测应该是在 idle_thread 协程里面进行 select() 操作。

idle 协程的 start 函数是 _st_idle_thread_start(),就下来就详细分析一下这个函数。流程图如下:

果然 在 _st_select_dispatch() 函数里面执行了 select()。代码里面的 宏 _ST_VP_IDLE 实际上就是 _st_select_dispatch()

可以看到,他是执行完所有协程的 sendto 之后,发送域名查询请求之后,再切换到 idl 协程来进行阻塞,等待对面的数据到来,跟我们手动执行所有 sendto,再执行 select/epoll 一样。只是协程的 st_sendtost_recvfrom 看起来是顺序执行的

这里说两个个扩展的知识点:

1,代码里经常看到 epds 变量名,这个其实是 end pds 的缩写,npds 是 number pds 的缩写。

2,_ST_SELECT_READ_CNT 宏 其实是统计有多少个协程监听这个套接字。如果减到 0 就会用 FD_CLR()fd_set 里面删掉

#define _ST_SELECT_READ_CNT(fd)  (_st_select_data->fd_ref_cnts[fd][0])

_st_select_dispatch() 执行完之后,之前的查询域名协程,就会从 _ST_ST_IO_WAIT 变成 _ST_ST_RUNNABLE

_st_vp_check_clock() 函数是处理 IO 等待超时的协程了,本文不讲。

最后就会调 _ST_SWITCH_CONTEXT(me); 重新调度线程,因为 do_resolve 协程 变成 _ST_ST_RUNNABLE,所以 _st_vp_schedule() 再次调度,之前的 do_resolve 就能再次跑起来了。

到这里,是时候画一张 do_resolve 的整体流程图,如下:

上图就是 do_resolve 函数在 系统线程中的执行顺序,可以看到,都是一个线程之间在各个函数中跳来跳去。

注意看 那个 if 判断,是从 idl 协程的 _ST_SWITCH_CONTEXT() 调度跳回来的。

跳回来之后 st_recvfrom() 就拿到数据,就继续执行域名解析,执行完之后,协程就退出了。

TODO:

1,另写一篇文章,讲一下 me->flags &= ~_ST_FL_INTERRUPT; 什么情况协程会被中断。


相关阅读:


由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。QQ:2338195090。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注