C語言使用setjmp和longjmp實現一個簡單的協程
正文
協程是什么呢,有人說是輕量級線程,有人說的用戶級線程,其和線程的區(qū)別可能就是更輕量、操作系統(tǒng)無感的。 其實從根本來說的話,協程本質上就是在一個進程上的程序而已,外部感知不到它的存在。
協程其實我感覺對理解函數壓棧入棧、進程的上下文切換也是非常有幫助的。
以下內容均在 Linux的 x86_64 環(huán)境下實現。 這里不討論其他的實現。
(C/C++)函數的工作原理
對于匯編上的函數來說,就是一個過程。
匯編執(zhí)行的邏輯就是一條指令一條指令的去執(zhí)行,去處理。
這里分為兩個地方,第一個是代碼區(qū),我們的pc指針指向當前指向的指令。
在x86_64下,寄存器$rip存放著 pc 指針。
第二個是棧區(qū)(別杠,這里不討論堆區(qū)、靜態(tài)區(qū)、常量區(qū)等等等),棧是一種先進后出的結構。一般用來存放數據。
在x86_64下,棧頂指針放在$rsp的位置。
$rbp用來存放棧幀的起始。
看一個最簡單的匯編代碼:
int fun() {
return 0;
}
fun:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
popq %rbp
ret
pushq 指令是將 $rbp的值壓入棧中,然后$rsp指針移動。
movq 指令就是將 A 移動到 B 位置去。
popq 指令就是把東西從棧中彈出到A,棧指針移動。
函數入棧示意圖

