雲風輕量級協程coroutine源碼分析(linux系統下基於ucontext)

雲風coroutine簡介

github源碼地址:https://github.com/cloudwu/coroutine.git

源碼下載後可以直接make,執行。示例程序清晰易懂,基本看完後大多數人都會使用了。

coroutine屬於非常輕量級的協程實現,核心接口coroutine_open、coroutine_close、coroutine_new、coroutine_resume、coroutine_yield。
coroutine_open、coroutine_close:創建和關閉調度器。
coroutine_new:創建一個新的協程。
coroutine_resume:恢復指定協程運行。
coroutine_yield:阻塞當前協程,切換回調度器主流程。

看概念可能有些空,建議直接看 main.c 文件中的示例代碼,5min即可讀懂。

以下是核心源碼介紹。

ucontext(在glibc庫中實現,posix標準)

ucontext爲用戶層程序提供了一組上下文(context)切換的接口。所謂上下文切換,我們可以類比線程的切換,通過ucontext提供的接口,可以直接修改cpu中寄存器值,從而完成程序的跳轉,類似線程的切換。

github源碼地址:
https://github.com/lattera/glibc/blob/master/sysdeps/unix/sysv/linux/x86_64/getcontext.S
https://github.com/lattera/glibc/blob/master/sysdeps/unix/sysv/linux/x86_64/setcontext.S
https://github.com/lattera/glibc/blob/master/sysdeps/unix/sysv/linux/x86_64/swapcontext.S
https://github.com/lattera/glibc/blob/master/sysdeps/unix/sysv/linux/x86_64/makecontext.c

使用介紹見:
https://blog.csdn.net/m0_37329910/article/details/98471368
https://segmentfault.com/p/1210000009166339/read#2-_ucontext_u5206_u6790

getcontext:獲取當前context
setcontext:切換到指定context
makecontext: 用於將一個新函數和堆棧,綁定到指定context中
swapcontext:保存當前context,並且切換到指定context

makecontext

#include <ucontext.h>
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);

The makecontext() function modifies the context pointed to by ucp (which was obtained from a call to getcontext(3)). Before invoking makecontext(), the caller must allocate a new stack for this context and assign its address to ucp->uc_stack, and define a successor context and assign its address to ucp->uc_link.
When this context is later activated (using setcontext(3) or swapcontext()) the function func is called, and passed the series of integer (int) arguments that follow argc; the caller must specify the number of these arguments in argc. When this function returns, the successor context is activated. If the successor context pointer is NULL, the thread exits.

ucontext注意:

  1. 上下文切換,這部分其實也不難,調用接口即可。setcontext或者swapcontext就幫我們完成了所有寄存器值的切換。
  2. 如果不同上下文共用一個棧,則需要注意棧的恢復,如雲風的coroutine中,調度器棧的恢復。
  3. 如果不同上下文使用不同的棧,棧內容不會被破壞,這種情況也就不需要關注第2步了。

核心數據結構

struct schedule {
	char stack[STACK_SIZE];  // 每個協程分配棧大小
	ucontext_t main;  // Posix接口標準中關於創建、保存、切換用戶態上下文的API
	int nco;  // 當前協程個數
	int cap;  // 協程容量(上限)
	int running; // 調用器正在執行的協程ID,沒有協程執行時等於-1
	struct coroutine **co; // 協程位置指針
};

struct coroutine {
	coroutine_func func; // 協程回調
	void *ud; // 回調函數參數
	ucontext_t ctx;  // 協程上下文
	struct schedule * sch; // 該協程位於的調度器
	ptrdiff_t cap; 
	ptrdiff_t size;
	int status;
	char *stack;
};

核心函數

coroutine_open

創建調度器instance並初始化。

struct schedule * coroutine_open(void) {
	struct schedule *S = malloc(sizeof(*S));
	S->nco = 0;
	S->cap = DEFAULT_COROUTINE; // DEFAULT_COROUTINE=16
	S->running = -1;
	S->co = malloc(sizeof(struct coroutine *) * S->cap);
	memset(S->co, 0, sizeof(struct coroutine *) * S->cap);
	return S;
}

coroutine_new

創建新的協程關聯回調,並將該協程指針存入調度器中。

int coroutine_new(struct schedule *S, coroutine_func func, void *ud) {
	struct coroutine *co = _co_new(S, func , ud);
	 // 1.當前協程數量大於等於調度器容許的最大值,擴容爲兩倍,初始化新增的協程指針存儲空間
     // 2.新創建的協程添加到調度器的協程指針存儲空間中
     if (S->nco >= S->cap) {
		int id = S->cap;
        // realloc會保留原指針指向的內容,但是指針值可能會變化。
        // 參考 https://www.cnblogs.com/droidxin/p/3617854.html
		S->co = realloc(S->co, S->cap * 2 * sizeof(struct coroutine *));
		memset(S->co + S->cap , 0 , sizeof(struct coroutine *) * S->cap);
		S->co[S->cap] = co;
		S->cap *= 2;
		++S->nco;
		return id;
	} else {
		int i;
		for (i=0;i<S->cap;i++) {
			int id = (i+S->nco) % S->cap;
            // 找到第一個爲NULL的位置將新的協程指針存下來
			if (S->co[id] == NULL) {
				S->co[id] = co;
				++S->nco;
				return id;
			}
		}
	}
	assert(0);
	return -1;
}

