I/O多路複用
I/O多路複用(multiplexing)的本質是透過一種機制(系統核心緩衝I/O資料),讓單個程序可以監視多個檔案描述符,一旦某個描述符就緒(一般是讀就緒或寫就緒),能夠通知程式進行相應的讀寫操作
select、poll 和 epoll 都是 Linux API 提供的 IO 複用方式。
相信大家都瞭解了Unix五種IO模型,不瞭解的可以 => 檢視這裡
blocking IO - 阻塞IOnonblocking IO - 非阻塞IOIO multiplexing - IO多路複用signal driven IO - 訊號驅動IOasynchronous IO - 非同步IO其中前面4種IO都可以歸類為synchronous IO - 同步IO,而select、poll、epoll本質上也都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的。
與多程序和多執行緒技術相比,I/O多路複用技術的最大優勢是系統開銷小,系統不必建立程序/執行緒,也不必維護這些程序/執行緒,從而大大減小了系統的開銷。
在介紹select、poll、epoll之前,首先介紹一下Linux作業系統中基礎的概念:
使用者空間 / 核心空間現在作業系統都是採用虛擬儲存器,那麼對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了保證使用者程序不能直接操作核心(kernel),保證核心的安全,作業系統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間。程序切換為了控制程序的執行,核心必須有能力掛起正在CPU上執行的程序,並恢復以前掛起的某個程序的執行。這種行為被稱為程序切換。因此可以說,任何程序都是在作業系統核心的支援下執行的,是與核心緊密相關的,並且程序切換是非常耗費資源的。程序阻塞正在執行的程序,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新資料尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由執行狀態變為阻塞狀態。可見,程序的阻塞是程序自身的一種主動行為,也因此只有處於執行態的程序(獲得了CPU資源),才可能將其轉為阻塞狀態。當程序進入阻塞狀態,是不佔用CPU資源的。檔案描述符檔案描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向檔案的引用的抽象化概念。檔案描述符在形式上是一個非負整數。實際上,它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。在程式設計中,一些涉及底層的程式編寫往往會圍繞著檔案描述符展開。但是檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。快取I/O快取I/O又稱為標準I/O,大多數檔案系統的預設I/O操作都是快取I/O。在Linux的快取I/O機制中,作業系統會將I/O的資料快取在檔案系統的頁快取中,即資料會先被複製到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區複製到應用程式的地址空間。Select
我們先分析一下select函式
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
【引數說明】int maxfdp1 指定待測試的檔案描述字個數,它的值是待測試的最大描述字加1。fd_set *readset , fd_set *writeset , fd_set *exceptsetfd_set可以理解為一個集合,這個集合中存放的是檔案描述符(file descriptor),即檔案控制代碼。中間的三個引數指定我們要讓核心測試讀、寫和異常條件的檔案描述符集合。如果對某一個的條件不感興趣,就可以把它設為空指標。const struct timeval *timeout timeout告知核心等待所指定檔案描述符集合中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。
【返回值】int 若有就緒描述符返回其數目,若超時則為0,若出錯則為-1
select執行機制
select()的機制中提供一種fd_set的資料結構,實際上是一個long型別的陣列,每一個數組元素都能與一開啟的檔案控制代碼(不管是Socket控制代碼,還是其他檔案或命名管道或裝置控制代碼)建立聯絡,建立聯絡的工作由程式設計師完成,當呼叫select()時,由核心根據IO狀態修改fd_set的內容,由此來通知執行了select()的程序哪一Socket或檔案可讀。
從流程上來看,使用select函式進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了新增監視socket,以及呼叫select函式的額外操作,效率更差。但是,使用select以後最大的優勢是使用者可以在一個執行緒內同時處理多個socket的IO請求。使用者可以註冊多個socket,然後不斷地呼叫select讀取被啟用的socket,即可達到在同一個執行緒內同時處理多個IO請求的目的。而在同步阻塞模型中,必須透過多執行緒的方式才能達到這個目的。
select機制的問題每次呼叫select,都需要把fd_set集合從使用者態複製到核心態,如果fd_set集合很大時,那這個開銷也很大同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd_set,如果fd_set集合很大時,那這個開銷也很大為了減少資料複製帶來的效能損壞,核心對被監控的fd_set集合大小做了限制,並且這個是透過宏控制的,大小不可改變(限制為1024)Poll
poll的機制與select類似,與select在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大檔案描述符數量的限制。也就是說,poll只解決了上面的問題3,並沒有解決問題1,2的效能開銷問題。
下面是pll的函式原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);typedef struct pollfd { int fd; // 需要被檢測或選擇的檔案描述符 short events; // 對檔案描述符fd上感興趣的事件 short revents; // 檔案描述符fd上當前實際發生的事件} pollfd_t;
poll改變了檔案描述符集合的描述方式,使用了pollfd結構而不是select的fd_set結構,使得poll支援的檔案描述符集合限制遠大於select的1024
【引數說明】struct pollfd *fds fds是一個struct pollfd型別的陣列,用於存放需要檢測其狀態的socket描述符,並且呼叫poll函式之後fds陣列不會被清空;一個pollfd結構體表示一個被監視的檔案描述符,透過傳遞fds指示 poll() 監視多個檔案描述符。其中,結構體的events域是監視該檔案描述符的事件掩碼,由使用者來設定這個域,結構體的revents域是檔案描述符的操作結果事件掩碼,核心在呼叫返回時設定這個域
nfds_t nfds 記錄陣列fds中描述符的總數量
【返回值】int 函式返回fds集合中就緒的讀、寫,或出錯的描述符數量,返回0表示超時,返回-1表示出錯;
Epoll
epoll在Linux2.6核心正式提出,是基於事件驅動的I/O方式,相對於select來說,epoll沒有描述符個數限制,使用一個檔案描述符管理多個描述符,將使用者關心的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy只需一次。
Linux中提供的epoll相關函式如下:
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);
epoll_create 函式建立一個epoll控制代碼,引數size表明核心要監聽的描述符數量。呼叫成功時返回一個epoll控制代碼描述符,失敗時返回-1。epoll_ctl 函式註冊要監聽的事件型別。四個引數解釋如下:epfd 表示epoll控制代碼op 表示fd操作型別,有如下3種EPOLL_CTL_ADD 註冊新的fd到epfd中EPOLL_CTL_MOD 修改已註冊的fd的監聽事件EPOLL_CTL_DEL 從epfd中刪除一個fdfd 是要監聽的描述符event 表示要監聽的事件epoll_event 結構體定義如下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64;} epoll_data_t;
epoll_wait 函式等待事件的就緒,成功時返回就緒的事件數目,呼叫失敗時返回 -1,等待超時返回 0。epfd 是epoll控制代碼events 表示從核心得到的就緒事件集合maxevents 告訴核心events的大小timeout 表示等待的超時事件epoll是Linux核心為處理大批次檔案描述符而作了改進的poll,是Linux下多路複用IO介面select/poll的增強版本,它能顯著提高程式在大量併發連線中只有少量活躍的情況下的系統CPU利用率。原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被核心IO事件非同步喚醒而加入Ready佇列的描述符集合就行了。
epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得使用者空間程式有可能快取IO狀態,減少epoll_wait/epoll_pwait的呼叫,提高應用程式效率。
水平觸發(LT):預設工作模式,即當epoll_wait檢測到某描述符事件就緒並通知應用程式時,應用程式可以不立即處理該事件;下次呼叫epoll_wait時,會再次通知此事件邊緣觸發(ET): 當epoll_wait檢測到某描述符事件就緒並通知應用程式時,應用程式必須立即處理該事件。如果不處理,下次呼叫epoll_wait時,不會再次通知此事件。(直到你做了某些操作導致該描述符變成未就緒狀態了,也就是說邊緣觸發只在狀態由未就緒變為就緒時只通知一次)。LT和ET原本應該是用於脈衝訊號的,可能用它來解釋更加形象。Level和Edge指的就是觸發點,Level為只要處於水平,那麼就一直觸發,而Edge則為上升沿和下降沿的時候觸發。比如:0->1 就是Edge,1->1 就是Level。
ET模式很大程度上減少了epoll事件的觸發次數,因此效率比LT模式下高。
總結
一張圖總結一下select,poll,epoll的區別:
select |
poll |
epoll | |
操作方式 |
遍歷 |
遍歷 |
回撥 |
底層實現 |
陣列 |
連結串列 |
雜湊表 |
IO效率 | 每次呼叫都進行線性遍歷,時間複雜度為O(n) |
每次呼叫都進行線性遍歷,時間複雜度為O(n) |
事件通知方式,每當fd就緒,系統註冊的回撥函式就會被呼叫,將就緒fd放到readyList裡面,時間複雜度O(1) |
最大連線數 |
1024(x86)或2048(x64) |
無上限 |
無上限 |
fd複製 |
每次呼叫select,都需要把fd集合從使用者態複製到核心態 |
每次呼叫poll,都需要把fd集合從使用者態複製到核心態 |
呼叫epoll_ctl時複製進核心並儲存,之後每次epoll_wait不複製 |
epoll是Linux目前大規模網路併發程式開發的首選模型。在絕大多數情況下效能遠超select和poll。目前流行的高效能web伺服器Nginx正式依賴於epoll提供的高效網路套接字輪詢服務。但是,在併發連線不高的情況下,多執行緒+阻塞I/O方式可能效能更好。