周末阅读了云风八年前用 C 语言实现的协程库。代码非常简洁,只有二百来行,展示了如何基于 ucontext 函数库实现非对称协程(Asymmetric Coroutine),且不同的协程共享同一个栈空间。
运行测试程序的时候发现,测试程序在 Linux 上可以成功运行,然而在 macOS 上却会进入一个死循环,看上去是由于 ucontext 在不同平台的实现不同引起的。于是,花了一些时间阅读 ucontext 在 macOS 上的实现,理解了协程无法继续运行的原因,并提供了一个解决思路。
什么是 ucontext?
ucontext 是指 C 语言库函数中用来控制函数调用上下文(context)的一组函数:getcontext, setcontext, makecontext 和 sawapcontext。这组函数可以被用来实现协程。这组函数作为 POSIX 标准只存在了短短七年时间(从 POSIX.1-2001 到 POSIX.1-2008),如今已经被移除了 POSIX 标准,类似的功能改为由 pthreads (POSIX Threads) 提供。尽管这几个函数已经被移除了标准,我们还能在大部分操作系统中找到实现。以苹果为例,macOS 的 ucontext 实现在其网站开源:https://opensource.apple.com/source/Libc/Libc-825.25/x86_64/gen。云风开源的协程库就是在 ucontext 之上实现的。
光说不练假把式,以下是一个简单的用例:
#define _XOPEN_SOURCE 600L
#include <stdio.h>
#include <stdlib.h>
#include <ucontext.h>
static ucontext_t uctx_main, uctx_func1;
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
static void func1(void) {
printf("func1: started\n");
}
int main(int argc, char *argv[]) {
char func1_stack[1024*1024]; // 131072
if (getcontext(&uctx_func1) == -1)
handle_error("getcontext");
uctx_func1.uc_stack.ss_sp = func1_stack;
uctx_func1.uc_stack.ss_size = sizeof(func1_stack);
uctx_func1.uc_link = &uctx_main;
makecontext(&uctx_func1, func1, 0);
printf("main: swapcontext(&uctx_main, &uctx_func1)\n");
if (swapcontext(&uctx_main, &uctx_func1) == -1)
handle_error("swapcontext");
printf("main: exiting\n");
exit(EXIT_SUCCESS);
}
// Output:
// main: swapcontext(&uctx_main, &uctx_func1)
// func1: started
// main: exiting
swapcontext
要说清楚 bug 的原因,需要阅读 swapcontext 在 macOS 上的实现。由于非常简洁,只有短短十五行,直接上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp) {
int ret;
if ((oucp == NULL) || (ucp == NULL)) {
errno = EINVAL;
return (-1);
}
oucp->uc_flags &= ~UCF_SWAPPED;
ret = getcontext(oucp);
if ((ret == 0) && !(oucp->uc_flags & UCF_SWAPPED)) {
oucp->uc_flags |= UCF_SWAPPED;
ret = setcontext(ucp);
}
return (ret);
}
这里的 8~11 行让人费解,先是将 uc_flags 的 UCF_SWAPPED 位设为 0,然后判断该位是不是为0(难道不是一定为 0 ?),如果为 0, 设为 1 并且执行 setcontext,否则直接返回。
打了断点调试了一下才发现,原来这里的判断语句会被执行两次。如图,从 Main 这个 context 来看,第一次是在首次执行 switchcontext 时,因为 UCF_SWAPPED 位刚刚被设为 0,所以会执行 11~12 行,即切换到 Func context,第二次是在 context 被恢复时,因为 getcontext 记录的是其返回地址,所以恢复之后会从第 10 行继续执行,而由于在第一次执行时,UCF_SWAPPED 已经被设为 1 了,所以这次不会执行 11~12 行。
了解了上述机制之后,再看 coroutine 是如何保存堆栈,然后使用 switchcontext 的。
1
2
3
4
5
6
7
8
9
10
void coroutine_yield(struct schedule * S) {
int id = S->running;
assert(id >= 0);
struct coroutine * C = S->co[id];
assert((char *)&C > S->stack);
_save_stack(C,S->stack + STACK_SIZE);
C->status = COROUTINE_SUSPEND;
S->running = -1;
swapcontext(&C->ctx , &S->main);
}
第 6 行保存堆栈信息,然后在第 9 行调用 swapcontext,由于函数的返回地址是被保存在栈中的,所以在恢复这里保存的 context 时,使用的是第 6 行保存的堆栈,即返回地址为第 7 行,而在 swapcontext 执行完 10~15 行之后,会继续执行 coroutine_yield 的 7~9 行,也就导致 swapcontext 再次被调用,context 被再次切换回 S->main,然而在 S->main 当中会等待协程完成,所以程序进入了死循环。
理解了造成 bug 的原因,修复起来也就不难了。自行实现 swapcontext 的内容,在同一个函数里保存堆栈,然后再执行原有的程序内容即可。
static int save_and_swapcontext(
ucontext_t *oucp,
const ucontext_t *ucp,
struct schedule * S,
struct coroutine *C) {
_save_stack(C, S->stack + STACK_SIZE);
C->status = COROUTINE_SUSPEND;
S->running = -1;
int ret;
if ((oucp == NULL) || (ucp == NULL)) {
errno = EINVAL;
return (-1);
}
oucp->uc_flags &= ~UCF_SWAPPED;
ret = getcontext(oucp);
if ((ret == 0) && !(oucp->uc_flags & UCF_SWAPPED)) {
oucp->uc_flags |= UCF_SWAPPED;
ret = setcontext(ucp);
}
return (ret);
}
...
void coroutine_yield(struct schedule * S) {
int id = S->running;
assert(id >= 0);
struct coroutine * C = S->co[id];
assert((char *)&C > S->stack);
save_and_swapcontext(&C->ctx , &S->main, S, C);
}
总结
因为是第一次尝试阅读汇编语言,单步调试操作系统提供的函数,再加上函数调用时内存空间的分布在脑子里不是一清二楚,最终花费了一天半的时间,才算彻底弄清楚造成死循环的原因。至于为什么相同的代码在 Linux 上可以成功运行,还有待于进一步阅读 swapcontext 在 GNU 中的实现。
在解决这个 bug 的过程中获益良多:阅读和单步调试了一些汇编语言,复习了一些在《程序员的自我修养》中读到的关于函数调用过程的知识,也算对线程/协程是如何实现的有了一些更底层的认识和理解。
参考链接
直接相关
苹果 ucontext 实现相关
- Adventure with ucontext on Linux and Mac - albertnetymk’s notes
- Apple-FOSS-Mirror/Libc
- PSA: avoiding the “ucontext routines are deprecated” error on Mac OS X Snow Leopard
- Stack Overflow - What does -D_XOPEN_SOURCE do/mean?
- Stack Overflow c - Context switching - Is makecontext and swapcontext working here (OSX):switchcontext 在 macOS 上的另一个限制
汇编语言相关
- Low-Level Programming: C, Assembly, and Program Execution on Intel® 64 Architecture
- Notes on x86-64 programming
- Stock Overflow - What registers are preserved through a linux x86-64 function call
- Stock Overflow - What’s the purpose of the LEA instruction?
- Stock Overflow - Word, Doubleword, Quadword
- Stock Overflow - xorl %eax, %eax in x86_64 assembly code produced by gcc
- Understanding C by learning assembly - Blog - Recurse Center
- x64 Cheat Sheet
- X86 64 Register and Instruction Quick Start - CDOT Wiki
- x86 ARITHMETIC AND LOGICAL OPERATIONS
调试 DYLD 相关:
- Stack Overflow - debug disassembled dylib with hopper?
- Reverse Engineering - disassembly - Importing external libraries in Hopper scripts? - Reverse Engineering Stack Exchange
- 0xc010d/DYLDSharedCache.hopperLoader: DYLD shared cache loader for Hopper
- Debugging Dyld - Low Level Bits
- Reverse | Mini灬哆啦
LLDB 相关
- Variable Formatting — The LLDB Debugger
- Stack Overflow - Watch points on memory address
- Stack Overflow - xcode - How to print memory in 0xb0987654 using lldb? - Stack Overflow
Linux 的 swapcontext 实现相关
- swapcontext.S source code [glibc/sysdeps/unix/sysv/linux/x86_64/swapcontext.S] - Woboq Code Browser
- glibc/swapcontext.S at master · git-mirror/glibc · GitHub
Stack Unwinding / _Unwind_Frames_Extra on Linux 相关
- 使用LeakTracer检测android NDK C C 代码中的memory leak EditNew Page
- WolfcsTech
- Deep Wizardry: Stack Unwinding
- Github Search · _Unwind_Frames_Extra
- Using the GNU Compiler Collection (GCC): x86 control-flow protection intrinsics
其他
- Coroutine in Boost - 1.72.0
- danluu.com
- danluu/debugging-stories: A collection of debugging stories. PRs welcome (sorry for the backlog) :-)
- danluu/setjmp-longjmp-ucontext-snippets: Implementing coroutines, channels, message passing, etc.
- davidbalbert/thimble: A small OS that doesn’t do much
- sigaltstack
- sigsetmask
- Stack Overflow - c - Is there something to replace the functions in ucontext.h?