[libco] 协程切换理解思路

程切换,可以理解为函数运行时上下文切换。

文章来源:[libco] 协程切换理解思路


1. 协程切换

正常情况下,函数代码从头到尾串行执行,直到函数生命期结束。而协程切换却能将当前运行的函数,切换到另外一个函数运行,这是协程的神奇之处。


2. 划重点

  • 理解协程切换原理,首先需要理解函数的运行原理。(《x86-64 下函数调用及栈帧原理》
  • 协程是啥?它本质上就是一个函数体,与普通函数相比,它只是特殊一点而已。
  • 协程函数上下文:寄存器数据 + 内存数据。
  • 协程切换(yield/resume)本质是函数运行时上下文切换。
  • 系统默认为函数运行时分配堆栈内存,而 libco 为协程函数分配堆空间(独立栈/共享栈)使其工作。
  • libco 协程切换核心源码在 co_routine.cpp/co_swap()/coctx_swap()coctx_swap 通过汇编实现。
  • 汇编源码的理解,关键对 call/ret 这两个汇编指令理解:call 调用函数,ret 返回函数运行地址;当执行这两个指令时寄存器和程序是如何在内存上压栈出栈的。
  • 用 lldb / gdb 走一下 coctx_swap 这段汇编源码逻辑,观察寄存器与内存数据的变化。

3. 协程上下文

协程上下文:寄存器数据 + 内存数据。


3.1. 协程拓扑结构

struct stCoRoutine_t {
    ...
    coctx_t ctx; /* 协程上下文。 */
    ...
    stStackMem_t *stack_mem; /* 函数在这个内存块上工作。 */
    ...
};

3.2. 内存分配

  • 协程函数运行的内存空间。
struct stStackMem_t {
    stCoRoutine_t *occupy_co; /* 使用该内存块的协程。 */
    int stack_size;           /* 栈大小。 */
    char *stack_bp;           /* 栈底指针。 */
    char *stack_buffer;       /* 栈顶指针。 */
};
  • 协程上下文。
struct coctx_t {
    void *regs[14]; /* 寄存器数组。 */
    size_t ss_size; /* 内存大小。 */
    char *ss_sp;    /* 内存块起始地址。 */
};
  • 协程函数运行时内存空间。
  • 协程运行时内存布局。

4. 协程切换汇编实现功能

co_routine.cpp/co_swap()/coctx_swap() 汇编工作流程。

    ; 将当前协程寄存器数据保存到 curr->ctx->regs
    leaq (%rsp),%rax
    movq %rax, 104(%rdi) ; rsp --> regs[13]
    movq %rbx, 96(%rdi)  ; rbx --> regs[12]
    movq %rcx, 88(%rdi)  ; rcx --> regs[11]
    movq %rdx, 80(%rdi)  ; rdx --> regs[10]
    movq 0(%rax), %rax   ; rax 寄存器指向函数返回地址。 
    movq %rax, 72(%rdi)  ; rax --> regs[9] 
    movq %rsi, 64(%rdi)  ; rsi --> regs[8]
    movq %rdi, 56(%rdi)  ; rdi --> regs[7]
    movq %rbp, 48(%rdi)  ; rbp --> regs[6]
    movq %r8, 40(%rdi)   ; r8  --> regs[5]
    movq %r9, 32(%rdi)   ; r9  --> regs[4]
    movq %r12, 24(%rdi)  ; r12 --> regs[3]
    movq %r13, 16(%rdi)  ; r13 --> regs[2]
    movq %r14, 8(%rdi)   ; r14 --> regs[1]
    movq %r15, (%rdi)    ; r15 --> regs[0]
    xorq %rax, %rax      ; rax = 0x0000000000000000

    ; 将 pending_co->ctx->regs 数据,写入对应寄存器。
    movq 48(%rsi), %rbp  ; regs[6]  --> rbp
    movq 104(%rsi), %rsp ; regs[13] --> rsp
    movq (%rsi), %r15    ; regs[0]  --> r15
    movq 8(%rsi), %r14   ; regs[1]  --> r14
    movq 16(%rsi), %r13  ; regs[2]  --> r13
    movq 24(%rsi), %r12  ; regs[3]  --> r12
    movq 32(%rsi), %r9   ; regs[4]  --> r9
    movq 40(%rsi), %r8   ; regs[5]  --> r8
    movq 56(%rsi), %rdi  ; regs[7]  --> rdi
    movq 80(%rsi), %rdx  ; regs[10] --> rdx
    movq 88(%rsi), %rcx  ; regs[11] --> rcx
    movq 96(%rsi), %rbx  ; regs[12] --> rbx
    leaq 8(%rsp), %rsp   ; rsp 上移 8 个字节。
    pushq 72(%rsi)       ; 将 regs[9] 返回地址压栈。
    movq 64(%rsi), %rsi  ; regs[8]  --> rsi
    ret

5. lldb 调试

调试测试程序(github),观察协程切换寄存器数据和内存数据的变化。

[root:.../other/coroutine/test_libco]# lldb test_libco -- 1 1                                          (main✱) 
Current executable set to 'test_libco' (x86_64).
(lldb) b co_routine.cpp : 664
Breakpoint 1: where = test_libco`co_swap(stCoRoutine_t*, stCoRoutine_t*) + 182 at co_routine.cpp:664, address = 0x0000000000402eb4
(lldb) r
Process 30842 launched: '/home/other/coroutine/test_libco/test_libco' (x86_64)
Process 30842 stopped
* thread #1: tid = 30842, 0x0000000000402eb4 test_libco`co_swap(curr=0x00000000020de590, pending_co=0x00000000020e0730) + 182 at co_routine.cpp:664, name = 'test_libco', stop reason = breakpoint 1.1
    frame #0: 0x0000000000402eb4 test_libco`co_swap(curr=0x00000000020de590, pending_co=0x00000000020e0730) + 182 at co_routine.cpp:664
   661          }
   662 
   663          //swap context
