協程庫基礎知識

weixin_34054866發表於2018-04-29

這篇文章主要介紹些彙編和函式呼叫棧的變化過程以及x86-64體系結構下各暫存器的作用,為後面兩篇部落格分析協程庫(Libco/Pebble/Phxrpc)用到的技術點作些預習,但這邊的協程非lua中的coroutine,雖然我工作中也用了一段時間的lua,看了些協程相關的API使用方法和原理,但沒怎麼使用過諸如coroutine.resume/coroutine.running/coroutine.yield等寫過協程相關的底層東西,但是原理還是差不多,後面的開源庫也主要是這幾個api的介紹和使用。

一)協程和執行緒區別
執行緒一般用的比較多,有專門的POSIX API使用,在一些專案的基礎庫中很容易見到各種封裝的程式碼,排程也不需要使用者去操心什麼時候排程和執行,但是如果能高效的進行多執行緒程式設計,還是要熟悉一些體系結構和原理。

一般執行緒的建立數量不易過多,受限於cpu的核數,且排程/上下文切換需要核心參與,有一定的效能代價,一個執行緒切換出去再切回來,那麼cache的資料都可能miss掉,不能充分利用區域性性,偽共享等,導致效能稍微有些抖動。
而協程雖然也要來回切換,但由使用者去控制,且要儲存的資料不多,後面會根據原始碼分析出有哪些。然後建立的協程可以非常多,只受限於記憶體。

然後對於一個需要等待的事件比如io,如果使用執行緒,比如使用epoll等網路模型,那麼就會短暫的阻塞在epoll上,而對於協程,可以在發生等待事件時,這個協程讓出cpu,然後讓另一協程執行,所以非常高效,讓cpu充分利用,另外協程是非搶佔式,需要使用者自己釋放cpu切換到其他協程。

對於執行緒來說,如果是單程式多執行緒,那麼可以並行執行在多核上,而對於協程,因為如果也是單程式單執行緒,那麼只有一個在執行,但可以fork多程式,如果在單程式多執行緒中實現協程,可能會比較複雜,Phxrpc中有類似的實現。

二)Lua協程原語說明
簡單介紹Lua中的幾個api來說明作用是什麼,會類比libco中的類似api。
引用“coroutine.create(f):建立一個新的協程,協程體中的內容是f,f必須是一個Lua函式。該函式返回這個新建立的型別為thread的協程。
coroutine.resume(co [, val1, …]):當首次resume一個協程的時候,便開始執行這個協程中的函式f,引數val1, … 傳遞給函式f作為f的引數。當協程掛起(yield)過,再次resume這個協程的時候,引數val1, … 作為yield的返回結果;如果協程執行過程中沒有發生錯誤,那麼resume返回true以及yield傳遞過來的值(當協程掛起時),或者從函式f返回的任何值(當協程終止時)。如果協程執行過程中發生錯誤,那麼返回false以及錯誤資訊。
coroutine.yield(…):將正在執行的執行緒掛起(暫停)。yield的任何引數將傳遞給resume,作為resume額外的返回結果。”

三)x86-64各暫存器的用途
引用“X86-64有16個64位暫存器,分別是:
%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,
%r11,%r12,%r13,%r14,%r15。
其中:
%rax 作為函式返回值使用;%rsp 棧指標暫存器,指向棧頂;%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函式引數,依次對應第1引數,第2引數。。。
%rbx,%rbp,%r12,%r13,%14,%15 用作資料儲存,遵循被呼叫者使用規則,簡單說就是隨便用,呼叫子函式之前要備份它,以防他被修改;
%r10,%r11 用作資料儲存,遵循呼叫者使用規則,簡單說就是使用之前要先儲存原值。”

rsp和esp,rbp和ebp的作用相同,一個是棧頂指標,一個是幀指標。

四)函式呼叫棧的變化
這部分是從網上/和書上收集的資料,這部分不難理解,如果不考慮安全類的問題比如棧溢位等。


1715344-f32d95f199cdcb08.png
圖一

