本文 基于 命令./obj/lookupdns www.xianwaizhiyin.net
做讲解,只查询一个域名。
本文 主要分析 st_thread_exit()
函数的内部实现。请看下图:
st_thread_exit()
第一次只会执行 _ST_SWITCH_CONTEXT(thread);
这个代码。其他逻辑先不管,第一次不会跑进去,最后面有一个递归调用的。
下面详细讲解 _ST_SWITCH_CONTEXT()
函数的实现,定义如下:
/*
* Switch away from the current thread context by saving its state and
* calling the thread scheduler
*/
#define _ST_SWITCH_CONTEXT(_thread) \
ST_BEGIN_MACRO \
ST_SWITCH_OUT_CB(_thread); \
if (!MD_SETJMP((_thread)->context)) { \
_st_vp_schedule(); \
} \
ST_DEBUG_ITERATE_THREADS(); \
ST_SWITCH_IN_CB(_thread); \
ST_END_MACRO
上面的代码 ST_SWITCH_OUT_CB
在本文命令中不会执行,先跳过,MD_SETJMP
就是之前讲的 _st_md_cxt_save
,把寄存器信息保存进去 jmp_buf
里面,然后 调 _st_vp_schedule()
。所以 _st_vp_schedule() 是重量级函数,需要仔细讲解。
_st_vp_schedule()
定义如下:
void _st_vp_schedule(void)
{
_st_thread_t *thread;
if (_ST_RUNQ.next != &_ST_RUNQ) {
/* Pull thread off of the run queue */
thread = _ST_THREAD_PTR(_ST_RUNQ.next);
_ST_DEL_RUNQ(thread);
} else {
/* If there are no threads to run, switch to the idle thread */
thread = _st_this_vp.idle_thread;
}
ST_ASSERT(thread->state == _ST_ST_RUNNABLE);
/* Resume the thread */
thread->state = _ST_ST_RUNNING;
_ST_RESTORE_CONTEXT(thread);
}
现在开始逐行代码分析 _st_vp_schedule()
。
_ST_RUNQ
是 _st_this_vp.run_q
,这里的逻辑是取下一个协程,_st_this_vp.run_q
如下图:
从上图可以看到,_st_this_vp.run_q
里面只有1个协程,run_q.next.next
就到头了,是因为我们 lookupdns
只查了1个域名。
_ST_DEL_RUNQ(thread)
是把 这个协程移除出队列,因为协程已经拿出来了,就可以运行这个协程了。
所以 _st_vp_schedule
里面的重点就是 _ST_RESTORE_CONTEXT()
,_ST_RESTORE_CONTEXT
就是恢复之前协程的上下文,然后执行 协程的 start
函数的。
_ST_RESTORE_CONTEXT
的定义如下:
/*
* Restore a thread context that was saved by _ST_SWITCH_CONTEXT or
* initialized by _ST_INIT_CONTEXT
*/
#define _ST_RESTORE_CONTEXT(_thread) \
ST_BEGIN_MACRO \
_ST_SET_CURRENT_THREAD(_thread); \
MD_LONGJMP((_thread)->context, 1); \
ST_END_MACRO
上面这个宏只有两句 代码。_ST_SET_CURRENT_THREAD
就是设置当前协程,比较简单,然后就是 MD_LONGJMP()
调用,所以 MD_LONGJMP
是重点函数
MD_LONGJMP
定义如下,声明一下,MD_LONGJMP
根据不同平台有不同的实现,我的是 Linux + x86_64:
#define MD_LONGJMP(env, val) _st_md_cxt_restore(env, val)
_st_md_cxt_restore
定义如下:
/* _st_md_cxt_restore(__jmp_buf env, int val) */
.globl _st_md_cxt_restore
.type _st_md_cxt_restore, @function
.align 16
_st_md_cxt_restore:
/*
* Restore registers.
*/
movq (JB_RBX*8)(%rdi), %rbx
movq (JB_RBP*8)(%rdi), %rbp
movq (JB_R12*8)(%rdi), %r12
movq (JB_R13*8)(%rdi), %r13
movq (JB_R14*8)(%rdi), %r14
movq (JB_R15*8)(%rdi), %r15
/* Set return value */
test %esi, %esi
mov $01, %eax
cmove %eax, %esi
mov %esi, %eax
movq (JB_PC*8)(%rdi), %rdx
movq (JB_RSP*8)(%rdi), %rsp
/* Jump to saved PC */
jmpq *%rdx
.size _st_md_cxt_restore, .-_st_md_cxt_restore
备注:.align 16
的作用 请看 《AT&T align手册》
通过 gdb
调试汇编可以看到,_st_md_cxt_restore
的两个参数 env
跟 val
分别 传进去了 rdi
跟 esi
寄存器。
/*
* Restore registers.
*/
movq (JB_RBX*8)(%rdi), %rbx
movq (JB_RBP*8)(%rdi), %rbp
movq (JB_R12*8)(%rdi), %r12
movq (JB_R13*8)(%rdi), %r13
movq (JB_R14*8)(%rdi), %r14
movq (JB_R15*8)(%rdi), %r15
上面这段代码 是恢复之前的寄存器的值,比较简单,跳过。
/* Set return value */
test %esi, %esi
test 指令会设置 标志位,标志位的知识请看 《X86汇编入门-标志位》。
说个技巧,可以把相关的宏展开,再用 gdb 对文件行进行断点就比较方便。经过 GDB 调试得知,上面的 test
指令执行之后,把 PF 位置为 0了。具体我就不贴图片了。继续运行下面代码。
mov $01, %eax
cmove %eax, %esi
mov %esi, %eax
movq (JB_PC*8)(%rdi), %rdx
movq (JB_RSP*8)(%rdi), %rsp
/* Jump to saved PC */
jmpq *%rdx
上面的 cmove 指令比较陌生,c 代表 condition(条件),只有满足某个条件才会 执行 mov
操作。所以,这句代码 cmove %eax, %esi
的意思是,如果 ZF 位置1,就把 eax 的值赋值给 esi。推荐阅读《cmov 指令》
现在 对比 _st_md_cxt_save()
与 _st_md_cxt_restore()
函数,如下图:
从上图看,代码逻辑并没有太多特别的地方,重点 就是那个 jmpq *%rdx
。jmp
可以控制指令跳转。跳到协程的执行路径。这里就要仔细研究一下 (JB_PC*8)(%rdi)
保存的是什么,基于之前的文章,这里保存的东西是 _ST_INIT_CONTEXT()
函数处理的。
(JB_PC*8)(%rdi)
指向的地址,就是之前的 st_thread_create()
的 if
判断,我把宏展开了,方便查看。如下图:
之前一直不知道什么情况会跳进去这个 if
,原来就是此时此刻跳进去。因为 _st_md_cxt_restore()
里面执行了 mov $01, %eax
,所以这个条件可以跳进去了。这时候就开始执行 _st_thread_main()
函数。
从上图可以看到,_st_thread_main() 会执行协程的 start 指针函数 (do_resolve),然后再递归 调 st_thread_exit()
。
至此 分析完毕,st_thread_exit
的流程图如下:
下篇文章开始讲解,_st_thread_main() 协程的 start 指针函数 (do_resolve),以及为什么 st_recvfrom()
不会阻塞其他的协程。
相关阅读:
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。QQ:2338195090。