-> 664          coctx_swap(&(curr->ctx),&(pending_co->ctx) );
   665 
   666          //stack buffer may be overwrite, so get again;
   667          stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();
di -l 
test_libco`co_swap(stCoRoutine_t*, stCoRoutine_t*) + 182 at co_routine.cpp:664
   663          //swap context
-> 664          coctx_swap(&(curr->ctx),&(pending_co->ctx) );
   665 
-> 0x402eb4:  movq   -0x40(%rbp), %rax
   0x402eb8:  leaq   0x18(%rax), %rdx
   0x402ebc:  movq   -0x38(%rbp), %rax
   0x402ec0:  addq   $0x18, %rax
   0x402ec4:  movq   %rdx, %rsi
   0x402ec7:  movq   %rax, %rdi
   0x402eca:  callq  0x407fca                  ; coctx_swap
...
# 设置调试打印数据,每执行一步,打印当前汇编编码和寄存器数据。
(lldb) target stop-hook add
Enter your stop hook command(s).  Type 'DONE' to end.
> di -p
> re r rbp rsp rax rsi rdi
> DONE
Stop hook #1 added.
(lldb) si
...
test_libco`co_swap(stCoRoutine_t*, stCoRoutine_t*) + 204 at co_routine.cpp:664:
-> 0x402eca:  callq  0x407fca                  ; coctx_swap
   0x402ecf:  callq  0x403133                  ; co_get_curr_thread_env() at co_routine.cpp:762
   0x402ed4:  movq   %rax, -0x18(%rbp)
   0x402ed8:  movq   -0x18(%rbp), %rax
     rbp = 0x00007ffdcfc18050
     rsp = 0x00007ffdcfc18010
     rax = 0x00000000020de5a8
     rsi = 0x00000000020e0748
     rdi = 0x00000000020de5a8
...
(lldb) si
Process 30842 stopped
* thread #1: tid = 30842, 0x0000000000407fca test_libco`coctx_swap, name = 'test_libco', stop reason = instruction step into
    frame #0: 0x0000000000407fca test_libco`coctx_swap
