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