IO複用,select、poll、epoll綜述


      如果不希望進程在對文件描述符執行I/O操作時被阻塞,我們可以創建一個新的進程來執行I/O。此時父進程可以去執行其他的任務,而子進程將阻塞直到I/O操作完成。如果我們需要處理多個文件描述符上的I/O,那麼需要爲每個文件描述符創建一個子進程。這種方法的問題在於開銷昂貴且複雜。創建及維護進程對系統來說都是開銷,而且一般來說子進程需要使用IPC機制來通知父進程有關I/O操作的狀態。

      如果使用多線程而不是多進程,能夠佔用更少的支援。但是線程間仍然需要通信,以告之有關I/O的操作狀態。尤其是如果使用多線程技術來最小化需要處理大量併發客戶的線程數量時。編碼工作變的複雜。(多線程特別有用的一個地方是如果應用程序需要調用一個會執行阻塞式I/O操作的第三方庫,那麼可以通過在分離的線程中調用這個庫從而避免應用被阻塞。)

 

I/O多路複用

I/O多路複用技術,使的單個進程或線程能同時檢查多個描述符。同時檢查多個描述符,看它們中的任何一個是否可以執行I/O操作。(準確的說,是看I/O系統調用是否可以非阻塞的執行)。需要注意這種技術都不會執行實際的I/O操作。它們只是告訴我們某個文件描述符已經處於就緒狀態了。這時需要調用其他的系統調用來完成I/O操作。

  •  系統調用select()和poll()在UNIX系統中已經存在了很長時間。同其他技術相比,它們主要的優勢在於可移植性,主要缺點在於當同時檢查大量文件描述符時性能延展性不佳。

select和poll存在的問題:

    1. 每次調用select或poll,內核都必須檢查所有被指定的文件描述符,看它們是否處於就緒狀態。當檢查大量處於處於密集範圍內的文件描述符時,該操作耗費的時間將大大超過接下來的操作。在輪詢描述符上花費太多時間,那麼應用程序響應I/O時間的延時可能會達到無法接受的程度。
    2. 每次調用sellect或poll,程序都必須傳遞一個表示所有需要檢查的文件描述符的結構體到內核,內核檢查多描述符後,修改這個結構體並返回給程序。對於poll,隨呆檢查文件描述符數量的增加,傳遞給內核的結構體的大小也隨之增加。當檢查大量的文件描述符時,從用戶空間到內核空間的來回拷貝這個結構體將佔用大量的CPU時間。對於select來說,這個結構體的大小固定是FD_SETSIZE,與待檢查的描述符無關。
    3. select或poll調用完成後,程序必須檢查返回的數據結構中的每個元素,以此查明哪個文件描述符處於就緒狀態。

(select和poll糟糕的性能延展性源自這些API的侷限性:通常,程序重複調用這些系統調用所檢查的文件描述符集合是相同的,可是內核並不會在每個調用成功後記錄下它們。信號驅動式I/O以及epoll可以使內核記錄下進程中感興趣的文件描述符)

  • epoll()的關鍵優勢在於它能讓應用程序高效的檢查大量的文件描述符。epoll的性能優勢源自內核能夠“記住”進程正在監視的文件描述符列表這一事實,與之相反的是,select和poll都必須反覆告訴內核哪些文件描述符需要監視。其主要缺點在於它專屬於Linux。(其他類UNIX提供了類似epoll的機制。比如,Solaris提供了特殊文件/dev/poll文件,其他一些BSD變種提供了kqueue(相比epoll,這是一種更爲通用的檢查機制))。

      (select和poll有更好的可移植性,而epoll則有更好的性能。對於某些應用來說,編寫一個軟件抽象層來檢查文件描述符事件是非常值得做的。有了這個抽象層,可移植的程序就可以在提供epoll機制的系統上應用epoll或類似API,而在其他系統上繼續使用select和poll。如Libevent庫就是這樣一個抽象層。)

 

水平觸發和邊沿觸發

區分兩種文件描述符就緒的通知模型:

  • 水平觸發條件:文件描述符此時可以非阻塞的執行I/O
  • 邊沿觸發條件:自上次狀態檢查以來出現了新的I/O活動(比如新的輸入)

I/O模型

水平觸發

邊沿觸發

select(), poll()

Y

 

信號驅動I/O

 

Y

epoll()

Y

Y

 

      當採用水平觸發通知時,我們可以在任意時刻檢查文件描述符的就緒狀態,只要處於就緒狀態,就可以對其執行I/O操作,然後重複檢查文件描述符,例如如果數據一次性沒有讀完,我們可以繼續檢查描述符狀態,此時會發現描述符仍然處於就緒狀態,然後我們可以繼續讀,以此類推。

      與之相反的是,當我們採用邊沿觸發時,只有當有I/O事件發生時我們纔會收到通知。在另一個I/O事件到來前我們不會收到任何新的通知。另外,當文件描述符收到I/O事件通知時,通常我們並不知道要處理多少I/O,因此應按如下規則來設計:

  • 收到通知後,程序應該在相應描述符上儘可能多的執行I/O。如果程序沒那麼做,那麼就有可能失去執行I/O的機會。因爲直到產生另一個通知爲止,在此之前程序都不會再接受到通知了。這將可能導致數據丟失。而且如果我們在某個時刻僅僅對一個描述符執行大量的I/O操作,可能會讓其他的文件描述符處於飢餓狀態。
  • 如果採用循環來對文件描述符執行儘可能多的I/O,而文件描述符又被置爲阻塞的,那麼最終當沒有更多I/O可執行時,I/O系統調用將阻塞。基於這個原因,每個被檢查的文件描述符通常都應該置爲非阻塞模式,在得到通知後重復執行I/O操作,直到相應的系統調用以錯誤碼EAGAIN或EWOULDBLOCK的形式失敗返回。

 

選擇非阻塞I/O模型配合使用

爲什麼選擇非阻塞型I/O會很有用:

  • 如上所述,非阻塞I/O通常和提供有邊緣觸發通知機制的I/O模型一起使用
  • 儘管水平觸發的系統調用,比如select、poll通知我們流式套接字的文件描述符已經寫就緒,如果我們在單個write()或send()調用中寫入足夠大塊的數據,那麼該調用將阻塞。
  • 如果多個進程(或線程)在同一個打開的文件描述符上執行I/O操作,那麼可能出現競爭(race condition),I/O多路複用的系統調用與接下來的實際的I/O操作不是原子的,從某個特定的進程角度看,文件描述符就緒狀態可能會在通知就緒和執行I/O調用之間發生變化,比如在這之間,另外一個進程對相同的文件描述符執行了I/O操作。結果就是本進程的阻塞式的I/O調用將可能被阻塞。(這種情況將發生在所有I/O模型上,無論它們是採用水平觸發還是邊緣觸發)

 

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