// 創建一個協程instance並初始化
struct coroutine * _co_new(struct schedule *S , coroutine_func func, void *ud) {
	struct coroutine * co = malloc(sizeof(*co));
	co->func = func;
	co->ud = ud;
	co->sch = S;
	co->cap = 0;
	co->size = 0;
	co->status = COROUTINE_READY;
	co->stack = NULL;
	return co;
}
// 協程回調typdef爲coroutine_func
typedef void (*coroutine_func)(struct schedule *, void *ud);

coroutine_resume

協程狀態 COROUTINE_READY:創建新協程的上下環境,關聯回調和棧信息。
協程狀態 COROUTINE_SUSPEND:被阻塞的協程恢復執行。

void coroutine_resume(struct schedule * S, int id) {
	assert(S->running == -1);
	assert(id >=0 && id < S->cap);
	struct coroutine *C = S->co[id];
	if (C == NULL)
		return;
	int status = C->status;
	switch(status) {
	case COROUTINE_READY:
        // 獲取一個上下文,只是爲了給C->ctx賦初值
        // 後面會根據切換目標修改其中部分內容
		getcontext(&C->ctx);
		C->ctx.uc_stack.ss_sp = S->stack;
		C->ctx.uc_stack.ss_size = STACK_SIZE;
		
        // 調度器中上下文存入uc_link,配合後面的swapcontext將
        // 當前函數上下文存入main中,也就同時存入了uc_link中
		C->ctx.uc_link = &S->main; 
		S->running = id;
		C->status = COROUTINE_RUNNING;
		uintptr_t ptr = (uintptr_t)S;
		
        // 新協程上下文關聯回調和參數
		makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, \
                  (uint32_t)ptr, (uint32_t)(ptr>>32));
                  
        // 切換協程,當前執行上下文存入S->main中,將C->ctx載入
        // 寄存器,從而去執行C->ctx關聯的回調      
		swapcontext(&S->main, &C->ctx);
		break;
	case COROUTINE_SUSPEND:
        // 協程恢復執行,首先將協程棧上保存的內容恢復到調度器棧中,然後置標誌,協程切換
		memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
		S->running = id;
		C->status = COROUTINE_RUNNING;
		swapcontext(&S->main, &C->ctx);
		break;
	default:
		assert(0);
	}
}

// mainfunc爲協程上下文關聯的回調函數,其中會解析出調度器地址和將要執行的協程ID,並執行該協程中由用戶
// 指定的回調函數。
// 注意這個函數不一定能一次性執行完,如果用戶傳入的回調函數(即C->func)中調用了coroutine_yield,則
// 會阻塞當前的函數,而直接跳轉回 coroutine_resume的swapcontext(&S->main, &C->ctx);位置處繼續執行。
static void mainfunc(uint32_t low32, uint32_t hi32) {
	uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
	struct schedule *S = (struct schedule *)ptr;
	int id = S->running;
	struct coroutine *C = S->co[id];
	C->func(S,C->ud);
	_co_delete(C);
	S->co[id] = NULL;
	--S->nco;
	S->running = -1;
}

linux2.6.13內核中ucontext相關結構體的定義:

struct ucontext {
	unsigned long	  uc_flags;    
	struct ucontext  *uc_link;  //後繼上下文
	stack_t		  uc_stack; //用戶自定義棧
	struct sigcontext uc_mcontext; //保存當前上下文,即各個寄存器的狀態
	sigset_t	  uc_sigmask; //保存當前線程的信號屏蔽掩碼
};

typedef struct sigaltstack {
	void __user *ss_sp;
	int ss_flags;
	size_t ss_size;
} stack_t;

coroutine_yield

在協程回調函數中調用,從而調度出當前協程。

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);
}

static void _save_stack(struct coroutine *C, char *top) {
   // 這個變量的目的就是爲了定位當前協程的棧頂指針
   // dummy這個變量現在本質上是存儲在 coroutine_resume函數 COROUTINE_READY分支
   // 處理時賦值的棧上 C->ctx.uc_stack.ss_sp = S->stack,所以實際是存儲在調度器
   // 的棧上
	char dummy = 0;
	assert(top - &dummy <= STACK_SIZE);
 
   // 如果調度器棧上存儲有數據,則需要爲協程棧申請空間用來保留協程切換前棧上的內容
	if (C->cap < top - &dummy) {
		free(C->stack);
		C->cap = top-&dummy;
		C->stack = malloc(C->cap);
	}
 
    // 很重要的一步,將調度器棧上數據存儲到協程的棧上,因爲之後就要進行協程切換了,調度器的
    // 棧會被其他協程佔用,所以在切換協程前先要將調度器棧上的數據保存下來,等到下次該協程重新
    // 繼續執行時可以將當時棧上的內容恢復到調度器的棧中
	C->size = top - &dummy;
	memcpy(C->stack, &dummy, C->size);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章