通常來說,網絡IO可以抽象成使用者态和核心态之間的資料交換。一次網絡資料讀取操作(read),可以拆分成兩個步驟:1)網卡驅動等待資料準備好(核心态)2)将資料從核心空間拷貝到程序空間(使用者态)。根據這兩個步驟處理方式不一樣,我們通常把網絡IO劃分成阻塞IO和非阻塞IO。
·阻塞IO。使用者調用網絡IO相關的系統調用時(例如read),如果此時核心網卡還沒有讀取到網絡資料,那麼本次系統調用将會一直阻塞,直到對端系統發送的資料到達為止。如果對端一直沒有發送資料,則本次調用将永遠不會傳回。
· 非阻塞IO。當使用者調用網絡IO相關的系統調用時(例如read),如果此時核心網絡還沒有收到網絡資料,那麼本次系統調用将會立即傳回,并傳回一個EAGAIN的錯誤碼。
在沒有IO多路複用技術之前,由于沒有一種好的方式來探測網絡IO是否可讀可寫。是以,為了增加系統的并發連接配接量,一般是借助多線程或多程序的方式來增加系統的并發連接配接數。但是這種方式有個問題就是系統的并發連接配接數受限于作業系統的最大線程或程序數,并且随着作業系統的線程或程序數增加,将會引發大量的上下文切換,導緻系統的性能急劇下降。為了解決這個問題,作業系統引入了IO多路轉接技術(IO multiplexing)。
IO多路轉接技術其實就是使用select、epoll等作業系統提供的系統調用來檢測IO事件的各種機制。通過select、epoll等機制,我們可以很輕松的同時管理大量的網絡IO連接配接,并且擷取到處于活躍狀态的連接配接。當其中一個或多個發生網絡IO事件時,select、epoll等系統調用就會傳回相應的連接配接,我們就可以對這些連接配接進行讀取或寫入操作,進而完成網絡資料互動。
select函數原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
select各個參數說明:
· nfds
這個參數的值一般設定為讀集合(readfds)、寫集合(writefds)以及exceptfds(異常集合)中最大的描述符(fd)+1,當然也可以設定為FD_SETSIZE。FD_SETSIZE是作業系統定義的一個宏,一般是1024。也就是說讀寫以及異常集合大小的最大值是1024,是以使用select最多隻能管理1024個連接配接。如果大于1024個連接配接,select将會産生不确定行為。
· readfds
指向可讀描述符集的指針,如果我們關心連接配接的可讀事件,需要把連接配接的描述符設定到讀集合中。
·writefds
指向可寫描述符集的指針,如果我們關心連接配接的可寫事件,需要把連接配接的描述符設定到可寫集合中。
· exceptfds
指向異常描述符集的指針,如果我們關心連接配接的是否發生異常,需要把連接配接的描述符設定到異常描述符集合中。
·timeout
指select願意等待的時間。
struct timeval {
longtv_sec; //秒數
longtv_usec; //微秒數
}
一般來說,分為三種情況:
·timeout為空,select将會永遠等待。直到有連接配接可讀、可寫或者被信号中斷時傳回。
·timeout->tv_sec = 0 且 timeout->tv_usec = 0,完全不等待。檢測所有指定的描述符後立即傳回。這是得到多個描述符的狀态而不阻塞select函數的輪詢方法。
·timeout->tv_sec != 且 timeout->tv_usec != 0,等待指定的秒數和微秒數。當指定的描述符之一已經準備好,或者超過了指定的時間值,則立即傳回。如果逾時了,還沒有一個描述符準備好,則傳回0。
select的工作原理,select通過輪詢來檢測各個集合中的描述符(fd)的狀态,如果描述符的狀态發生改變,則會在該集合中設定相應的标記位;如果指定描述符的狀态沒有發生改變,則将該描述符從對應集合中移除。是以,select的調用複雜度是線性的,即O(n)。舉個例子,一個保姆照看一群孩子,如果把孩子是否需要尿尿比作網絡IO事件,select的作用就好比這個保姆挨個詢問每個孩子:你要尿尿嗎?如果孩子回答是,保姆則把孩子拎出來放到另外一個地方。當所有孩子詢問完之後,保姆領着這些要尿尿的孩子去上廁所(處理網絡IO事件)。
select的限制,前面提到FD_SETSIZE宏,這個宏是作業系統定義的。在linux下面通常是1024,也就是說select最多隻能管理1024個描述符。如果大于1024的個描述,select将會産生不可預知的行為。那在沒有poll或epoll的情況下,怎樣使用select來處理連接配接數大于1024的情況呢?答案是使用多線程技術,每個線程單獨使用一個select進行檢測。這樣的話,你的系統能夠處理的并發連接配接數等于線程數*1024。早期的apache就是這種技術來支撐海量連接配接的。
epoll函數原型:
int epoll_create(int size);
intepoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(intepfd, struct epoll_event *events, intmaxevents, int timeout);
epoll依賴上述三個函數,既可以完成成千上萬的并發連接配接管理。epoll使用方式,1)通過epoll_create建立epoll句柄。2)将描述符所感興趣的事件通過epoll_ctl添加到epoll句柄中。3)調用epoll_wait傳回所有可讀寫的描述符。
還是以保姆照看一群孩子為例,在epoll機制下,保姆不再需要挨個的詢問每個孩子是否需要尿尿。取而代之的是,每個孩子如果自己需要尿尿的時候,自己主動的站到事先約定好的地方,而保姆的職責就是檢視事先約定好的地方是否有孩子。如果有小孩,則領着孩子去上廁所(網絡事件處理)。是以,epoll的這種機制,能夠高效的處理成千上萬的并發連接配接,而且性能不會随着連接配接數增加而下降。
綜上所述,select和epoll對比如下表所示
select
epoll
性能
随着連接配接數增加,急劇下降。處理成千上萬并發連接配接數時,性能很差。
随着連接配接數增加,性能基本上沒有下降。處理成千上萬并發連接配接時,性能很好。
連接配接數
連接配接數有限制,處理的最大連接配接數不超過1024。如果要處理超過1024個連接配接數,則需要修改FD_SETSIZE宏,并重新編譯 。
連接配接數無限制。
内在處理機制
線性輪詢
回調callback
開發複雜性
低
中
老男孩教育:select和epoll簡單差別比喻
select的調用複雜度是線性的,即O(n)。舉個例子,一個保姆照看一群孩子,如果把孩子是否需要尿尿比作網絡IO事件,select的作用就好比這個保姆挨個詢問每個孩子:你要尿尿嗎?如果孩子回答是,保姆則把孩子拎出來放到另外一個地方。當所有孩子詢問完之後,保姆領着這些要尿尿的孩子去上廁所(處理網絡IO事件)。
還是以保姆照看一群孩子為例,在epoll機制下,保姆不再需要挨個的詢問每個孩子是否需要尿尿。取而代之的是,每個孩子如果自己需要尿尿的時候,自己主動的站到事先約定好的地方,而保姆的職責就是檢視事先約定好的地方是否有孩子。如果有小孩,則領着孩子去上廁所(網絡事件處理)。是以,epoll的這種機制,能夠高效的處理成千上萬的并發連接配接,而且性能不會随着連接配接數增加而下降。
本文轉自 藍葉子Sheep 51CTO部落格,原文連結:http://blog.51cto.com/dellinger/1952776,如需轉載請自行聯系原作者