1. 程式人生 > >[轉]協程 及 Libco 介紹

[轉]協程 及 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() 函式。

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效能不斷提升)。

對程式設計師而言,可時常檢視自己的程式,是否做好並行與非同步,在硬體效能提升時,程式服務能力可不可以有相應比例的提升。