1. 程式人生 > >Linux高效能網路:協程系列04-協程實現之工作原理

Linux高效能網路:協程系列04-協程實現之工作原理

目錄

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)函式,步驟如下:

  1. 將sockfd 新增到epoll管理中;
  2. 進行上下文環境切換,由協程上下文yield到排程器的上下文;
  3. 排程器獲取下一個協程上下文。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