使用 ucontext 在 C 中實現簡易協程

之前寫過一篇文章簡單介紹了一下協程,但是其中具體的實現是用 JavaScript 去寫的,得益於閉包特性,JavaScript 實現協程比較容易,而 C 中並沒有提供現成的閉包特性,那怎麼去實現呢?實際上,閉包只是實現協程的一種方法,我們也可以使用上下文切換的方式去實現協程。

我們還是用斐波那契數列舉例,傳統的子過程調用可以用下面這個圖來表示:

fib 函數與其 caller 函數位於同一個棧上,fib 返回後,caller 需要將棧空間釋放,這樣 fib 函數中的所有臨時變數和寄存器狀態就都銷毀了,我們自然也就沒有辦法再回到 fib 返回前的狀態了。

但是得益於 ucontext 系列函數的幫助,我們可以自行控制函數運行的棧。ucontext 是類 System-V 操作系統中實現的標準,且它是完全位於用戶態的東西,使用它不需要陷入系統調用,十分地輕量級。

<ucontext.h> 中共定義了四個函數:

  • getcontext:使用當前上下文初始化指定ucontext_t 結構體;
  • setcontext:使用指定的 ucontext_t 結構體來恢復當前上下文,這個函數一旦調用成功,是不會返回的,因為 EIP 已經指向待跳轉的狀態了;
  • makecontext:將指定 ucontext_t 結構體修改為執行指定函數,通過這個函數我們可以為即將調用的函數手動分配運行的棧空間;
  • swapcontext:這個函數有點類似 getcontext 和 setcontext 的組合,它首先將當前狀態保存,然後跳轉到新的狀態去。需要注意的是,被保存的狀態 EIP 是這個函數返回後的下一條語句

對於上面的例子,改造成協程後,調用的過程可以這樣表示:

我們構想的 fib 函數大致是這樣的:

void fib() {n int a0 = 0;n int a1 = 1;nn while (1) {n fib_res = a0 + a1;n a0 = a1;n a1 = fib_res;nn // send the result to outer env and hand over the right of control.n // (3)n }n}n

fib 函數在交出控制權後並沒有銷毀其棧空間,而 caller 函數拿回控制權後也可以自行決定是否要繼續讓 fib 執行。想要讓 fib 繼續執行就直接恢復狀態到 (3) 的位置就可以了,下面就是引入 ucontext 後的全部實現:

#include <stdio.h>nn#define _XOPEN_SOURCEn#include <ucontext.h>n#undef _XOPEN_SOURCEnnint fib_res;nucontext_t main_ctx, fib_ctx;nnchar fib_stack[1024 * 32];nnvoid fib() {n // (1)n int a0 = 0;n int a1 = 1;nn while (1) {n fib_res = a0 + a1;n a0 = a1;n a1 = fib_res;nn // send the result to outer env and hand over the right of control.n swapcontext(&fib_ctx, &main_ctx); // (b)n // (3)n }n}nnint main(int argc, char **argv) {n // initialize fib_ctx with current context.n getcontext(&fib_ctx);n fib_ctx.uc_link = 0; // after fib() returns we exit the thread.n fib_ctx.uc_stack.ss_sp = fib_stack; // specific the stack for fib().n fib_ctx.uc_stack.ss_size = sizeof(fib_stack);n fib_ctx.uc_stack.ss_flags = 0;n makecontext(&fib_ctx, fib, 0); // modify fib_ctx to run fib() without arguments.nn while (1) {n // pass the right of control to fib() by swap the context.n swapcontext(&main_ctx, &fib_ctx); // (a)n // (2)n printf("%dn", fib_res);n if (fib_res > 100) {n break;n }n }nn return 0;n}n

由於 macOS 中 deprecated 了 ucontext 相關的函數,加 _XOPEN_SOURCE 的宏定義可以強制開啟 ucontext,Linux 中是可以正常使用的。

代碼中相關的調用我都加了注釋,下面我來簡單描述一下執行的過程:

  1. 首先標號 (a) 的調用會將控制權轉交給 fib 函數同時保存狀態,由於 fib_ctx 之前是由 makecontext 函數修改過的,所以這次跳轉會跳轉到標號 (1) 這個位置。
  2. fib 計算好一次迭代的結果,通過標號 (b) 將控制權交回 main 函數,跳轉到之前保存的狀態,即標號 (2)。
  3. 如果 fib_res <= 100,標號 (a) 繼續執行,由於標號 (b) 的調用保存了之前 fib 的狀態,所以這次跳轉到標號 (3) 的位置,fib 繼續之前的狀態執行。

至此,我們就在 C 中實現了這個簡單的協程。


References:

[1] Implementing coroutines with ucontext

[2] getcontext(3) - Linux manual page

推薦閱讀:

用MSIL寫程序:寫個函數做加法
為何沒有國產的 廣泛流行的編程語言?
函數 為什麼要Currying化,currying化有什麼優點?
學習編程有什麼前景?

TAG:编程语言 | CC | Linux |