具體函數傳參和棧幀請看這一篇文章 http://www.dhdzp.com/article/269423.htm
(C/C++)內嵌匯編
C/C++支持我們內嵌匯編在代碼中。 形如:
asm volatile("", :::)
(volatile是為了防止被優(yōu)化掉)
格式為:
asm volatile("InSTructiON List" : Output : Input : Clobber/Modify);
你可以利用匯編來完成賦值操作
int a=114514, b;
asm volatile("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
(linux下)setjmp和longjmp
如何用可以看這一篇文章 http://www.dhdzp.com/article/41250.htm
setjmp和longjmp在本文中主要起到什么作用呢?
切換上下文
啊,好高大上啊,聽不懂。
說人話,就是保存一下當前的寄存器(因為是協程,只有寄存器夠了)
setjmp原理就是保存好當前時刻的寄存器。
然后在longjmp調用的時候,將恢復 jmp_buf所存放的寄存器的值,以達到跨函數跳轉的目的。
這兩個東西就非常適合用來做我們這個的上下文切換。
當然,你也可以用 ucontext來做這一件事情,只不過,我們這個是個簡單的例子罷了。
協程的實現
首先,拋開調度器不談,我們只用關心什么?
獨立的運行空間、上下文...?
對于每一個協程來說,我們自然是不希望開辟在棧上的,(當前棧幀被摧毀\從新利用怎么辦?)
我們可以動態(tài)的分配在堆上,將這一塊內存當為這個協程的棧。
當然,協程是一個函數,并且可以調用另外的函數,(調用另外的函數的時候分配的內存就是這個協程所在的這一塊內存)
現在我們要做三件事情。
- 將
%rsp切換到新分配的堆,而不是用原來有的棧。 - 函數的傳參保存在哪兒。
- 還是就是,協程執(zhí)行完了,主程序肯定不能直接退出,當前協程是應該返回主程序的地址?顯然不可行,需要hook返回地址,讓我們的協程回不去來的位置。
有點像什么呢?
正常的調用是這樣:

我們將push一個新的函數地址進去。

下面是匯編實現:
asm volatile(
"movq %0, %%rsp;" // 更改 %rsp 為 當前分配的堆地址 now
"movq %2, %%rdi;" // 傳參
"pushq %3;" // 拆分call指令,將 自定義的新函數壓入返回地址
"jmp *%1;" // 跳轉到協程執(zhí)行
:
: "b"(now), "d"(func), "a"(arg), "c"(exit_)
: "memory");
結構體定義
#define alignment16(a) ((a) & (~(16 - 1))) // 向前對齊
#define STACK_SIZE 4096
enum co_status {
CO_NEW = 1,
CO_DEAD,
};
struct co {
void (*func)(void *);
void *arg;
enum co_status status;
jmp_buf context;
uint8_t stack[STACK_SIZE];
};
上下文管理
std::vector<co *> context; std::unordered_map<co *, int> has_context; co main_co; co *now_co;
輔助函數
void refresh_context(co *buf) {
if (!has_context.count(buf)) {
context.push_back(buf);
has_context[buf] = context.size() - 1;
}
}
void exit_() {
now_co->status = CO_DEAD;
while (1) {
yield();
}
}
新建協程
注意到rsp的對齊,不對齊rsp會段錯誤
注意堆和棧的增長是反的
co *coroutine(void (*func)(void *), void *arg) {
co *cur = new co;
cur->arg = arg;
cur->func = func;
cur->status = CO_NEW;
void *now = (void *)(alignment16(((uintptr_t)cur->stack + STACK_SIZE)));
int res = setjmp(main_co.context); // 保存當前上下文
refresh_context(&(main_co)); // 刷新上下文
if (res == 0) {
now_co = cur; // 協程創(chuàng)建成功,立馬開始執(zhí)行,直到第一次 yield
asm volatile(
"movq %0, %%rsp;"
"movq %2, %%rdi;"
"pushq %3;"
"jmp *%1;"
:
: "b"(now), "d"(func), "a"(arg), "c"(exit_)
: "memory");
}
return cur;
}
協程讓步
這里用的 0 和 1來區(qū)分是否為切換上下中的讓步和蘇醒操作。
void yield() {
assert(now_co != NULL);
int res = setjmp(now_co->context); // 保存上下文
refresh_context(now_co);
if (res == 0) {
now_co = context[(rand()) % context.size()]; // 挑選幸運觀眾
longjmp(now_co->context, 1); // 跳轉到其他上下文繼續(xù)執(zhí)行
}
}
協程回收
這里,當協程沒有執(zhí)行完,狀態(tài)不為 CO_DEAD 時,當前調用wait的程序就得一直讓出
直到等到 CO_DEAD時 ,將其回收掉。
void wait(co *co_) {
while (co_->status != CO_DEAD) yield();
for (auto v = context.begin(); v != context.end();
v++) // 比較慢,可改用紅黑樹引用刪除節(jié)點
if (*v == co_) {
context.erase(v);
break;
}
has_context.erase(co_);
delete co_;
}
總體代碼和測試代碼
#include <assert.h>
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <queue>
#include <unordered_map>
#define alignment16(a) ((a) & (~(16 - 1))) // 向前對齊
#define STACK_SIZE 4096
enum co_status {
CO_NEW = 1,
CO_DEAD,
};
struct co {
void (*func)(void *);
void *arg;
enum co_status status;
jmp_buf context;
uint8_t stack[STACK_SIZE];
};
std::vector<co *> context;
std::unordered_map<co *, int> has_context;
co main_co;
co *now_co;
char __init_time__ = [] {
srand(time(NULL));
return 0;
}();
void refresh_context(co *buf) {
if (!has_context.count(buf)) {
context.push_back(buf);
has_context[buf] = context.size() - 1;
}
}
void exit_();
co *coroutine(void (*func)(void *), void *arg) {
co *cur = new co;
cur->arg = arg;
cur->func = func;
cur->status = CO_NEW;
void *now = (void *)(alignment16(((uintptr_t)cur->stack + STACK_SIZE)));
int res = setjmp(main_co.context);
refresh_context(&(main_co));
if (res == 0) {
now_co = cur;
asm volatile(
"movq %0, %%rsp;"
"movq %2, %%rdi;"
"pushq %3;"
"jmp *%1;"
:
: "b"(now), "d"(func), "a"(arg), "c"(exit_)
: "memory");
}
return cur;
}
void yield() {
assert(now_co != NULL);
int res = setjmp(now_co->context);
refresh_context(now_co);
if (res == 0) {
now_co = context[(rand()) % context.size()];
longjmp(now_co->context, 1);
}
}
void wait(co *co_) {
while (co_->status != CO_DEAD) yield();
for (auto v = context.begin(); v != context.end();
v++) // 比較慢,可改用紅黑樹引用刪除節(jié)點
if (*v == co_) {
context.erase(v);
break;
}
has_context.erase(co_);
delete co_;
}
void exit_() {
now_co->status = CO_DEAD;
while (1) {
yield();
}
}
int count = 1;
void entry(void *arg) {
for (int i = 0; i < 5; i++) {
printf("task: [%s] seq:[%d] \n", (const char *)arg, count++);
yield();
}
}
int main() {
co *co1 = coroutine(entry, (void *)"a");
co *co2 = coroutine(entry, (void *)"b");
co *co3 = coroutine(entry, (void *)"c");
wait(co1);
wait(co2);
wait(co3);
printf("%d over\n", count);
return 0;
}
效果
task: [a] seq:[1]
task: [b] seq:[2]
task: [a] seq:[3]
task: [a] seq:[4]
task: [a] seq:[5]
task: [b] seq:[6]
task: [a] seq:[7]
task: [c] seq:[8]
task: [c] seq:[9]
task: [b] seq:[10]
task: [b] seq:[11]
task: [c] seq:[12]
task: [b] seq:[13]
task: [c] seq:[14]
task: [c] seq:[15]
16 over
調度順序是隨機的。
總結
本文主要簡單介紹了一個一種可能的協程的實現方法,但是極其簡陋和不規(guī)范,如有紕漏,請指正。
通過對協程的學習和理解,可以大概明白線程的工作原理,進程的工作原理,為什么線程要比進程耗費資源。
可以了解到C/C++函數調用的基礎流程,以及如何搞一個函數讓其不返回等操作。
本文沒有涉及調度,涉及得很簡陋,協程的狀態(tài)只有 新建和死亡。中間的其他狀態(tài)沒有標注。
以上就是C語言使用setjmp和longjmp實現一個簡單的協程的詳細內容,更多關于C語言setjmp longjmp實現協程的資料請關注腳本之家其它相關文章!
相關文章
Microsoft Visual Studio 2022的安裝與使用詳細教程
Microsoft Visual Studio 2022是Microsoft Visual Studio軟件的一個高版本,能夠編寫和執(zhí)行C/C++代碼,具有強大的功能,是開發(fā)C/C++程序的主流軟件,這篇文章主要介紹了Microsoft Visual Studio 2022的安裝與使用詳細教程2024-01-01

