協程相關視頻決議:
linux系統下協程的實作與原理剖析訓練營(上)
linux系統下協程的實作與原理剖析訓練營(下)
什么是協程?
協程被稱為“輕量級執行緒”或者“用戶態執行緒”,最近協程在高并發編程領域大放異彩,如Golang天生就支持協程,Lua和Python也支持協程,但其實協程并不是最近才出現的新技術,恰恰相反,協程是一項古老的技術,早期版本的Linux并不支持執行緒,這時就出現代替執行緒的輕量級執行緒–協程,比較有名的有: GNU Pth 和 Libtask(Go語言的作者之一Russ Cox的作品),下面我們來解釋下協程的原理,
基本概念
要理解協程,首先要知道程式是怎么運行的,所以下面我們先來聊聊程式是怎么跑起來的,
我們知道CPU的使命就是執行程式中的指令,而且CPU內部有很多用于存放資料的暫存器,其中比較重要的一個暫存器叫EIP暫存器,它用于存盤下一條要執行的指令,除了EIP暫存器之外,還有一個比較重要的暫存器叫ESP暫存器,它用于保存程式的堆疊頂位置,除此之外,CPU還有很多其他用途的暫存器,如:通用暫存器EAX、EDX和段暫存器CS、DS等等,
當一個程式被執行(稱為行程)的時候,這些暫存器的值通常會被修改,所以當要切換行程執行的時候,只需要把這些暫存器的值保存下來,然后把新行程暫存器的值賦值到CPU中,那么就完成行程切換了,通常我們把這個程序稱為背景關系切換,協程的切換也類似,

協程原理
上面討論過,只需要切換背景關系就可以切換協程,而背景關系就是CPU暫存器的值,所以要創建一個協程,首先要創建一個保存CPU暫存器值的物件,在Libtask中,使用mcontext結構體來保存暫存器的值,mcontext結構體定義如下:
struct mcontext {
int mc_gs;
int mc_fs;
int mc_es;
int mc_ds;
int mc_edi;
int mc_esi;
int mc_ebp;
int mc_isp;
int mc_ebx;
int mc_edx;
int mc_ecx;
int mc_eax;
int mc_trapno;
int mc_err;
int mc_eip;
int mc_cs;
int mc_eflags;
int mc_esp;
int mc_ss;
};
mcontext結構體用來保存暫存器的值,從mcontext的成員可以看到要保存的暫存器很多,包括CS、DS、EIP、EAX、EBX等等,
C函式呼叫原理
因為協程切換一般是通過呼叫一個swapcontext()的C函式來進行,這個函式的作用就是保存舊的協程背景關系和替換新的協程背景關系來進行協程切換,而新舊協程背景關系就是通過C函式的引數來傳遞的,所以我們先來了解下C函式呼叫程序的原理,
C函式是通過堆疊空間來傳遞引數的,通過下圖有個感性的認識:

在上圖中,淺綠色部分是呼叫函式時把引數入堆疊的,入堆疊時,C語言是從右到左開始入堆疊的,例如我們呼叫swapcontext(old, new)這個函式時,會先把new引數入堆疊,然后再把old引數入堆疊,
另外,在呼叫一個函式時,CPU會自動把當前指令的下一條指令入堆疊,所以,在上圖可以看到在引數后面還有回傳地址,在回傳地址下面保存的是函式的區域變數,
注意
堆疊空間是從記憶體的高地址向地址增長的
【文章福利】需要C/C++ Linux服務器架構師學習資料加群812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)

協程切換
現在到了重頭戲–協程的切換,協程的切換是通過保存舊協程的背景關系和替換新協程的背景關系來實作的,
在Libtask庫中,保存協程背景關系通過getcontext()實作,而替換協程背景關系是通過setcontext()實作,這兩個函式都是使用匯編語言實作的,所以要看明白這兩個函式就必須有匯編的基礎,我們來看看這兩個函式的實作:
1getcontext()
gexcontext:
movl 4(%esp), %eax
movl %fs, 8(%eax)
movl %es, 12(%eax)
movl %ds, 16(%eax)
movl %ss, 76(%eax)
movl %edi, 20(%eax)
movl %esi, 24(%eax)
movl %ebp, 28(%eax)
movl %ebx, 36(%eax)
movl %edx, 40(%eax)
movl %ecx, 44(%eax)
movl $1, 48(%eax)
movl (%esp), %ecx
movl %ecx, 60(%eax)
leal 4(%esp), %ecx
movl %ecx, 72(%eax)
movl 44(%eax), %ecx
movl $0, %eax
ret
getcontext()函式的原型如下:
int getcontext(struct mcontext *ctx);
其作用是把當前暫存器的值保存到引數ctx中,上面這段匯編代碼就不詳細解說了,有興趣可以根據C函式引數傳遞的原理來對照一下就很容易理解,
需要說明的一點是,“movl 4(%esp), %eax”這行匯編代碼的作用是把ctx引數放置到EAX暫存器中,后面的操作都是通過mcontext結構體的偏移量來賦值的,
2setcontext()
setcontext:
movl 4(%esp), %eax
movl 8(%eax), %fs
movl 12(%eax), %es
movl 16(%eax), %ds
movl 76(%eax), %ss
movl 20(%eax), %edi
movl 24(%eax), %esi
movl 28(%eax), %ebp
movl 36(%eax), %ebx
movl 40(%eax), %edx
movl 44(%eax), %ecx
movl 72(%eax), %esp
pushl 60(%eax)
movl 48(%eax), %eax
ret
setcontext()函式是協程切換的切換點,原型如下:
int setcontext(struct mcontext *ctx);
其作用是把ctx引數中暫存器的值替換成CPU暫存器的值來實作切換,
最后,我們就可以通過getcontext()和setcontext()這兩個函式來實作swapcontext()函式了,實作很簡單:
int swapcontext(struct mcontext *new, struct mcontext *old)
{
getcontext(old);
setcontext(new);
return 0;
}
以后我們就可以通過swapcontext()函式來進行協程的切換了,
總結
在本文中,我們只要解釋了協程的基本原理,但是要真正實作一個可以使用的協程庫還需要做很多細節的作業,例如切換協程的堆疊空間(因為每個協程都需要有自己獨立的堆疊空間才不會影響其協程),
另外,一個完善的協程庫還應該支持定時器和I/O阻塞自動切換協程等功能,對于怎么實作一個完善的協程庫后續更新,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/253142.html
標籤:其他
