ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

详解ucontext

详解ucontext ucontext是一个POSIX / Unix 系统级 C API。在 C 里可以用它但本质上它属于#include ucontext.h它的作用是保存和切换程序执行上下文。可以把它理解成“手动保存当前函数执行现场然后跳到另一个执行现场”。它常被用来实现协程用户态线程green threadfiber简单调度器教学版操作系统/运行时但现代 C 项目里一般更推荐用 C20 coroutine、线程库、Boost.Context、Boost.Coroutine、libco 等。ucontext比较底层也有不少坑。一、什么是 context一个“执行上下文”大致包含这些东西当前 CPU 寄存器当前栈指针当前指令位置信号屏蔽字后续返回到哪里比如程序执行到这里foo();它的状态包括当前执行到哪一行、局部变量在哪个栈上、寄存器里有什么值、函数返回后去哪。ucontext可以把这些状态保存下来将来再恢复。二、ucontext_t核心类型是ucontext_t ctx;它通常包含这些字段具体结构由系统实现决定ucontext_t { ucontext_t* uc_link; sigset_t uc_sigmask; stack_t uc_stack; mcontext_t uc_mcontext; };你平时主要关心这几个ctx.uc_stack ctx.uc_link其中uc_stack这个上下文使用哪块栈。uc_link这个上下文对应的函数执行完之后回到哪个上下文。uc_mcontext底层机器寄存器状态一般不要直接碰。三、四个核心函数ucontext主要有四个函数int getcontext(ucontext_t* ucp); int setcontext(const ucontext_t* ucp); void makecontext(ucontext_t* ucp, void (*func)(), int argc, ...); int swapcontext(ucontext_t* oucp, const ucontext_t* ucp);分别是getcontext保存当前上下文getcontext(ctx);把当前执行状态保存到ctx里。注意如果以后用setcontext(ctx)恢复程序会像从getcontext返回一样继续执行。所以它有点像“时间存档点”。setcontext恢复某个上下文setcontext(ctx);跳转到ctx保存的执行状态。这个函数通常不会正常返回因为它直接切走了。makecontext创建一个新的执行上下文makecontext(ctx, func, argc, ...);它把一个ucontext_t初始化成以后切到这个上下文时从func函数开始执行。不过在调用makecontext之前通常要先getcontext(ctx); ctx.uc_stack.ss_sp stack_memory; ctx.uc_stack.ss_size stack_size; ctx.uc_link main_ctx; makecontext(ctx, func, 0);也就是说先getcontext给它分配一块栈设置函数执行完后回到哪里用makecontext指定入口函数swapcontext保存当前上下文并切到另一个上下文swapcontext(current_ctx, next_ctx);意思是把当前执行状态保存到current_ctx切换到next_ctx以后如果再次切回current_ctx程序会从swapcontext的下一行继续跑。这是实现协程调度最常用的函数。四、一个最小例子下面是一个简单的 C 示例主函数切到协程协程再切回来。#include ucontext.h #include iostream ucontext_t main_ctx; ucontext_t task_ctx; char task_stack[1024 * 64]; void task_function() { std::cout task: start\n; std::cout task: switch back to main\n; swapcontext(task_ctx, main_ctx); std::cout task: resumed\n; std::cout task: end\n; } int main() { getcontext(task_ctx); task_ctx.uc_stack.ss_sp task_stack; task_ctx.uc_stack.ss_size sizeof(task_stack); task_ctx.uc_stack.ss_flags 0; task_ctx.uc_link main_ctx; makecontext(task_ctx, task_function, 0); std::cout main: switch to task\n; swapcontext(main_ctx, task_ctx); std::cout main: back from task\n; std::cout main: switch to task again\n; swapcontext(main_ctx, task_ctx); std::cout main: done\n; }可能输出main: switch to task task: start task: switch back to main main: back from task main: switch to task again task: resumed task: end main: done关键点是这里swapcontext(task_ctx, main_ctx);协程主动让出执行权回到主函数。然后主函数再次swapcontext(main_ctx, task_ctx);协程会从上次暂停的位置继续执行也就是std::cout task: resumed\n;这就是“有栈协程”的感觉。五、uc_link的作用这行很重要task_ctx.uc_link main_ctx;它表示当task_function()执行结束后自动切回main_ctx。如果写成task_ctx.uc_link nullptr;那么task_function()结束后整个线程通常就结束了。举个例子void task_function() { std::cout task end\n; }如果uc_link main_ctx函数结束后回到main。如果uc_link nullptr函数结束后可能直接退出当前线程。六、栈是你自己管理的ucontext是“有栈上下文”。每个通过makecontext创建的上下文通常需要自己的栈char stack[64 * 1024]; ctx.uc_stack.ss_sp stack; ctx.uc_stack.ss_size sizeof(stack);这意味着栈太小会栈溢出栈内存必须在上下文运行期间一直有效不能用已经销毁的局部数组作为栈多个 context 不能随便共享同一块栈比如这样是危险的ucontext_t create_task() { ucontext_t ctx; char stack[64 * 1024]; ctx.uc_stack.ss_sp stack; return ctx; }因为stack是局部变量函数返回后就失效了。更常见的做法是std::vectorchar stack(64 * 1024);或者char* stack new char[64 * 1024];但要注意释放时机。七、用它实现简单协程可以抽象成这样struct Coroutine { ucontext_t ctx; std::vectorchar stack; bool finished false; };主调度器维护多个Coroutine每个协程自己在适当的时候调用yield()void yield() { swapcontext(current-ctx, scheduler_ctx); }调度器再切到另一个协程swapcontext(scheduler_ctx, next-ctx);这种模型叫协作式调度。也就是说切换发生在协程主动让出 CPU 的时候而不是系统强制抢占。类似task A running task A yield scheduler task B running task B yield scheduler task A resume八、 和线程的区别ucontext实现的是用户态上下文切换不是操作系统线程。和std::thread对比项目ucontextstd::thread调度者你自己操作系统是否并行通常不并行可以多核并行切换方式主动yield抢占式调度开销较低较高栈自己管理系统管理安全性坑多更标准可移植性差好很多ucontext更像是“一个线程里模拟多个执行流”。它不能自动利用多核。除非你自己在多个 OS 线程里分别跑多个调度器。九、 和 C20 coroutine 的区别C20 coroutine 是语言级协程但它通常是无栈协程。ucontext是有栈协程。区别很大项目ucontextC20 coroutine类型有栈协程无栈协程暂停位置几乎任意函数层级只能在 coroutine 内co_await/co_yield栈管理手动分配编译器生成 coroutine frame标准性非 C 标准C 标准可移植性较差较好性能模型接近 fiber接近状态机举个直观例子。ucontext可以这样void deep() { yield(); } void middle() { deep(); } void task() { middle(); }只要yield()里切上下文整个调用栈都能暂停。但 C20 coroutine 不是这样。你不能在普通函数深处随便暂停整个调用栈相关函数要参与 coroutine 机制。十、makecontext参数的坑makecontext可以传参数makecontext(ctx, func, 1, value);但这个 API 很老参数传递有历史坑。它原本更适合传int级别的值不保证安全传递指针。比如void func(void* p);你可能想这么写makecontext(ctx, (void (*)())func, 1, ptr);在某些系统上能跑但严格来说可移植性不好尤其是 64 位指针和int参数大小不一致时。很多代码会这么做makecontext(ctx, reinterpret_castvoid (*)()(entry), 1, arg);在 Linux/glibc 某些平台上通常能工作但不要把它当成跨平台保证。更稳一点的设计是入口函数不传复杂参数用全局/静态调度器找到当前协程对象或使用平台已知支持的封装库例如 Boost.Context十一、一个带 yield 的小例子#include ucontext.h #include iostream #include vector ucontext_t main_ctx; ucontext_t coro_ctx; std::vectorchar coro_stack(64 * 1024); void yield_to_main() { swapcontext(coro_ctx, main_ctx); } void coroutine_body() { std::cout coro: 1\n; yield_to_main(); std::cout coro: 2\n; yield_to_main(); std::cout coro: 3\n; } int main() { getcontext(coro_ctx); coro_ctx.uc_stack.ss_sp coro_stack.data(); coro_ctx.uc_stack.ss_size coro_stack.size(); coro_ctx.uc_stack.ss_flags 0; coro_ctx.uc_link main_ctx; makecontext(coro_ctx, coroutine_body, 0); std::cout main: resume coro\n; swapcontext(main_ctx, coro_ctx); std::cout main: resume coro\n; swapcontext(main_ctx, coro_ctx); std::cout main: resume coro\n; swapcontext(main_ctx, coro_ctx); std::cout main: done\n; }输出类似main: resume coro coro: 1 main: resume coro coro: 2 main: resume coro coro: 3 main: done这就已经有点像手写协程了。十二、getcontext的一个经典陷阱看这个代码ucontext_t ctx; int main() { getcontext(ctx); std::cout hello\n; setcontext(ctx); }这会怎样它可能无限输出hello hello hello ...因为setcontext(ctx)恢复到了getcontext(ctx)刚返回的位置。于是继续打印hello再setcontext又回去。所以如果用getcontext/setcontext通常需要一个额外状态变量防止重复int resumed 0; getcontext(ctx); if (!resumed) { resumed 1; setcontext(ctx); }但协程里更常用的是swapcontext逻辑会清楚很多。十三、 生命周期问题这是ucontext最容易出 bug 的地方。比如一个 context 正在使用这块栈std::vectorchar stack;如果你销毁了stack但以后又切回那个 context就炸了。类似Coroutine* c new Coroutine; delete c; swapcontext(main_ctx, c-ctx); // 严重错误要保证context 对象还活着栈内存还活着栈足够大不要切到已经结束的 context不要重复使用已经无效的上下文实际项目里通常要给协程加状态enum class State { Ready, Running, Suspended, Finished };
返回列表