C語(yǔ)言使用setjmp和longjmp實(shí)現(xiàn)一個(gè)簡(jiǎn)單的協(xié)程
正文
協(xié)程是什么呢,有人說(shuō)是輕量級(jí)線程,有人說(shuō)的用戶級(jí)線程,其和線程的區(qū)別可能就是更輕量、操作系統(tǒng)無(wú)感的。 其實(shí)從根本來(lái)說(shuō)的話,協(xié)程本質(zhì)上就是在一個(gè)進(jìn)程上的程序而已,外部感知不到它的存在。
協(xié)程其實(shí)我感覺(jué)對(duì)理解函數(shù)壓棧入棧、進(jìn)程的上下文切換也是非常有幫助的。
以下內(nèi)容均在 Linux的 x86_64 環(huán)境下實(shí)現(xiàn)。 這里不討論其他的實(shí)現(xiàn)。
(C/C++)函數(shù)的工作原理
對(duì)于匯編上的函數(shù)來(lái)說(shuō),就是一個(gè)過(guò)程。
匯編執(zhí)行的邏輯就是一條指令一條指令的去執(zhí)行,去處理。
這里分為兩個(gè)地方,第一個(gè)是代碼區(qū),我們的pc指針指向當(dāng)前指向的指令。
在x86_64下,寄存器$rip
存放著 pc 指針。
第二個(gè)是棧區(qū)(別杠,這里不討論堆區(qū)、靜態(tài)區(qū)、常量區(qū)等等等),棧是一種先進(jìn)后出的結(jié)構(gòu)。一般用來(lái)存放數(shù)據(jù)。
在x86_64下,棧頂指針?lè)旁?code>$rsp的位置。
$rbp
用來(lái)存放棧幀的起始。
看一個(gè)最簡(jiǎn)單的匯編代碼:
int fun() { return 0; }
fun: pushq %rbp movq %rsp, %rbp movl $0, %eax popq %rbp ret
pushq
指令是將 $rbp
的值壓入棧中,然后$rsp
指針移動(dòng)。
movq
指令就是將 A 移動(dòng)到 B 位置去。
popq
指令就是把東西從棧中彈出到A,棧指針移動(dòng)。
函數(shù)入棧示意圖
具體函數(shù)傳參和棧幀請(qǐng)看這一篇文章 http://www.dbjr.com.cn/article/269423.htm
(C/C++)內(nèi)嵌匯編
C/C++支持我們內(nèi)嵌匯編在代碼中。 形如:
asm volatile("", :::)
(volatile是為了防止被優(yōu)化掉)
格式為:
asm volatile("InSTructiON List" : Output : Input : Clobber/Modify);
你可以利用匯編來(lái)完成賦值操作
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.dbjr.com.cn/article/41250.htm
setjmp
和longjmp
在本文中主要起到什么作用呢?
切換上下文
啊,好高大上啊,聽(tīng)不懂。
說(shuō)人話,就是保存一下當(dāng)前的寄存器(因?yàn)槭菂f(xié)程,只有寄存器夠了)
setjmp
原理就是保存好當(dāng)前時(shí)刻的寄存器。
然后在longjmp
調(diào)用的時(shí)候,將恢復(fù) jmp_buf
所存放的寄存器的值,以達(dá)到跨函數(shù)跳轉(zhuǎn)的目的。
這兩個(gè)東西就非常適合用來(lái)做我們這個(gè)的上下文切換。
當(dāng)然,你也可以用 ucontext
來(lái)做這一件事情,只不過(guò),我們這個(gè)是個(gè)簡(jiǎn)單的例子罷了。
協(xié)程的實(shí)現(xiàn)
首先,拋開(kāi)調(diào)度器不談,我們只用關(guān)心什么?
獨(dú)立的運(yùn)行空間、上下文...?
對(duì)于每一個(gè)協(xié)程來(lái)說(shuō),我們自然是不希望開(kāi)辟在棧上的,(當(dāng)前棧幀被摧毀\從新利用怎么辦?)
我們可以動(dòng)態(tài)的分配在堆上,將這一塊內(nèi)存當(dāng)為這個(gè)協(xié)程的棧。
當(dāng)然,協(xié)程是一個(gè)函數(shù),并且可以調(diào)用另外的函數(shù),(調(diào)用另外的函數(shù)的時(shí)候分配的內(nèi)存就是這個(gè)協(xié)程所在的這一塊內(nèi)存)
現(xiàn)在我們要做三件事情。
- 將
%rsp
切換到新分配的堆,而不是用原來(lái)有的棧。 - 函數(shù)的傳參保存在哪兒。
- 還是就是,協(xié)程執(zhí)行完了,主程序肯定不能直接退出,當(dāng)前協(xié)程是應(yīng)該返回主程序的地址?顯然不可行,需要hook返回地址,讓我們的協(xié)程回不去來(lái)的位置。
有點(diǎn)像什么呢?
正常的調(diào)用是這樣:
我們將push一個(gè)新的函數(shù)地址進(jìn)去。
下面是匯編實(shí)現(xiàn):
asm volatile( "movq %0, %%rsp;" // 更改 %rsp 為 當(dāng)前分配的堆地址 now "movq %2, %%rdi;" // 傳參 "pushq %3;" // 拆分call指令,將 自定義的新函數(shù)壓入返回地址 "jmp *%1;" // 跳轉(zhuǎn)到協(xié)程執(zhí)行 : : "b"(now), "d"(func), "a"(arg), "c"(exit_) : "memory");
結(jié)構(gòu)體定義
#define alignment16(a) ((a) & (~(16 - 1))) // 向前對(duì)齊 #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;
輔助函數(shù)
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(); } }
新建協(xié)程
注意到rsp的對(duì)齊,不對(duì)齊rsp會(huì)段錯(cuò)誤
注意堆和棧的增長(zhǎng)是反的
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); // 保存當(dāng)前上下文 refresh_context(&(main_co)); // 刷新上下文 if (res == 0) { now_co = cur; // 協(xié)程創(chuàng)建成功,立馬開(kāi)始執(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; }
協(xié)程讓步
這里用的 0 和 1來(lái)區(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()]; // 挑選幸運(yùn)觀眾 longjmp(now_co->context, 1); // 跳轉(zhuǎn)到其他上下文繼續(xù)執(zhí)行 } }
協(xié)程回收
這里,當(dāng)協(xié)程沒(méi)有執(zhí)行完,狀態(tài)不為 CO_DEAD
時(shí),當(dāng)前調(diào)用wait的程序就得一直讓出
直到等到 CO_DEAD時(shí)
,將其回收掉。
void wait(co *co_) { while (co_->status != CO_DEAD) yield(); for (auto v = context.begin(); v != context.end(); v++) // 比較慢,可改用紅黑樹(shù)引用刪除節(jié)點(diǎn) if (*v == co_) { context.erase(v); break; } has_context.erase(co_); delete co_; }
總體代碼和測(cè)試代碼
#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))) // 向前對(duì)齊 #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++) // 比較慢,可改用紅黑樹(shù)引用刪除節(jié)點(diǎn) 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
調(diào)度順序是隨機(jī)的。
總結(jié)
本文主要簡(jiǎn)單介紹了一個(gè)一種可能的協(xié)程的實(shí)現(xiàn)方法,但是極其簡(jiǎn)陋和不規(guī)范,如有紕漏,請(qǐng)指正。
通過(guò)對(duì)協(xié)程的學(xué)習(xí)和理解,可以大概明白線程的工作原理,進(jìn)程的工作原理,為什么線程要比進(jìn)程耗費(fèi)資源。
可以了解到C/C++函數(shù)調(diào)用的基礎(chǔ)流程,以及如何搞一個(gè)函數(shù)讓其不返回等操作。
本文沒(méi)有涉及調(diào)度,涉及得很簡(jiǎn)陋,協(xié)程的狀態(tài)只有 新建和死亡。中間的其他狀態(tài)沒(méi)有標(biāo)注。
以上就是C語(yǔ)言使用setjmp和longjmp實(shí)現(xiàn)一個(gè)簡(jiǎn)單的協(xié)程的詳細(xì)內(nèi)容,更多關(guān)于C語(yǔ)言setjmp longjmp實(shí)現(xiàn)協(xié)程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決c++?error:crosses?initialization?of?問(wèn)題
最近在寫代碼的時(shí)候,碰到了?crosses?initialization?of?...?的問(wèn)題,只因我在?switch?的某個(gè)?case?分支下定義了一個(gè)變量,于是乎便將這個(gè)問(wèn)題整理一下,需要的朋友可以參考下2023-03-03C++11特性小結(jié)之decltype、類內(nèi)初始化、列表初始化返回值
這篇文章主要介紹了C++11特性小結(jié)之decltype、類內(nèi)初始化、列表初始化返回值,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05C語(yǔ)言完整實(shí)現(xiàn)12種排序算法(小結(jié))
本文主要介紹了C語(yǔ)言完整實(shí)現(xiàn)12種排序算法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05Qt實(shí)現(xiàn)TCP同步與異步讀寫消息的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何在?Qt?中實(shí)現(xiàn)?TCP?客戶端和服務(wù)器的同步和異步讀寫消息,有需要的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-04-04用C語(yǔ)言實(shí)現(xiàn)圣誕樹(shù)(簡(jiǎn)易版+進(jìn)階版)
大家好,本篇文章主要講的是用C語(yǔ)言實(shí)現(xiàn)圣誕樹(shù)(簡(jiǎn)易版+進(jìn)階版),感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12