I/O多路複用和Socket

原文:I/O多路複用和Socket

由於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多路複用技術有selectpollepoll等。select具有最大數量描述符限制,而epoll則沒有,並且在機制上,epoll也更爲高效。select的優勢僅僅是跨平臺支持性,所有平臺和較低版本的內核都支持select模式,epoll則不是。

在IO相關的編程中,IO複用起到的作用相當於一個閥門,讓後續IO操作更爲精準高效。

編程模型

綜上討論,我們在進行實際的Socket編程的時候,無論是客戶端還是服務端,大致有幾種模式可以選擇:

  1. 阻塞式。純採用阻塞式,這種方式很少見,基本只會出現在demo中。多個描述符需要用多個進程或者線程來一一對應處理。

  2. 非阻塞式。純非阻塞式,對IO的就緒與否需要在用戶空間通過輪詢來實現。

  3. IO多路複用+阻塞式。僅使用一個線程就可以實現對多個描述符的狀態管理,但由於IO輸入輸出調用本身是阻塞的,可能出現某個IO輸入輸出過慢,影響其他描述符的效率,從而體現出整體性能不高。此種方式編程難度比較低。

  4. 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得到相應的待處理錯誤。

補充:

非阻的調用recvsendaccept,分別地,如果收緩衝中無數據、發送緩衝不夠空間發、沒有外來連接,將立即返回,此時全局errno將得到EWOULDBLOCKEAGIAN,表示“本應阻塞的調用,由於採用了非阻塞模式,而返回”。非阻的調用connect將立即返回,此時全局errno將得到EINPROGRESS,表示連接正在進行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章