ucontext簇函數學習
https://github.com/zfengzhen/Blog/blob/master/article/ucontext%E7%B0%87%E5%87%BD%E6%95%B0%E5%AD%A6%E4%B9%A0.md
作者: fergus (zfengzhen@gmail.com)
系統手冊學習:
名字
getcontext, setcontext —— 獲取或者設置用戶上下文
概要
#include <ucontext.h> int getcontext(ucontext_t *ucp); int setcontext(const ucontext_t *ucp);
描述
在類System-V環境中,定義在<ucontext.h>
頭文件中的mcontext_t和ucontext_t的兩種數據類型,以及getcontext(),setcontext(),makecontext()和swapcontext()四個函數允許在一個進程不同的協程中用戶級別的上下文切換。
mcontext_t數據結構是依賴機器和不透明的。ucontext_t數據結構至少包含下面的字段:
typedef struct ucontext { struct ucontext *uc_link; sigset_t uc_sigmask; stack_t uc_stack; mcontext_t uc_mcontext; ... } ucontext_t;
sigset_t和stack_t定義在<signal.h>
頭文件中。uc_link指向當前的上下文結束時要恢復到的上下文(只在當前上下文是由makecontext創建時,個人理解:只有makecontext創建新函數上下文時需要修改),uc_sigmask表示這個上下文要阻塞的信號集合(參見sigprocmask),uc_stack是這個上下文使用的棧(個人理解:非makecontext創建的上下文不要修改),uc_mcontext是機器特定的保存上下文的表示,包括調用協程的機器寄存器。
getcontext()函數初始化ucp所指向的結構體,填充當前有效的上下文。
setcontext()函數恢復用戶上下文為ucp所指向的上下文。成功調用不會返回。ucp所指向的上下文應該是getcontext()或者makeontext()產生的。
如果上下文是getcontext()產生的,切換到該上下文,程序的執行在getcontext()后繼續執行。
如果上下文被makecontext()產生的,切換到該上下文,程序的執行切換到makecontext()調用所指定的第二個參數的函數上。當該函數返回時,我們繼續傳入makecontext()中的第一個參數的上下文中uc_link所指向的上下文。如果是NULL,程序結束。
返回值
成功時,getcontext()返回0,setcontext()不返回。錯誤時,都返回-1並且賦值合適的errno。
注意
這個機制最早的化身是setjmp/longjmp機制。但是它們沒有定義處理信號的上下文,下一步就出了sigsetjmp/siglongjmp。當前這套機制給予了更多的控制權。但是另一方面,沒有簡單的方法去探明getcontext()的返回是第一次調用還是通過setcontext()調用。用戶不得不發明一套他自己的書簽的數據,並且當寄存器恢復時,register聲明的變量不會恢復(寄存器變量)。
當信號發生時,當前的用戶上下文被保存,一個新的內核為信號處理器產生的上下文被創建。不要在信號處理器中使用longjmp:它是未定義的行為。使用siglongjmp()或者setcontext()替代。
名字
makecontext,swapcontext —— 操控用戶上下文
概要
#include <ucontext.h> void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...); int swapcontext(ucontext_t *restrict oucp, const ucontext_t *restrict ucp);
描述
makecontext()函數修改ucp所指向的上下文,ucp是被getcontext()所初始化的上下文。當這個上下文采用swapcontext()或者setcontext()被恢復,程序的執行會切換到func的調用,通過makecontext()調用的argc傳遞func的參數。
在makecontext()產生一個調用前,應用程序必須確保上下文的棧分配已經被修改。應用程序應該確保argc的值跟傳入func的一樣(參數都是int值4字節);否則會發生未定義行為。
當makecontext()修改過的上下文返回時,uc_link用來決定上下文是否要被恢復。應用程序需要在調用makecontext()前初始化uc_link。
swapcontext()函數保存當前的上下文到oucp所指向的數據結構,並且設置到ucp所指向的上下文。
保存了舊值oucp,跳轉到ucp所指的地方
返回值
成功完成,swapcontext()返回0。否則返回-1,並賦值合適的errno。
錯誤
swapcontext()函數可能會因為下面的原因失敗:
ENOMEM ucp參數沒有足夠的棧空間去完成操作。
例子
#include <stdio.h> #include <ucontext.h> static ucontext_t ctx[3]; static void f1 (void) { puts("start f1"); swapcontext(&ctx[1], &ctx[2]); puts("finish f1"); } static void f2 (void) { puts("start f2"); swapcontext(&ctx[2], &ctx[1]); puts("finish f2"); } int main (void) { char st1[8192]; char st2[8192]; getcontext(&ctx[1]); ctx[1].uc_stack.ss_sp = st1; ctx[1].uc_stack.ss_size = sizeof st1; ctx[1].uc_link = &ctx[0]; makecontext(&ctx[1], f1, 0); getcontext(&ctx[2]); ctx[2].uc_stack.ss_sp = st2; ctx[2].uc_stack.ss_size = sizeof st2; ctx[2].uc_link = &ctx[1]; makecontext(&ctx[2], f2, 0); swapcontext(&ctx[0], &ctx[2]); return 0; }
代碼試用總結:
- 1 makecontext之前必須調用getcontext初始化context,否則會段錯誤core
- 2 makecontext之前必須給uc_stack分配棧空間,否則也會段錯誤core
- 3 makecontext之前如果需要上下文恢復到調用前,則必須設置uc_link以及通過swapcontext進行切換
- 4 getcontext產生的context為當前整個程序的context,而makecontext切換到的context為新函數獨立的context,但setcontext切換到getcontext的context時,getcontext所在的函數退出時,並不需要uc_link的管理,依賴於該函數是在哪被調用的,整個棧會向調用者層層剝離
- 5 不產生新函數的上下文切換指需要用到getcontext和setcontext
- 6 產生新函數的上下文切換需要用到getcontext,makecontext和swapcontext
ucontext性能小試:
運行環境為我的mac下通過虛擬機開啟的centos64位系統,不代表一般情況,正常在linux實體機上應該會好很多吧
-
1 單純的getcontext:
function[ getcontext(&ctx) ] count[ 10000000 ]
cost[ 1394.88 ms] avg_cost[ 0.14 us]
total CPU time[ 1380.00 ms] avg[ 0.14 us]
user CPU time[ 560.00 ms] avg[ 0.06 us]
system CPU time[ 820.00 ms] avg[ 0.08 us] -
2 新函數的協程調用
通過getcontext和對uc_link以及uc_stack賦值,未了不增加其他額外開銷,uc_stack為靜態字符串數組分配,運行時不申請,makecontext中的函數foo為空函數,調用swapcontext切換協程調用測試
function[ getcontext_makecontext_swapcontext() ] count[ 1000000 ]
cost[ 544.55 ms] avg_cost[ 0.54 us]
total CPU time[ 550.00 ms] avg[ 0.55 us]
user CPU time[ 280.00 ms] avg[ 0.28 us]
system CPU time[ 270.00 ms] avg[ 0.27 us]
每秒百萬級別的調用性能。
ucontext協程的實際使用:
將getcontext,makecontext,swapcontext封裝成一個類似於lua的協同式協程,需要代碼中主動yield釋放出CPU。
協程的棧采用malloc進行堆分配,分配后的空間在64位系統中和棧的使用一致,地址遞減使用,uc_stack.uc_size設置的大小好像並沒有多少實際作用,使用中一旦超過已分配的堆大小,會繼續向地址小的方向的堆去使用,這個時候就會造成堆內存的越界使用,更改之前在堆上分配的數據,造成各種不可預測的行為,coredump后也找不到實際原因。
對使用協程函數的棧大小的預估,協程函數中調用其他所有的api的中的局部變量的開銷都會分配到申請給協程使用的內存上,會有一些不可預知的變量,比如調用第三方API,第三方API中有非常大的變量,實際使用過程中開始時可以采用mmap分配內存,對分配的內存設置GUARD_PAGE進行mprotect保護,對於內存溢出,准確判斷位置,適當調整需要分配的棧大小。