由於IO操作涉及到系統調用,涉及到用戶空間和內核空間的切換,所以理解系統的IO模型,對於需要進入到系統調用層面進行編程來說是很重要的。
阻塞IO和非阻塞IO
從程序編寫的角度來看,I/O就是調用一個或多個系統函數,完成對輸入輸出設備的操作。輸入輸出設置可以是顯示器、字符終端命令行、網絡適配器、磁盤等。操作系統在這些設備與用戶程序之間完成一個銜接,稱爲驅動程序,驅動程序向下驅動硬件,向上提供抽象的函數調用入口。
一般來說I/O操作是需要時間的,因爲這涉及到系統、硬件等計算器模塊的互相配合,所以必然不像普通的函數調用那樣能夠按照既定的方式立即返回。從用戶代碼的角度,I/O操作的系統調用分爲“阻塞”和“非阻塞”兩種。
-
“阻塞”的調用會在I/O調用完成前,掛起調用線程,即CPU會不再執行後續代碼,而是等到I/O完成後再回來繼續執行,在用戶代碼看來,線程停止執行了,在調用處等待了。
-
“非阻塞”的調用則不同,I/O調用基本上是立即返回,而且往往實際上I/O此時並沒有完成,所以需要用戶的程序輪詢結果。
那麼我們以網絡IO爲例,看一下對於一個服務器,“阻塞”和“非阻塞”兩種模式,該如何設計。由於服務器要同時服務多個客戶端,所以需要同時操作多個Socket。
可以看到,如果使用阻塞的IO方式,因爲每個Socket都會阻塞,爲了同時服務多個客戶端,需要多個線程同時掛起;而如果採用非阻塞的調用方式,則需要在一個線程中不斷輪訓每個客戶端是否有數據到來。
顯然純粹阻塞式的調用不可取,非阻塞式的調用看起來不錯,但是仍不夠好,因爲輪詢實際也是通過某種系統調用完成的,相當於在用戶空間進行的,效率不高,如果能夠在內核空間進行這種類似輪詢,然後讓內核通知用戶空間哪個IO就緒了,就更好了。於是引出接下來的概念:IO多路複用
IO多路複用
IO多路複用是一種系統調用,內核能夠同時對多個IO描述符進行就緒檢查。當所有被監聽的IO都沒有就緒時,調用將阻塞;當至少有一個IO描述符就緒時,調用將返回,用戶代碼可通過檢查究竟是哪個IO就緒來進一步處理業務。顯然,IO多路複用是解決系統裏面存在N個IO描述符的問題的,這裏必須明確IO複用和IO阻塞與否並不是一個概念,IO複用只檢測IO是否就緒(讀就緒或者寫就緒等),具體的數據的輸入輸出還是需要依靠具體的IO操作完成(阻塞操作或非阻塞操作)。最典型的IO多路複用技術有select
、poll
、epoll
等。select
具有最大數量描述符限制,而epoll
則沒有,並且在機制上,epoll
也更爲高效。select
的優勢僅僅是跨平臺支持性,所有平臺和較低版本的內核都支持select
模式,epoll
則不是。
在IO相關的編程中,IO複用起到的作用相當於一個閥門,讓後續IO操作更爲精準高效。
編程模型
綜上討論,我們在進行實際的Socket編程的時候,無論是客戶端還是服務端,大致有幾種模式可以選擇:
-
阻塞式。純採用阻塞式,這種方式很少見,基本只會出現在demo中。多個描述符需要用多個進程或者線程來一一對應處理。
-
非阻塞式。純非阻塞式,對IO的就緒與否需要在用戶空間通過輪詢來實現。
-
IO多路複用+阻塞式。僅使用一個線程就可以實現對多個描述符的狀態管理,但由於IO輸入輸出調用本身是阻塞的,可能出現某個IO輸入輸出過慢,影響其他描述符的效率,從而體現出整體性能不高。此種方式編程難度比較低。
-
IO多路複用+非阻塞式。在多路複用的基礎上,IO採用非阻塞式,可以大大降低單個描述符的IO速度對其他IO的影響,不過此種方式編程難度較高,主要表現在需要考慮一些慢速讀寫時的邊界情況,比如讀黏包、寫緩衝不夠等。
下面以select爲例,整理 在select下,socket的阻塞和非阻塞的一些問題。這些細節在編寫基於Socket的網絡程序時,尤其是底層數據收發時,是十分重要的。
socket讀就緒:
-
【阻/非阻】接收緩衝區有數據,數據量大於
SO_RCVLOWAT
水位(默認是0)。此時調用recv
將返回>0(即讀到的字節數)。 -
【阻/非阻】對端關閉,即收到FIN。此時調用
recv
將返回=0。 -
【阻/非阻】
accept
到一個新的連接,此時accept通常不會阻塞。 -
【阻/非阻】socket發生某種錯誤。此時調用recv將返回-1,並應通過
getsockopt
得到相應的待處理錯誤。
socket寫就緒:
-
【阻/非阻】發送緩衝區有空餘的空間,空間大小大於
SO_SNDLOWAT
水位(默認是2048)。這種就緒是水平觸發的,只要有空間就會觸發寫就緒,即如果保持對這種套接字的就緒檢查將使得select
每次都認爲有描述符寫就緒。所以應當對描述符進行寫狀態管理,一旦某個描述符可寫,應立即停止對該描述符的寫狀態檢查,直到寫緩衝區滿後,再次select寫狀態。 -
【阻/非阻】連接的寫半部關閉,此時調用send將產生
SIGPIPE
信號。 -
【非阻】
connect
完成。由於非阻的connect將不會阻塞握手過程,所以,當握手在後續時刻完成後,在此保持寫狀態檢查,將觸發一次就緒,表示connect完成。 -
【阻/非阻】socket發生某種錯誤。此時調用
send
將返回-1,並應通過getsockopt
得到相應的待處理錯誤。
補充:
非阻的調用recv
、send
、accept
,分別地,如果收緩衝中無數據、發送緩衝不夠空間發、沒有外來連接,將立即返回,此時全局errno
將得到EWOULDBLOCK
或EAGIAN
,表示“本應阻塞的調用,由於採用了非阻塞模式,而返回”。非阻的調用connect
將立即返回,此時全局errno
將得到EINPROGRESS
,表示連接正在進行。