select、poll、epoll總結及ET、LT區別
select,poll,epoll都是IO多路複用的機制。I/O多路複用就是通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。
但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。
I/O複用模型會用到select、poll、epoll函式:對一個IO埠,兩次呼叫,兩次返回,比阻塞IO並沒有什麼優越性。但關鍵是能實現同時對多個IO埠進行監聽。
這幾個函式也會使程序阻塞,但是和阻塞I/O所不同的是,這幾個函式可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函式進行檢測,直到有資料可讀或可寫時,才真正呼叫I/O操作函式。
1、select
1.1 select進行IO複用原理
當一個客戶端連線上伺服器時,伺服器就將其連線的fd加入fd_set集合,等到這個連線準備好讀或寫的時候,就通知程式進行IO操作,與客戶端進行資料通訊。!!!
大部分 Unix/Linux 都支援 select 函式,該函式用於探測多個檔案描述符的狀態變化。
1.2 select函式原型
int select(
int maxfdp, //Winsock中此引數無意義
fd_set* readfds, //進行可讀檢測的Socket
fd_set* writefds, //進行可寫檢測的Socket
fd_set* exceptfds, //進行異常檢測的Socket
const struct timeval* timeout //非阻塞模式中設定最大等待時間
)
1.3 使用select的步驟
1)建立所關注的事件的描述符集合(fd_set),對於一個描述符,可以關注其上面的讀(read)、寫(write)、異常(exception)事件,所以通常,要建立三個fd_set,一個用來收集關注讀事件的描述符,一個用來收集關注寫事件的描述符,另外一個用來收集關注異常事件的描述符集合。
2)呼叫select()等待事件發生。這裡需要注意的一點是,select的阻塞與是否設定非阻塞I/O是沒有關係的。
3)輪詢所有fd_set中的每一個fd,檢查是否有相應的事件發生,如果有,就進行處理。
1.4 select的優缺點
相比其他模型,使用 select() 的事件驅動模型只用單執行緒(程序)執行,佔用資源少,不消耗太多 CPU,同時能夠為多客戶端提供服務。如果試圖建立一個簡單的事件驅動的伺服器程式,這個模型有一定的參考價值。
select的缺點:
(1)每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大!!!(複製大量控制代碼資料結構,產生巨大的開銷 )。
(2)同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大!!!(消耗大量時間去輪詢各個控制代碼,才能發現哪些控制代碼發生了事件)。
(3)單個程序能夠監視的檔案描述符的數量存在最大限制,32位機預設是1024。
(4)select的觸發方式是水平觸發,應用程式如果沒有完成對一個已經就緒的檔案描述符進行IO操作,那麼之後每次select呼叫還是會將這些檔案描述符通知程序。
(5)該模型將事件探測和事件響應夾雜在一起,一旦事件響應的執行體龐大,則對整個模型是災難性的。
2、poll
poll庫是在linux2.1.23中引入的,windows平臺不支援poll。poll本質上和select沒有太大區別,都是先建立一個關注事件的描述符的集合,然後再去等待這些事件發生,然後再輪詢描述符集合,檢查有沒有事件發生,如果有,就進行處理。
因此,poll有著與select相似的處理流程:
1)建立描述符集合,設定關注的事件
2)呼叫poll(),等待事件發生。下面是poll的原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
類似select,poll也可以設定等待時間,效果與select一樣。
3)輪詢描述符集合,檢查事件,處理事件。
poll與select的主要區別在於:select需要為讀、寫、異常事件分別建立一個描述符集合,最後輪詢的時候,需要分別輪詢這三個集合。而poll只需要一個集合,在每個描述符對應的結構上分別設定讀、寫、異常事件,最後輪詢的時候,可以同時檢查三種事件。
它沒有最大連線數的限制,原因是它是基於連結串列來儲存的。
缺點:
1)大量的fd的陣列被整體複製於使用者態和核心地址空間之間,而不管這樣的複製是不是有意義。
2)poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。
3、epoll
3.1 epoll概述
poll和select,它們的最大的問題就在於效率。它們的處理方式都是建立一個事件列表,然後把這個列表發給核心,返回的時候,再去輪詢檢查這個列表,這樣在描述符比較多的應用中,效率就顯得比較低下了。
epoll是一種比較好的做法,它把描述符列表交給核心,一旦有事件發生,核心把發生事件的描述符列表通知給程序,這樣就避免了輪詢整個描述符列表。
epoll支援水平觸發和邊緣觸發,最大的特點在於邊緣觸發,它只告訴程序哪些fd剛剛變為就緒態,並且只會通知一次。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,核心就會採用類似callback的回撥機制來啟用該fd,epoll_wait便可以收到通知。
epoll與select和poll的呼叫介面上的不同:select和poll都只提供了一個函式——select或者poll函式。而epoll提供了三個函式,epoll_create,epoll_ctl和epoll_wait
,epoll_create是建立一個epoll控制代碼;epoll_ctl是註冊要監聽的事件型別;epoll_wait則是等待事件的產生。
3.2 epoll的使用步驟
1)建立一個epoll描述符,呼叫epoll_create()來完成。epoll_create()有一個整型的引數size,用來告訴核心,要建立一個有size個描述符的事件列表(集合)。
int epoll_create(int size)
2)給描述符設定所關注的事件,並把它新增到核心的事件列表中。這裡需要呼叫epoll_ctl()來完成。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
3)等待核心通知事件發生,得到發生事件的描述符的結構列表。該過程由epoll_wait()完成。得到事件列表後,就可以進行事件處理了。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
3.3 epoll的LT和ET的區別
水平觸發和邊緣觸發的區別:只要控制代碼滿足某種狀態,水平觸發就會發出通知;而只有當控制代碼狀態改變時,邊緣觸發才會發出通知。
LT:水平觸發,效率會低於ET觸發,尤其在大併發,大流量的情況下。但是LT對程式碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有資料沒有被獲取,核心就不斷通知你,因此不用擔心事件丟失的情況。
ET:邊緣觸發,效率非常高,在併發,大流量的情況下,會比LT少很多epoll的系統呼叫,因此效率高。但是對程式設計要求高,需要細緻的處理每個請求,否則容易發生丟失事件的情況。
3.4 epoll的優點
1)沒有最大併發連線的限制,能開啟FD的上限遠大於1024(1G的記憶體上能監聽約10萬個埠);
2)效率提升。不是輪詢的方式,不會隨著FD數目的增加效率下降。只有活躍可用的FD才會呼叫callback函式;
即epoll最大的優點就在於它只管你“活躍”的連線,而跟連線總數無關,因此在實際的網路環境中,epoll的效率就會遠遠高於select和poll。
3)記憶體拷貝。epoll通過核心和使用者空間共享一塊記憶體來實現訊息傳遞的。利用mmap()檔案對映記憶體加速與核心空間的訊息傳遞;即epoll使用mmap 減少複製開銷。epoll保證了每個fd在整個過程中只會拷貝一次(select,poll每次呼叫都要把fd集合從使用者態往核心態拷貝一次)。
5、例題:
1、關於epoll和select的區別,哪些說法是正確的?
正確答案:A B C
A、epoll和select都是I/O多路複用的技術,都可以實現同時監聽多個I/O事件的狀態
B、epoll相比select效率更高,主要是基於其作業系統支援的I/O事件通知機制,而select是基於輪詢機制
C、epoll支援水平觸發和邊沿觸發兩種模式
D、select能並行支援I/O比較小,且無法修改