協程 及 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進程負責具體的業務邏輯處理並回包應答

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

 

進程/線程是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性能不斷提升)。

對程序員而言,可時常檢視自己的程序,是否做好並行與異步,在硬件性能提升時,程序服務能力可不可以有相應比例的提升。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM