ST源码分析-st_thread_exit - 弦外之音

/ 0评 / 0

本文 基于 命令./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 的两个参数 envval 分别 传进去了 rdiesi 寄存器。

/*
* 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 *%rdxjmp 可以控制指令跳转。跳到协程的执行路径。这里就要仔细研究一下 (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() 不会阻塞其他的协程。


相关阅读:

  1. X86汇编入门-标志位
  2. 《cmov 指令》

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

发表回复

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