日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

libco协程库上下文切换原理详解

發布時間:2025/3/15 编程问答 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 libco协程库上下文切换原理详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

緣起

libco 協程庫在單個線程中實現了多個協程的創建和切換。按照我們通常的編程思路,單個線程中的程序執行流程通常是順序的,調用函數同樣也是 “調用——返回”,每次都是從函數的入口處開始執行。而libco 中的協程卻實現了函數執行到一半時,切出此協程,之后可以回到函數切出的位置繼續執行,即函數的執行可以被“攔腰斬斷”,這種在函數任意位置 “切出——恢復” 的功能是如何實現的呢? 本文從libco 代碼層面對協程的切換進行了剖析,希望能讓初次接觸 libco 的同學能快速了解其背后的運行機理。

函數調用與協程切換的區別

下面的程序是我們常規調用函數的方法:

void A() {cout << 1 << " ";cout << 2 << " ";cout << 3 << " "; }void B() {cout << “x” << " ";cout << “y” << " ";cout << “z” << " "; }int main(void) {A();B(); }

在單線程中,上述函數的輸出為:

1 2 3 x y z

如果我們用 libco 庫將上面程序改造一下:

void A() {cout << 1 << " ";cout << 2 << " ";co_yield_ct(); // 切出到主協程cout << 3 << " "; }void B() {cout << “x” << " ";co_yield_ct(); // 切出到主協程cout << “y” << " ";cout << “z” << " "; }int main(void) {... // 主協程co_resume(A); // 啟動協程 Aco_resume(B); // 啟動協程 Bco_resume(A); // 從協程 A 切出處繼續執行co_resume(B); // 從協程 B 切出處繼續執行 }

同樣在單線程中,改造后的程序輸出如下:

1 2 x 3 y z

可以看出,切出操作是由 co_yield_ct() 函數實現的,而協程的啟動和恢復是由 co_resume 實現的。函數 A() 和 B() 并不是一個執行完才執行另一個,而是產生了 “交叉執行“ 的效果,那么,在單個線程中,這種 ”交叉執行“,是如何實現的呢?

Read the f**king source code!

Talk is cheap, show me code.

下面我們就深入 libco 的代碼來看一下,協程的切換是如何實現的。通過分析代碼看到,無論是 co_yield_ct() 還是 co_resume,在協程切出和恢復時,都調用了同一個函數co_swap,在這個函數中調用了 coctx_swap 來實現協程的切換,這一函數的原型是:

void coctx_swap( coctx_t *,coctx_t* ) asm("coctx_swap");

兩個參數都是 coctx_t *指針類型,其中第一個參數表示要切出的協程,第二個參數表示切出后要進入的協程。

在上篇文章 “x86-64 下函數調用及棧幀原理” 中已經指出,調用子函數時,父函數會把兩個調用參數放入了寄存器中,并且把返回地址壓入了棧中。即在進入 coctx_swap 時,第一個參數值已經放到了 %rdi 寄存器中,第二個參數值已經放到了 %rsi 寄存器中,并且棧指針 %rsp 指向的位置即棧頂中存儲的是父函數的返回地址。進入 coctx_swap 后,堆棧的狀態如下:?

由于coctx_swap 是在 co_swap() 函數中調用的,下面所提及的協程的返回地址就是 co_swap() 中調用 coctx_swap() 之后下一條指令的地址:

void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co) {....// 從本協程切出coctx_swap(&(curr->ctx),&(pending_co->ctx) );// 此處是返回地址,即協程恢復時開始執行的位置stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();....}

coctx_swap 函數是用匯編實現的,我們這里只關注 x86-64 相關的部分,其代碼如下:

coctx_swap:leaq 8(%rsp),%raxleaq 112(%rdi),%rsppushq %raxpushq %rbxpushq %rcxpushq %rdxpushq -8(%rax) //ret func addrpushq %rsipushq %rdipushq %rbppushq %r8pushq %r9pushq %r12pushq %r13pushq %r14pushq %r15movq %rsi, %rsppopq %r15popq %r14popq %r13popq %r12popq %r9popq %r8popq %rbppopq %rdipopq %rsipopq %rax //ret func addrpopq %rdxpopq %rcxpopq %rbxpopq %rsppushq %raxxorl %eax, %eaxret

可以看出,coctx_swap 中并未像常規被調用函數一樣創立新的棧幀。先看前兩條語句:

leaq 8(%rsp),%raxleaq 112(%rdi),%rsp

leaq 用于把其第一個參數的值賦值給第二個寄存器參數。第一條語句用來把 8(%rsp) 的本身的值存入到 %rax 中,注意這里使用的并不是 8(%rsp) 指向的值,而是把 8(%rsp) 表示的地址賦值給了 %rax。這一地址是父函數棧幀中除返回地址外棧幀頂的位置。

在第二條語句 leaq 112(%rdi), %rsp 中,%rdi 存放的是coctx_swap 第一個參數的值,這一參數是指向 coctx_t 類型的指針,表示當前要切出的協程,這一類型的定義如下:

struct coctx_t {void *regs[ 14 ]; size_t ss_size;char *ss_sp;};

因而 112(%rdi) 表示的就是第一個協程的 coctx_t 中 regs[14] 數組的下一個64位地址。而接下來的語句:

pushq %rax pushq %rbxpushq %rcxpushq %rdxpushq -8(%rax) //ret func addrpushq %rsipushq %rdipushq %rbppushq %r8pushq %r9pushq %r12pushq %r13pushq %r14pushq %r15

第一條語句 pushq %rax 用于把 %rax 的值放入到 regs[13] 中,resg[13] 用來存儲第一個協程的 %rsp 的值。這時 %rax 中的值是第一個協程 coctx_swap 父函數棧幀除返回地址外棧幀頂的地址。由于 regs[] 中有單獨的元素存儲返回地址,棧中再保存返回地址是無意義的,因而把父棧幀中除返回地址外的棧幀頂作為要保存的 %rsp 值是合理的。當協程恢復時,把保存的 regs[13] 的值賦值給 %rsp 即可恢復本協程 coctx_swap 父函數堆棧指針的位置。第一條語句之后的語句就是用pushq 把各CPU 寄存器的值依次從 regs 尾部向前壓入。即通過調整%rsp 把 regs[14] 當作堆棧,然后利用 pushq 把寄存器的值和返回地址存儲到 regs[14] 整個數組中。regs[14] 數組中各元素與其要存儲的寄存器對應關系如下:

//------------- // 64 bit //low | regs[0]: r15 | // | regs[1]: r14 | // | regs[2]: r13 | // | regs[3]: r12 | // | regs[4]: r9 | // | regs[5]: r8 | // | regs[6]: rbp | // | regs[7]: rdi | // | regs[8]: rsi | // | regs[9]: ret | //ret func addr, 對應 rax // | regs[10]: rdx | // | regs[11]: rcx | // | regs[12]: rbx | //hig | regs[13]: rsp |

接下來的匯編語句:

movq %rsi, %rsppopq %r15popq %r14popq %r13popq %r12popq %r9popq %r8popq %rbppopq %rdipopq %rsipopq %rax //ret func addrpopq %rdxpopq %rcxpopq %rbxpopq %rsp

這里用的方法還是通過改變%rsp 的值,把某塊內存當作棧來使用。第一句 movq %rsi, %rsp 就是讓%rsp 指向 coctx_swap 第二個參數,這一參數表示要進入的協程。而第二個參數也是coctx_t 類型的指針,即執行完 movq 語句后,%rsp 指向了第二個參數 coctx_t 中 regs[0],而之后的pop 語句就是用 regs[0-13] 中的值填充cpu 的寄存器,這里需要注意的是popq 會使得 %rsp 的值增加而不是減少,這一點保證了會從 regs[0] 到regs[13] 依次彈出到 cpu 寄存器中。在執行完最后一句 popq %rsp 后,%rsp 已經指向了新協程要恢復的棧指針(即新協程之前調用 coctx_swap 時父函數的棧幀頂指針),由于每個協程都有一個自己的棧空間,可以認為這一語句使得%rsp 指向了要進入協程的棧空間。

coctx_swap 中最后三條語句如下:

pushq %raxxorl %eax, %eaxret

pushq %rax 用來把 %rax 的值壓入到新協程的棧中,這時 %rax 是要進入的目標協程的返回地址,即要恢復的執行點。然后用 xorl 把 %rax 低32位清0以實現地址對齊。最后ret 語句用來彈出棧的內容,并跳轉到彈出的內容表示的地址處,而彈出的內容正好是上面 pushq %rax 時壓入的 %rax 的值,即之前保存的此協程的返回地址。即最后這三條語句實現了轉移到新協程返回地址處執行,從而完成了兩個協程的切換。可以看出,這里通過調整%rsp 的值來恢復新協程的棧,并利用了 ret 語句來實現修改指令寄存器 %rip 的目的,通過修改 %rip 來實現程序運行邏輯跳轉。注意%rip 的值不能直接修改,只能通過 call 或 ret 之類的指令來間接修改。

整體上看來,協程的切換其實就是cpu 寄存器內容特別是%rip 和 %rsp 的寫入和恢復,因為cpu 的寄存器決定了程序從哪里執行(%rip) 和使用哪個地址作為堆棧 (%rsp)。寄存器的寫入和恢復如下圖所示:

執行完上圖的流程,就將之前 cpu 寄存器的值保存到了協程A 的 regs[14] 中,而將協程B regs[14] 的內容寫入到了寄存器中,從而使執行邏輯跳轉到了 B 協程 regs[14] 中保存的返回地址處開始執行,即實現了協程的切換(從A 協程切換到了B協程執行)。

結語

為實現單線程中協程的切換,libco 使用匯編直接讀寫了 cpu 的寄存器。由于通常我們在高級語言層面很少接觸上下文切換的情形,因而會覺得在單線程中切換上下文的方法會十分復雜,但當我們對代碼抽絲剝繭后,發現其實現機理也是很容易理解的。從libco 上下文切換中可以看出,用匯編與 cpu 硬件寄存器配合竟然可以設計出如此神奇的功能,不得不驚嘆于 cpu 硬件設計的精妙。

libco 庫的說明中提及這種上下文切換的方法取自 glibc,看來基礎庫中隱藏了不少 “屠龍之技”。

看來,想要提高編程技能,無他,Read the f**king source code !

The End.

新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!

總結

以上是生活随笔為你收集整理的libco协程库上下文切换原理详解的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。