舉個32位中的情況,呼叫者需要入棧呼叫引數,從右往左,然後是呼叫函式的下一條指令地址和ebp,然後進入被呼叫的函式,開始分配區域性變數,其中esp是動態變化的,ebp是不變的,取變數的值是根據ebp加上偏移量來進行的。
比如引用參考資料中某個連結的例子:

int bar(int c,int d)  
{  
 8048394:   55                      push   %ebp  
 8048395:   89 e5                   mov    %esp,%ebp  
 8048397:   83 ec 10                sub    $0x10,%esp 

在call bar函式時,已經把返回bar下一條指令入棧了,是隱含執行的,然後修改eip,跳轉到bar執行,然後壓ebp,通過esp-0x10分配棧空間;

return e;  
 80483a6:   8b 45 fc                mov    -0x4(%ebp),%eax  
}  
 80483a9:   c9                      leave    
 80483aa:   c3                      ret 

返回時,leave操作過程是bar的相反操作,把ebp值賦給esp,然後彈出上一個幀的ebp,此時esp指向的是返回地址,剩下的操作是ret,和call相反,把返回地址給eip,這些過程esp值都會有變化,以上是要介紹的基本知識。

五)Hook技術
具體怎麼hook的原理就不在這裡詳細分析了,有興趣自己在網上搜一下相關的資料,這裡主要是介紹如何hook一些linux系統庫函式,主要是為後面協程分析作些說明。
比如

void *malloc(size_t size) {
    static void *(*my_malloc(size_t ) = NULL;
    if (! my_malloc) {
        my_malloc = dlsym(RTLD_NEXT, "malloc");
    }
    return my_malloc(size);
}

The dlsym() function takes two parameters: the first is a handle returned by dlopen(). Here, we must use RTLD_NEXT for function interposition.
This tells the dynamic linker to find the next reference to the specified function, not the one that is calling dlsym(). The second parameter is the symbol name (malloc, in this case), as a character string. dlsym() returns the address of the symbol specified as the second parameter.

比如Libco中hook read系統呼叫:

330 ssize_t read( int fd, void *buf, size_t nbyte )
331 {
332     HOOK_SYS_FUNC( read );
333 
334     if( !co_is_enable_sys_hook() )
335     {
336         return g_sys_read_func( fd,buf,nbyte );
337     }
338     rpchook_t *lp = get_by_fd( fd );
339 
340     if( !lp || ( O_NONBLOCK & lp->user_flag ) )
341     {
342         ssize_t ret = g_sys_read_func( fd,buf,nbyte );
343         return ret;
344     }
345     int timeout = ( lp->read_timeout.tv_sec * 1000 )
346                 + ( lp->read_timeout.tv_usec / 1000 );
347 
348     struct pollfd pf = { 0 };
349     pf.fd = fd;
350     pf.events = ( POLLIN | POLLERR | POLLHUP );
351 
352     int pollret = poll( &pf,1,timeout );
353 
354     ssize_t readret = g_sys_read_func( fd,(char*)buf ,nbyte );
355 
356     if( readret < 0 )
357     {
358         co_log_err("CO_ERR: read fd %d ret %ld errno %d poll ret %d timeout %d",
359                     fd,readret,errno,pollret,timeout);
360     }
361 
362     return readret;
363 
364 }

對於fd,如果設定了非阻塞,那麼直接read,否則加入epoll中等待可read事件,並切出協程(在poll中執行co_yield_env),如果有read事件發生了則切回來,執行程式碼354行read。

參考資料:
《深入理解計算機系統》
https://segmentfault.com/a/1190000013177055
https://blog.csdn.net/lqt641/article/details/73002566
http://opensourceforu.com/2011/08/lets-hook-a-library-function/
https://www.cnblogs.com/zrtqsk/p/4374360.html
https://www.zhihu.com/question/20511233
https://blog.csdn.net/corfox_liu/article/details/51024729
https://segmentfault.com/a/1190000012561446
https://blog.csdn.net/wangyezi19930928/article/details/16921927
http://www.it165.net/os/html/201407/8847.html
https://linux.die.net/man/3/dlsym