本文 基于 命令./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_sendto
跟 st_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。