協程 及 libco 介紹

libco 是騰訊開源的一個協程庫,主要應用於微信後臺RPC框架,下面咱們從爲何使用協程、如何實現協程、libco使用等方面瞭解協程和libco。程序員

 

why協程後端

爲何使用協程,咱們先從server框架的實現提及,對於client-server的架構,server最簡單的實現:微信

while(1) {accept();recv();do();send();}多線程

串行地接收鏈接、讀取請求、處理、應答,該實現弊端顯而易見,server同一時間只能爲一個客戶端服務。架構

 

爲充分利用好多核cpu進行任務處理,咱們有了多進程/多線程的server框架,這也是server最經常使用的實現方式:框架

accept進程 - n個epoll進程 - n個worker進程異步

  1. accpet進程處理到來的鏈接,並將fd交給各個epoll進程
  2. epoll進程對各fd設置監控事件,當事件觸發時經過共享內存等方式,將請求傳給各個worker進程
  3. worker進程負責具體的業務邏輯處理並回包應答

以上框架以事件監聽、進程池的方式,解決了多任務處理問題,但咱們還能夠對其做進一步的優化。socket

 

進程/線程是Linux內核最小的調度單位,一個進程在進行io操做時 (常見於分佈式系統中RPC遠程調用),其所在的cpu也處於iowait狀態。直到後端svr返回,或者該進程的時間片用完、進程被切換到就緒態。是否能夠把本來用於iowait的cpu時間片利用起來,發生io操做時讓cpu處理新的請求,以提升單核cpu的使用率?分佈式

 

協程在用戶態下完成切換,由程序員完成調度,結合對socket類/io操做類函數掛鉤子、添加事件監聽,爲以上問題提供瞭解決方法。函數

 

用戶態下上下文切換

Linux提供了接口用於用戶態下保存進程上下文信息,這也是實現協程的基礎:

  • getcontext(ucontext_t *ucp): 獲取當前進程/線程上下文信息,存儲到ucp中
  • makecontext(ucontext_t *ucp, void (*func)(), int argc, ...): 將func關聯到上下文ucp
  • setcontext(const ucontext_t *ucp): 將上下文設置爲ucp
  • swapcontext(ucontext_t *oucp, ucontext_t *ucp): 進行上下文切換,將當前上下文保存到oucp中,切換到ucp

以上函數與保存上下文的 ucontext_t 結構都在 ucontext.h 中定義,ucontext_t 結構中,咱們主要關心兩個字段:

  • struct ucontext *uc_link: 協程後繼上下文
  • stack_t uc_stack: 保存協程數據的棧空間

stack_t 結構用於保存協程數據,該空間須要事先分配,咱們主要關注該結構中的如下兩個字段:

  • void __user *ss_sp: 棧頭指針
  • size_t ss_size: 棧大小

獲取進程上下文並切換的方法,總結有如下幾步:

  1. 調用 getcontext(),獲取當前上下文
  2. 預分配棧空間,設置 xxx.uc_stack.ss_sp 和 xxx.uc_stack.ss_size 的值
  3. 設置後繼上下文環境,即設置 xxx.uc_link 的值
  4. 調用 makecontext(),變動上下文環境
  5. 調用 swapcontext(),完成跳轉

 

Socket族函數/io異步處理

當進程使用socket族函數 (connect/send/recv等)、io函數 (read/write等),咱們使用協程切換任務前,需對相應的fd設置監聽事件,以便io完成後原有邏輯繼續執行。

 

對io函數,咱們能夠事先設置鉤子,在真正調用接口前,對相應fd設置事件監聽。一樣,Linux爲咱們設置鉤子提供了接口,以read()函數爲例:

  1. 編寫名字爲 read() 的函數,該函數先對fd調用epoll函數設置事件監聽
  2. read() 中使用dlsym(),調用真正的 read()
  3. 將編寫好的文件打包,編譯成庫文件:gcc -shared -Idl -fPIC prog2.c -o libprog2.so
  4. 執行程序時引用以上庫文件:LD_PRELOAD=/home/qspace/lib/libprog2.so ./prog

當在prog程序中調用 read() 時,使用的就是咱們實現的 read() 函數。

對於glibc函數設置鉤子的方法,可參考:Let's Hook a Librarg Function

 

libco

有了以上準備工做,咱們能夠構建這樣的server框架:

accept進程 - epoll進程(n個epoll協程) - n個worker進程(每一個worker進程n個worker協程) 

該框架下,接收請求、業務邏輯處理、應答均可以看作單獨的任務,相應的epoll、worker協程事先分配,服務流程以下:

  1. mainloop主循環,負責 i/監聽請求事件,有請求則拉起一個worker協程處理;ii/若是timeout時間內沒有請求,則處理就緒協程(即io操做已返回) 
  2. worker協程,若是遇到io操做則掛起,對fd加監聽事件,讓出cpu

libco 提供瞭如下接口:

  • co_create: 建立協程,可在程序啓動時建立各任務協程
  • co_yield: 協程主動讓出cpu,調io操做函數後調用
  • co_resume: io操做完成後(觸發相應監聽事件)調用,使協程繼續往下執行

socket族函數(socket/connect/sendto/recv/recvfrom等)、io函數(read/write) 在libco的co_hook_sys_call.cpp中已經重寫,以read爲例:

ssize_t read( int fd, void *buf, size_t nbyte )

{
    struct pollfd pf = { 0 };
    pf.fd = fd;
    pf.events = ( POLLIN | POLLERR | POLLHUP ); 

    int pollret = poll( &pf,1,timeout );  /*對相應fd設置監聽事件*/
    ssize_t readret = g_sys_read_func( fd,(char*)buf ,nbyte );   /*真正調用read()*/
    return readret;
}

 

小結

由最簡單的單任務處理,到多進程/多線程(並行),再到協程(異步),server在不斷地往極致方向優化,以更好地利用硬件性能的提高(多核cpu的出現、單核cpu性能不斷提高)。

對程序員而言,可時常檢視本身的程序,是否作好並行與異步,在硬件性能提高時,程序服務能力可不能夠有相應比例的提高。

相關文章
相關標籤/搜索