libco简介
libco是微信后台大规模使用的c/c++协程库,2013年至今稳定运行在微信后台的数万台机器上,使得微信后端服务能同时hold大量请求,被誉为微信服务器稳定性的基石。libco在2013年的时候作为腾讯六大开源项目首次开源。libco源码地址。
libco首先能解决CPU利用率与IO利用率不平衡,比用多线程解决IO阻塞CPU问题更高效。因为用户态协程切换比线程切换性能高:线程切换保存恢复的数据更多,需要用户态和内核态切换。其次libco又避免了异步调用和回调分离导致的代码结构破碎。
libco采用epoll多路复用使得一个线程处理多个socket连接,采用钩子函数hook住socket族函数,采用时间轮盘处理等待超时事件,采用协程栈保存、恢复每个协程上下文环境。
为了让大家更容易阅读libco源码,本文以源码为主介绍libco,内容偏底层细节。更多个人文章,欢迎关注作者博客。
设计思想
1. 协程切换
1.1 函数栈
首先复习下进程的地址空间,如图1所示,与本文相关的有代码段、堆、栈。代码段包含应用程序的汇编代码,指令寄存器eip存的是代码段中某一条汇编指令地址,cpu从eip中取出汇编指令的地址,并在代码段中找到对应汇编指令开始执行。CPU执行指令时在栈里存参数、局部变量等数据。代码通过malloc、new在堆上申请内存空间。
图1
图2所示C代码,通过gcc -m32 test.c -o test.o在i386下编译,然后执行gdb test.o。disas main可看到图3所示的main函数汇编码,disas sum可看到图4所示sum函数的汇编代码。调用sum时,main和sum的函数栈如图5所示。图5的表共有两列,第一列为内存地址,第二列为该地址存的内容,除了用“...”省略的内存地址,其他每一行均比上一行低4byte,因为栈地址从高到低增长。
从图5可以看出:一,每个函数的栈在ebp栈底指针和esp栈顶指针之间;二,存在调用关系的两个函数的栈内存地址是相邻的;三,ebp指针指的位置存储的是上级函数的ebp地址,例如sum的ebp 0xffffd598位置存的是main的ebp0xffffd5c8,目的是sum执行后可恢复main的ebp,而main的esp可通过sum的ebp + 4恢复;四,sum的ebp + 4位置,即main的esp位置存的是sum执行后的返回地址0x08048415,该地址不在图1中的栈(Stack)里,而在图1中的代码段里,sum执行后,leave指令恢复ebp、esp,ret指令将esp处的内容0x08048415放到寄存器eip,cpu从eip里取出下一条待执行的指令地址,并根据指令地址从代码段里获取指令执行;五,sum的参数y、x按高地址到低地址,依次存在sum的ebp + 12、ebp + 8位置处。
图2
图3 main函数汇编码
图4 sum函数汇编码
图5 32位系统函数栈
1.2 协程栈
共享栈下文介绍,此处介绍非共享栈。在非共享栈模式下,每个非主协程有自己的栈,而该栈是在堆上分配的,并不是系统栈,但主协程的栈仍然是系统栈,每两个协程的栈地址不相邻。协程栈切换分为第1次、第k次(k>=2)换到目的协程TargetCoroutine。
因为主协程即当前线程的第1次运行是系统调度的,后续才由用户调度,而非主协程每次都由用户调度。所以每次主协程切回的行为都一样,且和非主协程第k次(k>=2)的切回行为一致。
第1次切到TargetCoroutine之前, coctx_make(图6)将函数地址pfn写入协程变量regs[ kEIP ],pfn即为CoRoutineFunc的指针。CoRoutineFunc函数(图7)在第448行调进用户自定义的协程函数UserCoRoutineFunc(图8)。图6中ss_sp为128K协程栈低地址,ss_size为128K将ss_sp+ss_size – sizeof(coctx_param_t)–sizeof(void*)作为esp开始位置,记录在regs[kESP]。因为栈从高到低增长,所以真正的栈空间从高地址ss_sp + ss_size – sizeof(void*) – sizeof(coctx_param_t)增长到低地址ss_sp。这部分空间虽然是协程栈,但实际是通过stack_mem->stack_buffer= (char*)malloc(stack_size)申请的堆空间。CoRoutineFunc、其调用的函数、其调用的函数再调用的函数…的函数栈均在该128K的堆空间里。
图6
图7