腾讯开源的 libco 号称千万级协程支持,那个共享栈模式原理是什么?

揭秘:微信如何用 libco 支撑8亿用户 共享栈模式那一段没大看懂,谁能讲讲? 还有,这种共享栈模式主要用于哪些场景?
关注者
652
被浏览
22,673

14 个回答

这个其实是作者写的太玄乎,试想操作系统中有500个任务,而系统只有固定的255个tss段,怎么办?分时复用,当前被调度的任务可以获得一个TSS段,然后把自己的TSS内容拷贝到那个TSS段,等到这个进程的时间用完,就把自己的TSS拷贝到一边保存起来。下一个被调度的再拷贝进来。

就是一个碗10个人用来吃饭。
Windows和Linux的任务调度都是这个思想和实现。
很想邀请leiffy过来答一发,但在知乎上找不到他的账户。我先来强答一下

首先这个「共享栈」也可以叫作 Copy Stack ,意思是栈空间是所有的协程共享的,在切换的时候通过把协程栈的内容copy-in/copy-out来实现栈的切换。

我们来一步步看关键调用:

在协程环境初始化时,要先调用 (co_alloc_sharestack) 来分配共享栈的内容,其中第一个参数 count 是指分配多少个共享栈,stack_size 是指每个栈的大小 libco/co_routine.cpp at master · Tencent/libco · GitHub ,分配出来的结构名是 stShareStack_t 。

共享栈的结构是一个数组,它里面有 count 个元素,每个元素都是一个指向一段内存的指针 stStackMem_t 。在新分配协程时 (co_create_env) ,它会从刚刚分配的 stShareStack_t 中,按 RoundRobin 的方式取一个 stStackMem_t 出来,然后就算作是这个协程自己的栈。显然,这个时候这个空间是与其它协程共享的,因此叫「共享栈」。

因为 libco 实现的是 Stackful Coroutine ,这种协程的主要特点是每个协程都有独立的栈,所有的局部变量等信息都会保存在栈上。若所有协程都共享一个栈(或N个栈,N < 总协程数),那么如何保证在协程切换时 (context swap) ,能顺利恢复执行环境?

答案就是 memcpy 。关键调用是 co_swap (libco/co_routine.cpp at master · Tencent/libco · GitHub) 。

我们看到,co_swap 中,在切换前,先对当前正在执行的 Coroutine 执行 save_stack_buffer ,然后再真正调用 coctx_swap (汇编,Swap寄存器)。而在 coctx_swap 之后,libco/co_routine.cpp at master · Tencent/libco · GitHub ,此时 Coroutine 是被 co_resume 重新唤醒了,这时会把保存了的栈数据,重新 memcpy 到共享栈上 (copy-in),再继续执行。

那么 save_stack_buffer 是干啥的?libco/co_routine.cpp at master · Tencent/libco · GitHub

很明显了,它通过计算 bp 到 sp 的距离,知道目前这个协程使用了的栈空间的大小,然后通过malloc分配一段这么大的空间,把栈上的内容全部复制进去(aka. Copy Stack, copy-out),栈上的内容也一样是储存在每个协程自己的结构 stCoroutine_t 上,因此每个协程依然有自己独立的栈空间。

这种做法有什么好处?其实我们可以直接想想以前的方法(每个协程单独分配栈)有什么坏处好了:
  • 以前的方法为每个协程都单独分配一段内存空间,因为是固定大小的,实际使用中协程并不能使用到这么大的内存空间,于是就会造成非常大的内存浪费(有同学一定会问为什么不用 Split Stack ,这个东西的性能有多垃圾有目共睹)。而且因为绝大多数协程使用的栈空间都极少,复制栈空间的开销非常小。
  • 因为协程的调度是非抢占的(non-preempt),而在 libco 中,切换的时机都是做 I/O 的时候,并且只有在切换的时候才会去复制栈空间,所以开销也可控。