網絡IO模型理解與分析


在面對異步IO頻繁的業務需求的時,可以使用回調的機制。在利用回調的過程中,如果利用狀態機則會發生回調金字塔(callback hell),主要表現爲:1.代碼複用率極低。2.邏輯複雜。此時利用協同程序可以很好的解決這個問題。

CPU (CentralProcessingUnit)

計算機的核心是CPU,所有的計算任務都是由它完成。CPU概念包括:

物理CPU

物理CPU就是插在主機上的真實的CPU硬件,在Linux下可以數不同的physical id 來確認主機的物理CPU個數。

核心數

物理CPU下一層概念就是核心數,我們常常會聽說多核處理器,其中的核指的就是核心數。在Linux下可以通過cores來確認主機的物理CPU的核心數。

邏輯CPU

邏輯CPU跟超線程技術有聯繫,假如物理CPU不支持超線程的,那麼邏輯CPU的數量等於核心數的數量;如果物理CPU支持超線程,那麼邏輯CPU的數目是核心數數目的兩倍。在Linux下可以通過 processors 的數目來確認邏輯CPU的數量。

層級如下圖所示:

多核與多線程

進程與線程

**進程是CPU資源分配的最小單位,線程是CPU調度的最小單位。**進程包含線程,常見的模型有:

1.單進程單線程模型

進程與線程的區別在於:進程掌管着資源,線程是進程的一部分,CPU執行調度的是線程。

2.單進程多線程模型

多個線程共享全局資源,但不包括線程自己的棧空間。

3.多進程單線程模型

單個應用可以通過fork拷貝出多個進程,進程的資源會進行復制。注意:fd同樣會複製,不建議多個進程共用同一個fd,會出現數據亂序的情況。

4.多進程單線程模型

涉及多線程時,鎖是個必須掌握的知識點,否則同時對共享資源進行處理可能導致覆蓋寫問題。

網絡編程中5種I/O模型

在Unix(linux)平臺下有5中I/O模型:
同步I/O模型

堵塞I/O模型(blocking I/O)
非堵塞I/O模型(un-blocking I/O)
I/O多路複用模型(select, poll, epoll)(較常見)
信號驅動I/O模型(SIGIO)

異步I/O模型
同步與異步的區別
同步:指關於這個I/O中的一系列動作都需要自己來完成,無論你是原地等待事件的發生(阻塞)還是當某個事件已經準備好的時候你去完成後面的的動作(非阻塞)都屬於同步。
異步:它是指是調用另一個執行者去完成,當執行者發現要處理的事件後調用你,你再完成這件事情,執行的過程和你的動作是不牽扯的。
因此,前四種是同步I/O模型,只有第五種是異步的。

I/O操作一般分爲兩個階段:

  • 等待數據達到內核緩存區
  • 將數據從內核拷貝到用戶進程

阻塞型I/O

阻塞型I/O

通常把阻塞的文件描述符(file descriptor,fd)稱之爲阻塞I/O。默認條件下,創建的socket fd是阻塞的,針對阻塞I/O調用系統接口,可能因爲等待的事件沒有到達而被系統掛起,直到等待的事件觸發調用接口才返回,例如,tcp socket的recvfrom調用會阻塞至連接有數據返回,如上圖所示。另外socket 的系統API ,如,accept、send、connect等都可能被阻塞。

非阻塞型I/O

一種輪詢的機制
非阻塞型I/O
把非阻塞的文件描述符稱爲非阻塞I/O。可以通過設置SOCK_NONBLOCK標記創建非阻塞的socket fd,或者使用fcntl將fd設置爲非阻塞。
對非阻塞fd調用系統接口時,不需要等待事件發生而立即返回,事件沒有發生,接口返回-1,此時需要通過errno的值來區分是否出錯,有過網絡編程的經驗的應該都瞭解這點。不同的接口,立即返回時的errno值不盡相同,如,recv、send、accept errno通常被設置爲EAGIN 或者EWOULDBLOCK,connect 則爲EINPRO- GRESS 。

I/O多路複用

I/O多路複用
最常用的I/O事件通知機制就是I/O複用(I/O multiplexing)。Linux 環境中使用select/poll/epoll 實現I/O複用,I/O複用接口本身是阻塞的,在應用程序中通過I/O複用接口向內核註冊fd所關注的事件,當關注事件觸發時,通過I/O複用接口的返回值通知到應用程序,如圖3所示,以recv爲例。I/O複用接口可以同時監聽多個I/O事件以提高事件處理效率。

好處:其實這就是一個回調實現的機制。在這個過程中,只需要兩個線程就可以完成多個連接請求。一個爲業務線程,一個爲epoll模型監聽線程。這樣的話,就可以利用一個業務線程進行大量的訪問請求處理。而不必像PHP等實現機制,每一個請求都分配一個線程,之後阻塞等待。

回調機制

在這種典型的回調機制的實現過程中,通過epoll模型返回的結果即狀態機的條件,以及結合上下文即狀態機的現態,可以觸發動作。即:條件+現態->動作(狀態機是一種switch-case的結構,邏輯是非順序的,類似於一種表格的結構—詳見深入淺出理解有限狀態機),因爲狀態機的邏輯非順序化,所以將其邏輯順序化的過程,會出現callback hell的問題。如下圖所示:
回調金字塔-callback_hell

解決這樣的問題協程是一種有效的手段,即:協程可以處理大量的異步I/O操作的需求業務。

信號驅動I/O

信號驅動I/O
除了I/O複用方式通知I/O事件,還可以通過SIGIO信號來通知I/O事件,如上圖所示。兩者不同的是,在等待數據達到期間,I/O複用是會阻塞應用程序,而SIGIO方式是不會阻塞應用程序的。

異步I/O

異步I/O
POSIX規範定義了一組異步操作I/O的接口,不用關心fd 是阻塞還是非阻塞,異步I/O是由內核接管應用層對fd的I/O操作。異步I/O嚮應用層通知I/O操作完成的事件,這與前面介紹的I/O 複用模型、SIGIO模型通知事件就緒的方式明顯不同。以aio_read 實現異步讀取IO數據爲例,如圖5所示,在等待I/O操作完成期間,不會阻塞應用程序。

目前常見的服務端模型(多進程結合I/O多路複用)

目前常見的服務端模型
僞代碼:

void dispatch(...){
    將connect_fd加入epoll隊列,等待可讀(可寫)事件
}
void run(...){
    執行業務邏輯(read或者write該fd)
    //其他業務邏輯(如curl)
}
int main()
{
    創建listen_fd監聽端口
    fork創建子進程
    if(當前進程爲父進程){
        /*************父進程***************/
        管理子進程
    }else{
        /*************子進程***************/
        子進程創建epoll隊列
        將dispatch事件綁定到listen_fd的可讀事件上
        使用epoll函數監聽listen_fd
        while(1){
            //第一次觸發epoll函數
            出現listen_fd可讀(可寫)事件,觸發dispatch事件
            epoll隊列中有listen_fd和每個客戶端的connect_fd
            //第N次(N!=1)
            出現listen_fd或者connect_fd的可讀(可寫)事件,觸發對應的dispatch事件或者run事件

        }

    }
}

從服務器端的代碼邏輯可以知道,epoll是可以同時監聽多個fd的,並且在有對應的事件時才喚醒對應的事件函數,這是通常說的異步調用。
但是,這裏有個問題,如果在run函數中業務邏輯需要創建tcp連接(如curl)請求其他服務接口(創建fd),此時的fd因爲沒有使用epoll,就會出現阻塞。
那我們設想一下,如果在run函數中寫epoll監聽該fd,那如果這個tcp連接請求的後續事件依舊需要創建tcp請求呢,這個代碼該怎麼編寫下去呢,這就是常說的一種情況,回調地獄。
那如何優雅解決這個問題呢?
設想一下,如果我們不需要去寫epoll監聽,直接對read/write函數進行hook,默認所有IO操作行爲會被直接加入epoll隊列,這樣就不會有回調地獄。因此,協程誕生了!

協程

協程是一種程序組件,是由子例程(過程、函數、例程、方法、子程序)的概念泛化而來的,子例程只有一個入口點且只返回一次,而協程允許多個入口點,可以在指定位置掛起和恢復執行。協程是一種“僞多線程”,一個線程中可以包含多個協程,但同一時刻只能有一個協程在運行。

協程是一種可以暫停執行過程的函數,它可以中斷當前的執行過程直到下一個Yield指令達成。在實現上,大多數都是以函數來作爲一個協程,因此這裏列出此簡化的定義方便理解。
例如:實現一個0到9的循環輸出。

int function(void) {
  static int i, state = 0;
  switch (state) {
    case 0: /* start of function */
    for (i = 0; i < 10; i++) {
      state = __LINE__ + 2; /* so we will come back to "case __LINE__" */
      return i;
      case __LINE__:; /* resume control straight after the return */
    }
  }
}

用static變量保存上下文,再用switch進行代碼行的跳轉,就可以實現一個簡單的協程。

協程的運用

協程模型

總結

協程主要適合於一些IO比較頻繁的系統,在這樣的系統中,使用協程跟多線程的優缺點比較如下:

  • 單線程異步IO: 優點是性能高,代碼執行是順序的,不需要關心鎖,競爭等情況;缺點是需要自己處理異步I/O、epoll等,無法便捷地做到hook所有fd操作;
  • 協程: 比單線程異步I/O容易編程,代碼更好寫,協程裏面是順序編程的,但協程之間是獨立棧,共享堆內存,單線程執行環境,在一個CPU上運行。協程切換代價比線程少多了,只需要十幾條彙編指令切換寄存器。每秒據說能達到上百萬次切換。
  • 多線程同步I/O: 代碼相對也好寫,跟協程一樣獨立棧,共享堆內存。 需要處理資源競爭問題,而且線程切換代價特別大,linux裏面沒有原生的線程,是用進程實現的。
  • 多進程:代碼相對容易編寫,但需要解決共享數據的問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章