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

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

內部 coroutine 朋友 null 數據存儲 測試 處理 交流 系列

目錄
  • 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)函數,步驟如下:

  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

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