在前一篇文章《基於彙編的 C/C++ 協程 - 背景知識》中提到一個用於 C/C++ 的協程所須要實現的兩大功能:html
其中調度,其實在技術實現上與其餘的線程、進程調度沒有什麼特別的差別,同時也要看具體業務的需求。限制 C/C++ 協程應用的最大技術條件是上下文切換。理由在前文也說了。linux
既然本系列講的是基於彙編的 C/C++ 協程,那麼這篇文章咱們就來說講使用匯編來進行上下文切換的原理。git
本文地址:http://www.javashuo.com/article/p-pqrzzlzl-da.htmlgithub
首先咱們須要明白上下文切換具體須要作什麼工做。我想,看這篇文章的讀者應該對編譯原理和操做系統基礎知識已經有必定的基礎了吧?小程序
協程的切換要作的事情,和進程的切換,實際上是差很少的。這裏咱們將本文涉及的要點提一下:segmentfault
當進程開始執行、以及進程執行結束的時候,操做系統還有別的工做:微信
當觸發進程切換時(不管是進程調用阻塞的系統調用,可是操做系統主動觸發 schedule),操做系統要作如下的幾件事情:架構
沒有調查就沒有發言權,沒有實驗也就沒有講解權。實際上本人已經有實現的代碼了。後文就以個人代碼爲脈絡來講明。oracle
相關說明:異步
程序入口參見 main.cpp 文件的第 67 至 91 行,_true_main()
函數。
建立協程使用的是 AMCCoroutineAdd()
函數,函數定義在這裏。能夠參照 struct _CoroutineInfo
結構體。
要執行協程,咱們須要爲協程做如下準備:
協程執行起來就像進程同樣,須要有堆棧來實現函數調用。線程的堆棧是由操做系統分配的;協程因爲工做在用戶態,所以只能由咱們寫代碼分配了。
在個人代碼中,棧空間使用 mmap()
分配。固然也可使用 malloc()
——libco
就是這麼作的。
棧空間的使用,是經過向棧寄存器
直接賦值來實現的。這在後面再講。
協程函數入口其實就是提供的協程函數自己,所以咱們只須要直接將函數的地址直接保存下來就好了。
可是協程出口就比較複雜了。協程執行到出口位置時(也就是協程函數的 return
語句)即表明協程結束。此時協程庫應該可以正確捕捉而且記錄下協程結束的狀態,而且正確的切換到下一個應當被切換的堆棧。
被切換至的堆棧,多是另外一個協程,也有多是協程庫的調用線程。
這一段代碼我使用太重定向協程函數返回地址來實現的,須要搭配彙編使用。能夠參見代碼中 _coroutine_did_end()
函數。該函數在協程初始化的時候,保存在了 func_ret_addr
成員變量中。
請注意這個變量在結構體中的偏移值:64,下文的 asm_amc_coroutine_enter()
彙編函數就用上了。
當切換協程時,須要切換函數的上下文。切換上下文也稱爲 「保存現場」 和 「恢復現場」。所謂的 「現場」,其實就是必要的 CPU 寄存器值,這些寄存器裏就已經包含了協程的堆棧。
參考資料用戶態調度要保存些什麼中就說明了在 GCC 程序中,須要保存的寄存器內容(x86_64 / x64):
線程調用保存的環境更多,不過做爲協程,咱們只須要保存上面這些寄存器就夠了。
啓動線程的入口是 AMCCoroutineRun()
函數。函數的基本邏輯以下:
asm_amc_coroutine_dump(g_pMainThreadInfo); // dump main thread again to get return point of this function. g_pMainThreadInfo->reg_rsp += 1 * sizeof(uint64_t); // ignore return address for function "asm_amc_coroutine_dump"
協程要求單線程執行。本文所謂的主線程,指的就是啓動協程的線程。這兩句的邏輯以下:
asm_amc_coroutine_dump()
將主線程的上下文保存在一個全局變量中asm_amc_coroutine_dump()
中保存的函數返回地址,使得全局變量中保存的是 AMCCoroutineRun()
的返回地址。調用匯編函數 asm_amc_coroutine_enter()
,直接進入協程。函數很簡單:
asm_amc_coroutine_enter: movq (%rdi), %rbx movq 8(%rdi), %rsp movq 16(%rdi), %rbp push 64(%rdi) # create a function return point jmp 56(%rdi)
五句命令的含義分別是:
func_ret_addr
成員,將這個地址壓入堆棧,使得協程函數結束時即進入相應的函數中,這樣咱們就能夠檢測到一個協程已經執行完畢了。而因爲協程是單線程運行的,所以咱們可使用全局變量判斷出剛剛結束的是哪個協程。前文不是說了一大堆須要保存的上下文嗎,爲何這裏賦值的寄存器那麼少?很簡單,協程尚未開始執行呢,那些寄存器都不用恢復,讓協程直接用就好了。
注意,這個函數其實是不會返回的。返回到主線程的工做已經交給了被重定向了的 _coroutine_did_end()
函數來完成。
當切換協程時,調度函數須要獲取 CPU 使用權,其實很簡單:只是要求協程程序本身主動調用相關的函數,從而達到交出 CPU 使用權的目的。
參見 main.cpp 文件的第 33 至 62 行。這裏定義了兩個如出一轍的函數,至關於兩個協程
做爲 demo 程序,這裏協程只調用了一個函數 AMCCoroutineSchedule()
提請切換協程。
這裏調用的是彙編函數 asm_amc_coroutine_dump()
。實際上這個函數在前面保存主線程現場中已經使用過了,這裏咱們再詳細說明一下函數的實現:
asm_amc_coroutine_dump: movq %rbx, (%rdi) movq %rsp, 8(%rdi) movq %rbp, 16(%rdi) movq %r12, 24(%rdi) movq %r13, 32(%rdi) movq %r14, 40(%rdi) movq %r15, 48(%rdi) movq 16(%rsp), %rsi movq %rsi, 56(%rdi) retq
除了標號以外的最前面的七行很好理解,就是將必要的現場保存起來。至於倒數第2、三行的 movq 16(%rsp), %rsi
和 movq %rsi, 56(%rdi)
就很回味無窮啦。
寄存器 rsi
在 GCC 中是做爲第二參數使用的。這個函數中沒有第二個參數,所以就只是做爲臨時變量而已。16(%rsp)
這一句,和前文中 「保存主線程的現場」 中的第二句代碼的做用殊途同歸。
另外,協程上下文的保存,還包含函數外面的一句 C 代碼:
g_pCurrentCoroutine->reg_rip = (uint64_t)(&&RETURN);
這句話把被切換掉的協程恢復的現場重定向爲 AMCCoroutineSchedule()
的 return
語句。效果是跳過了下面的 asm_amc_coroutine_restore()
函數,避免重複調度。
本 demo 中沒有實質性的調度,只是輪詢而已,找到協程鏈上的下一個協程並執行。
這個過程就是下面兩句:
g_pCurrentCoroutine = g_pCurrentCoroutine->p_next; asm_amc_coroutine_restore(g_pCurrentCoroutine);
只是簡單的調用 asm_amc_coroutine_restore()
彙編函數的過程。這個彙編函數我就不貼上來了,由於其邏輯和前面的 asm_amc_coroutine_enter()
相同,只是保存的現場比較多而已。
前文說到,當協程結束的時候,會調用 return
返回。這個時候在彙編中作了如下的事情:
retq
返回(retq 同時會將返回地址出棧丟掉)這就是咱們前文中將協程返回地址重定向的原理基礎。
協程結束後,會返回到 _coroutine_did_end()
函數中。這裏須要注意的是,返回的位置是該函數的入口,所以反彙編會發現,這個函數還額外作了壓棧的動做。不過不要緊,由於這個動做是在即將被銷燬的協程堆棧中進行的,所以不用擔憂內存泄露啥的。
這個函數作了如下幾個操做:
調用匯編函數 asm_amc_coroutine_switch_sp_rip_to()
把當前的堆棧切換的主線程中。之因此要馬上切換掉,是由於協程已經結束了,協程的資源也應該銷燬。若是還在協程的堆棧上工做的話,那麼堆棧銷燬掉後會致使 segment fault。
這很好理解了,前面給協程分配了堆棧,用完了確定要還的。
若是還有其餘未完成的協程,那就調度過去,和前文同樣。
這裏用的則是 asm_amc_coroutine_return_to_main()
彙編函數,和切換協程的函數就是差在第一句彙編語句上:
popq %rsi
這句話後面的註釋也說了,其實仍是玩堆棧。這句話將這個彙編函數原來的返回地址出棧掉,採用以前重定向的地址——也就是主線程調用 AMCCoroutineRun()
以後的下一句代碼
我的以爲我關於協程的兩篇文章恐怕看的人不多,或許如今用 C/C++ 寫後臺服務的人不多了吧,sad ……
計劃這系列文章是分三個部分的,分別是: