本文咱們主要了解一下Unix/Linux下5種網絡IO模型:blocking IO, nonblocking IO, IO multiplexing, signal driven IO, asynchronous IO以及select/poll/epoll的基本原理,更好的理解在高級語言中的異步編程,但以理解概念爲主,並不會涉及到具體的C語言代碼編寫,若是想要深刻的朋友建議閱讀Richard Stevens的Unix Network Programming。git
爲了更好的理解下面提到的Linux下5種網絡IO的概念,咱們仍是有必要先理清幾個概念。github
在Linux中,對於一次讀取IO的操做,數據並不會直接拷貝到程序的程序緩衝區。它首先會被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的緩衝區。p.s: 最後一句話很是重要,重複一遍。算法
Waiting for the data to be ready
(等待數據到達內核緩衝區)Copying the data from the kernel to the process
(從內核緩衝區拷貝數據到程序緩衝區)阻塞就是說咱們某一個請求不能當即獲得返回應答,不然就能夠理解爲非阻塞。編程
這裏先直接引用Stevens(POSIX)的定義:網絡
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes. An asynchronous I/O operation does not cause the requesting process to be blocked.異步
對於同步與異步,咱們能夠用一個簡單的生活場景來描述。當咱們排隊在實體店買東西能夠視做同步,而網購則能夠視做異步。實體店排隊這種同步情形顯然是很是的浪費時間,等待的這段時間咱們被阻塞住了不能幹其餘的事情,而網購只要咱們提交一下訂單以後其餘什麼都不用管了,商品到了,快遞員給咱們發送一個信號(打電話)咱們直接到門口去拿,等待的這段時間咱們能夠用來擼代碼。socket
p.s: 等你閱讀完文章的後面部分,回過頭來看異步其實就是將等待的這段時間去處理IO操做,把CPU(咱們的大腦)讓出來作其餘更有價值的事情(擼代碼),而不是像同步那樣去傻傻地排隊。更加詳細準確的定義能夠在閱讀完本文後面部分後參考維基百科。async
在Linux下面一切皆文件,文件描述符(file descriptor)是內核爲文件所建立的索引
,全部I/O操做都經過調用文件描述符(索引)來執行,包括下面咱們要提到的socket。Linux剛啓動的時候會自動設置0是標準輸入,1是標準輸出,2是標準錯誤。異步編程
你們仍是應該多結合Stevens的圖片來理解,不要只看我枯燥的文字總結。函數
如圖所示,進程調用一個recvfrom請求,可是它不能馬上收到回覆,直到數據返回,而後將數據從內核空間複製到程序空間。這裏咱們再次回顧開篇提到的兩個過程:
注意到沒有,在上面這兩個過程當中,進程都處於blocked(阻塞)狀態,在等待數據返回的過程當中不能空閒出來幹其餘的事情。
當咱們設置一個socket爲nonblocking(非阻塞),至關於告訴內核當咱們請求的IO操做不能當即獲得返回結果,不要把進程設置爲sleep狀態,而是返回一個錯誤信息(下圖中的EWOULDBLOCK)。
咱們來分析一下圖片中的整個流程。前三次咱們調用recvfrom請求,可是並無數據返回,因此內核只能返回一個錯誤信息(EWOULDBLOCK)。可是當咱們第四次調用recvfrom,數據已經準備好了,而後將它從內核空間複製到程序空間。
在非阻塞狀態下,咱們的過程一(wait for data)並非徹底的阻塞的,可是過程二(copy data from kernel to user)依然處於一個阻塞狀態。
IO複用的好處是咱們能夠經過(select/poll/epoll)一個時刻處理多個文件描述符,這裏以select爲例來分析一下。
IO複用實際上也是徹底阻塞的,請仔細看圖(圖中咱們有兩個return,前面咱們都只有一個return),Stevens在書中提到這裏並無阻塞在recfrom階段而是阻塞在select階段,其實這樣說並非很是的嚴謹,由於recform其實也是一個阻塞過程(圖中也描述了),recvfrom過程當中進程除了等待copy data from kernel to user之外,並不能空閒出來幹其餘事情。
兩個過程的都是阻塞的,看起來IO複用和阻塞IO相比彷佛並無什麼優點,並且還須要兩個return,可是這裏注意在IO複用中咱們能夠同時監聽多個文件描述符。
咱們也可使用信號驅動IO,要求內核通知咱們當文件描述符準備就緒之後發送相應的信號。
咱們根據圖片來分析一下。階段1:
咱們首先設置socket爲一個信號驅動IO,而且經過sigaction system call安裝一個signal handler,注意這個過程是瞬時的,因此這個階段是非阻塞的。階段2
: 當數據已經準備好了之後,一個SIGIO信號傳送給咱們的進程告訴咱們數據準備好了,而後進程開始等待數據從內核空間複製到程序空間(copy data from kernel to user),這個過程是阻塞的,由於咱們的進程只能等待數據複製完畢。
咱們來看一下異步的概念,異步就是說對於上面兩個步驟(wait for data 和copy of the data from the kernel to our buffer)當它們完成的時候會自動通知進程,在這段時間裏面進程什麼都不用操心,就像網購同樣,下了單什麼也不用管了等着快遞員通知咱們(即咱們一般所說的callback)。相比前面的信號驅動IO,異步IO兩個階段都是非阻塞的。
阻塞式IO(默認),非阻塞式IO(nonblock),IO複用(select/poll/epoll),signal driven IO(信號驅動IO)都是屬於同步型IO,由於在第二個階段: 從內核空間拷貝數據到程序空間的時候不能幹別的事。只有異步I/O模型(AIO)纔是符合咱們上面對於異步型IO操做的含義,在1.wait for data,2.copy data from kernel to user,這兩個等待/接收數據的時間段內進程能夠幹其餘的事情,只要等着被通知就能夠了。
即便如今的各個Linux版本廣泛引入了copy on write和線程,但實際上進程/線程之間的切換依然仍是一筆很大的開銷,這個時候咱們能夠考慮使用上面提到到多路IO複用,回顧一下咱們上面提到的多路IO複用模型的基本原理:一個進程能夠監視多個文件描述符,一旦某個文件描述符就緒(讀/寫準備就緒),可以信號通知程序進行相應的讀寫操做。下面咱們就來簡單的看一下多路IO複用的三種方式。
int select (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
如上面的方法聲明所示, select監聽三類描述符: readset(讀), writeset(寫), exceptset(異常), 咱們編程的時候能夠制定這三個參數監聽對應的文件描述符。正如前面提到的,select調用後進程會阻塞, 當select返回後,能夠經過遍歷fdset,來找到就緒的描述符。
select優勢在於它的跨平臺,可是也有顯著的缺點單個進程可以監視的文件描述符的數量存在最大限制,默認設置爲1024/2048,雖然設置能夠超過這一限制,可是這樣也可能會形成效率的下降。並且select掃描的時候也是採用的輪循,算法複雜度爲O(n),這在fdset不少時效率會較低。
下面總結一下select的三個缺點,在下面咱們來看epool是如何解決這些缺點的:
每次調用select,都須要將fd_set從用戶態拷貝到內核態。
每次調用select都要在內核遍歷全部傳遞過來的fd_set看哪些描述已經準備就緒。
select有1024的容量限制。
int poll (struct pollfd *fdarray, unsigned long nfds, int timeout);
poll和select並無太大的區別,可是它是基於鏈表實現的因此並無最大數量限制,它將用戶傳入的數據拷貝到內核空間,而後查詢每一個fd對應的設備狀態,若是設備就緒則在設備等待隊列中加入一項並繼續遍歷,若是遍歷完全部fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了屢次的遍歷。算法複雜度也是O(n)
。
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
select和poll都只提供了一個函數。而epoll提供了三個函數: epoll_create是建立一個epoll句柄, epoll_ctl是註冊要監聽的事件類型, epoll_wait則是等待事件的產生。與select相比,epoll幾乎沒有描述符限制(cat /proc/sys/fs/file-max可查看)。它採用一個文件描述符管理多個描述符,將用戶的文件描述符的事件存放到kernel的一個事件表中,這樣在程序空間和內核空間的只要作一次拷貝。它去掉了遍歷文件描述符這一步驟,採用更加先進的回調(callback)機制,算法複雜度降到了O(1)。p.s: 雖然表面看起來epoll很是好,可是對於鏈接數少而且鏈接都十分活躍的狀況下,select和poll的性能可能比epoll好,由於epoll是創建在大量的函數回調的基礎之上。
下面咱們來總結一下epoll是如何解決select的三個缺點的:
GitHub: https://github.com/ziwenxie
Blog: https://www.ziwenxie.site
本文爲做者原創,轉載請於文章開頭明顯處聲明博客出處:)