IO多路復用之Select、Poll、Epoll
本文轉載自微信公眾號「搬運工來架構」,作者cocodroid。轉載本文請聯(lián)系搬運工來架構公眾號。
I/O多路復用(multiplexing)的本質(zhì)是通過一種機制(系統(tǒng)內(nèi)核緩沖I/O數(shù)據(jù)),讓單個進程可以監(jiān)視多個文件描述符,一旦某個描述符就緒(一般是讀就緒或寫就緒),能夠通知程序進行相應的讀寫操作。
01select
- int select (int n, fd_set *readfds, fd_set *writefds,
- fd_set *exceptfds, struct timeval *timeout);
- // fd_set 結構體簡化為:
- typedef struct{
- long int fds_bits[32];
- }fd_set;
select 函數(shù)監(jiān)視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調(diào)用后select函數(shù)會阻塞,直到有描述符就緒(有數(shù)據(jù) 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函數(shù)返回。當select函數(shù)返回后,可以通過遍歷fdset,來找到就緒的描述符。
select本質(zhì)上是通過設置或者檢查存放fd標志位的數(shù)據(jù)結構來進行下一步處理。
缺點:
1、 單個進程可監(jiān)視的fd數(shù)量被限制,即能監(jiān)聽端口的大小有限。
一般來說這個數(shù)目和系統(tǒng)內(nèi)存關系很大,具體數(shù)目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
2、 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低:
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調(diào)度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調(diào)函數(shù),當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。
3、需要維護一個用來存放大量fd的數(shù)據(jù)結構,每次調(diào)用select時把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這樣會使得用戶空間和內(nèi)核空間在傳遞該結構時復制開銷大。
02poll
- int poll (struct pollfd *fds, unsigned int nfds, int timeout);
- struct pollfd {
- int fd; /* file descriptor */
- short events; /* requested events to watch */ // 請求監(jiān)視的事件
- short revents; /* returned events witnessed */ // 返回發(fā)生的事件
- };
和select沒有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個fd對應的設備狀態(tài),如果設備就緒則在設備等待隊列中加入一項并繼續(xù)遍歷,如果遍歷完所有fd后沒有發(fā)現(xiàn)就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經(jīng)歷了多次無謂的遍歷。
它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的。
缺點:
1、大量的fd的數(shù)組被整體復制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復制是不是有意義。
2、poll還有一個特點是“水平觸發(fā)”,如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。
LT模式:level trigger。當epoll_wait檢測到描述符事件發(fā)生并將此事件通知應用程序,
應用程序可以不立即處理該事件。下次調(diào)用epoll_wait時,會再次響應應用程序并通知此事件。
ET模式:edge trigger。當epoll_wait檢測到描述符事件發(fā)生并將此事件通知應用程序,
應用程序必須立即處理該事件。如果不處理,下次調(diào)用epoll_wait時,不會再次響應應用程序并通知此事件。
03epoll
- 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:創(chuàng)建一個epoll的句柄,size用來告訴內(nèi)核這個監(jiān)聽的數(shù)目一共有多大。參數(shù)size并不是限制了epoll所能監(jiān)聽的描述符最大個數(shù),只是對內(nèi)核初始分配內(nèi)部數(shù)據(jù)結構的一個建議。
epoll_ctl:對指定描述符fd執(zhí)行op操作。
-epfd:是epoll_create()的返回值。
-op操作:對應宏:添加EPOLL_CTL_ADD,刪除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD,對應添加、刪除和修改對fd的監(jiān)聽事件。
- fd:是需要監(jiān)聽的fd(文件描述符)。
- epoll_event:是告訴內(nèi)核需要監(jiān)聽什么事件(讀、寫事件等)。
epoll_wait:等待epfd上的io事件,最多返回maxevents個事件。
-events:用來從內(nèi)核得到事件的集合,
-maxevents:告之內(nèi)核這個events有多大,這個maxevents的值不能大于創(chuàng)建epoll_create()時的size,
-timeout:是超時時間。
epoll有EPOLLLT和EPOLLET兩種觸發(fā)模式,LT是默認的模式,ET是“高速”模式。LT模式下,只要這個fd還有數(shù)據(jù)可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作,而在ET(邊緣觸發(fā))模式中,它只會提示一次,直到下次再有數(shù)據(jù)流入之前都不會再提示了,無論fd中是否還有數(shù)據(jù)可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀光,也就是說一直讀到read的返回值小于請求值,或者遇到EAGAIN錯誤。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內(nèi)核就會采用類似callback的回調(diào)機制來激活該fd,epoll_wait便可以收到通知。
epoll為什么要有EPOLLET觸發(fā)模式?
如果采用EPOLLLT模式的話,系統(tǒng)中一旦有大量你不需要讀寫的就緒文件描述符,它們每次調(diào)用epoll_wait都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率.。而采用EPOLLET這種邊沿觸發(fā)模式的話,當被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫事件才會通知你!!!這種模式比水平觸發(fā)效率高,系統(tǒng)不會充斥大量你不關心的就緒文件描述符。
epoll優(yōu)點:
1、沒有最大并發(fā)連接的限制,能打開的FD的上限遠大于1024(1G的內(nèi)存上能監(jiān)聽約10萬個端口);
2、效率提升,不是輪詢的方式,不會隨著FD數(shù)目的增加效率下降。只有活躍可用的FD才會調(diào)用callback函數(shù);
即Epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數(shù)無關,因此在實際的網(wǎng)絡環(huán)境中,Epoll的效率就會遠遠高于select和poll。
3、 內(nèi)存拷貝,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少復制開銷。
04區(qū)別
0、底層數(shù)據(jù)結構
select:數(shù)組,poll:鏈表,epoll:紅黑樹。
1、支持一個進程所能打開的最大連接數(shù)
select 單個進程所能打開的最大連接數(shù)有FD_SETSIZE宏定義,其大小是32個整數(shù)的大小(在32位的機器上,大小就是32*32,同理64位機器上FD_SETSIZE為32*64),當然我們可以對進行修改,然后重新編譯內(nèi)核,但是性能可能會受到影響,這需要進一步的測試。
poll本質(zhì)上和select沒有區(qū)別,但是它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的。
epoll 雖然連接數(shù)有上限,但是很大,1G內(nèi)存的機器上可以打開10萬左右的連接,2G內(nèi)存的機器可以打開20萬左右的連接。
2、FD劇增后帶來的IO效率問題
select/poll 因為每次調(diào)用時都會對連接進行線性遍歷,所以隨著FD的增加會造成遍歷速度慢的“線性下降性能問題”。
epoll 因為epoll內(nèi)核中實現(xiàn)是根據(jù)每個fd上的callback函數(shù)來實現(xiàn)的,只有活躍的socket才會主動調(diào)用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。
3、消息傳遞方式
select/poll 內(nèi)核需要將消息傳遞到用戶空間,都需要內(nèi)核拷貝動作。
epoll通過內(nèi)核和用戶空間共享一塊內(nèi)存來實現(xiàn)的。
select、poll與epoll之間的區(qū)別總結圖:
歷史背景:
1)select出現(xiàn)是1984年在BSD里面實現(xiàn)的。
2)14年之后也就是1997年才實現(xiàn)了poll,其實拖那么久也不是效率問題, 而是那個時代的硬件實在太弱,一臺服務器處理1千多個鏈接簡直就是神一樣的存在了,select很長段時間已經(jīng)滿足需求 。
3)2002, 大神 Davide Libenzi 實現(xiàn)了epoll。
參考資料:
https://www.cnblogs.com/Anker/p/3265058.html
https://www.cnblogs.com/aspirant/p/9166944.html
https://www.cnblogs.com/dhcn/p/12731883.html