# 进入 coctx_swap 函数。
test_libco`coctx_swap:
-> 0x407fca:  leaq   (%rsp), %rax
   0x407fce:  movq   %rax, 0x68(%rdi)
   0x407fd2:  movq   %rbx, 0x60(%rdi)
   0x407fd6:  movq   %rcx, 0x58(%rdi)
test_libco`coctx_swap:
-> 0x407fca:  leaq   (%rsp), %rax
   0x407fce:  movq   %rax, 0x68(%rdi)
   0x407fd2:  movq   %rbx, 0x60(%rdi)
   0x407fd6:  movq   %rcx, 0x58(%rdi)
     rbp = 0x00007ffdcfc18050
     rsp = 0x00007ffdcfc18008
     rax = 0x00000000020de5a8
     rsi = 0x00000000020e0748
     rdi = 0x00000000020de5a8
# 查看 rsp 内存内容。
(lldb) me read -fx -s8 -c1 0x00007ffdcfc18008
0x7ffdcfc18008: 0x0000000000402ecf

# 查看该地址的汇编内容,刚好是紧接 coctx_swap 函数下面的代码。
(lldb) di -s 0x0000000000402ecf
test_libco`co_swap(stCoRoutine_t*, stCoRoutine_t*) + 209 at co_routine.cpp:667:
   0x402ecf:  callq  0x403133                  ; co_get_curr_thread_env() at co_routine.cpp:762
   0x402ed4:  movq   %rax, -0x18(%rbp)
   0x402ed8:  movq   -0x18(%rbp), %rax
   0x402edc:  movq   0x418(%rax), %rax
   0x402ee3:  movq   %rax, -0x20(%rbp)
   0x402ee7:  movq   -0x18(%rbp), %rax
(lldb) bt
* thread #1: tid = 30842, 0x0000000000407fca test_libco`coctx_swap, name = 'test_libco', stop reason = instruction step into
  * frame #0: 0x0000000000407fca test_libco`coctx_swap
    frame #1: 0x0000000000402ecf test_libco`co_swap(curr=0x00000000020de590, pending_co=0x00000000020e0730) + 209 at co_routine.cpp:664
    frame #2: 0x0000000000402bf5 test_libco`co_resume(co=0x00000000020e0730) + 165 at co_routine.cpp:568
    frame #3: 0x00000000004022de test_libco`main(argc=3, argv=0x00007ffdcfc181e8) + 458 at test_libco.cpp:145
    frame #4: 0x00007fc67f4ee505 libc.so.6`__libc_start_main + 245
(lldb) f 1
frame #1: 0x0000000000402ecf test_libco`co_swap(curr=0x00000000020de590, pending_co=0x00000000020e0730) + 209 at co_routine.cpp:664
   661          }
   662 
   663          //swap context
-> 664          coctx_swap(&(curr->ctx),&(pending_co->ctx) );
   665 
   666          //stack buffer may be overwrite, so get again;
   # 0x0000000000402ecf 代码。
   667          stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();
(lldb) 

6. 小结

  • 通过 libco 的源码学习,感觉终于比较深入理解协程是啥玩意了。
  • 协程这东西实在太抽象了,不是三言两语能描述清楚,只看源码和资料也是不够的,还是动手画图,写测试例子,上调试器观察寄存器和内存数据。
  • 这里只整理了我自己看源码时的思路,很多细节没有补充进来,也可能有些地方没理解正确。
  • 虽然写了多年代码,但是汇编知识早已还给了老师,都是边看边查,边写日志。

7. 参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容

  • 腾讯开源的Libco协程库,以前看过部分源码,所有的协程都用数组模拟栈表示,里面使用到的技术点有hook系统函数,...
    fooboo阅读 4,585评论 0 9
  • 协程的创建和让出 以下代码基于swoole4.4.5-alpha, php7.1.26 我们按照执行流程去逐步分析...
    张皓旻_24cd阅读 781评论 0 0
  • 对于协程做一个整体的描述,从概念、原理、实现三个方面叙述。侧重有栈协程。 1 概览 1.1 什么是协程 有很多与协...
    chnmagnus阅读 770评论 0 1
  • 如今,微信拥有月活跃用户8亿。 不可否认,当今的微信后台拥有着强大的并发能力。 不过, 正如罗马并非一日建成;微信...
    一凡_44e0阅读 1,021评论 0 0
  • 在分析了各大开源协程库实现后,最终选择参考boost.context的汇编实现,来写tbox的切换内核。 在这过程...
    waruqi阅读 1,371评论 0 50