Linux高效能網路:協程系列04-協程實現之工作原理
目錄
- Linux高效能網路:協程系列01-前言
- Linux高效能網路:協程系列02-協程的起源
- Linux高效能網路:協程系列03-協程的案例
- Linux高效能網路:協程系列04-協程實現之工作原理
- Linux高效能網路:協程系列05-協程實現之原語操作
- Linux高效能網路:協程系列06-協程實現之切換
- Linux高效能網路:協程系列07-協程實現之定義
- Linux高效能網路:協程系列08-協程實現之排程器
- Linux高效能網路:協程系列09-協程效能測試
-
[Linux高效能網路:協程系列10 待續]()
4.協程的實現之工作原理
- [4.0 前言]
- [4.1.建立協程]
- [4.2.實現IO非同步操作]
- [4.3.回撥協程的子過程]
4.0 前言
問題:協程內部是如何工作呢?
先來看一下協程伺服器案例的程式碼, 程式碼參考:
https://github.com/wangbojing/NtyCo/blob/master/nty_server_test.c
分別討論三個協程的比較晦澀的工作流程。第一個協程的建立;第二個IO非同步操作;第三個協程子過程回撥。
4.1.建立協程
當我們需要非同步呼叫的時候,我們會建立一個協程。比如accept返回一個新的sockfd,建立一個客戶端處理的子過程。再比如需要監聽多個埠的時候,建立一個
server的子過程,這樣多個埠同時工作的,是符合微服務的架構的。
建立協程的時候,進行了如何的工作?建立API如下:
int nty_coroutine_create(nty_coroutine \**new_co, proc_coroutine func, void *arg);
- 引數1:new_co,需要傳入空的協程的物件,這個物件是由內部建立的,並且在函式返回的時候,會返回一個內部建立的協程物件。
- 引數2:func,協程的子過程。當協程被排程的時候,就會執行該函式。
- 引數3:arg,需要傳入到新協程中的引數。
協程不存在親屬關係,都是一致的排程關係,接受排程器的排程。呼叫create API就會建立一個新協程,新協程就會加入到排程器的就緒佇列中。建立的協程具體
步驟會在《協程的實現之原語操作》來描述。
4.2.實現IO非同步操作
大部分的朋友會關心IO非同步操作如何實現,在send與recv呼叫的時候,如何實現非同步操作的。
先來看一下一段程式碼:
while (1) {
int nready = epoll_wait(epfd, events, EVENT_SIZE, -1);
for (i = 0;i < nready;i ++) {
int sockfd = events[i].data.fd;
if (sockfd == listenfd) {
int connfd = accept(listenfd, xxx, xxxx);
setnonblock(connfd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else {
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
recv(sockfd, buffer, length, 0);
//parser_proto(buffer, length);
send(sockfd, buffer, length, 0);
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, NULL);
}
}
}
在進行IO操作(recv,send)之前,先執行了 epoll_ctl的del操作,將相應的sockfd從epfd中刪除掉,在執行完IO操作(recv,send)再進行epoll_ctl的add的動作。這段程式碼看起來似乎好像沒有什麼作用。
如果是在多個上下文中,這樣的做法就很有意義了。能夠保證sockfd只在一個上下文中能夠操作IO的。不會出現在多個上下文同時對一個IO進行操作的。協程的IO非同步操作正式是採用此模式進行的。
把單一協程的工作與排程器的工作的劃分清楚,先引入兩個原語操作 resume,yield會在《協程的實現之原語操作》來講解協程所有原語操作的實現,yield就是讓出執行,resume就是恢復執行。排程器與協程的上下文切換如下圖所示:
在協程的上下文IO非同步操作(nty_recv,nty_send)函式,步驟如下:
- 將sockfd 新增到epoll管理中;
- 進行上下文環境切換,由協程上下文yield到排程器的上下文;
- 排程器獲取下一個協程上下文。Resume新的協程。
IO非同步操作的上下文切換的時序圖如下:
4.3.回撥協程的子過程
在create協程後,何時回撥子過程?何種方式回撥子過程?
首先來回顧一下x86_64暫存器的相關知識。彙編與暫存器相關知識還會在《協程的實現之切換》繼續深入探討的。x86_64 的暫存器有16個64位暫存器,分別是:
%rax, %rbx,%rcx, %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, %r14, %r15 用作資料儲存,遵循呼叫者使用規則,換句話說,就是隨便用。呼叫子函式之前要備份它,以防它被修改;
%r10, %r11 用作資料儲存,就是使用前要先儲存原值
以NtyCo的實現為例,來分析這個過程。CPU有一個非常重要的暫存器叫做EIP,用來儲存CPU執行下一條指令的地址。我們可以把回撥函式的地址儲存到EIP中,將相應的引數儲存到相應的引數暫存器中。實現子過程呼叫的邏輯程式碼如下:
void _exec(nty_coroutine *co) {
co->func(co->arg); //子過程的回撥函式
}
void nty_coroutine_init(nty_coroutine *co) {
//ctx 就是協程的上下文
co->ctx.edi = (void*)co; //設定引數
co->ctx.eip = (void*)_exec; //設定回撥函式入口
//當實現上下文切換的時候,就會執行入口函式_exec , _exec 呼叫子過程func
}
更多分享
email: [email protected]
email: [email protected]
email: [email protected]
協程技術交流群:829348971