導語
IO多路復用:通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。
應用:適用於針對大量的io請求的情況,對於服務器必須在同時處理來自客戶端的大量的io操作的時候,就非常適合
與多進程和多線程技術相比,I/O多路復用技術的最大優勢就是系統開銷小,系統不必創建進程/線程,也不必維護這些進程/線程,從而大大減小了系統的開銷。
目前支持I/O多路復用的系統調用有select, pselect, poll, epoll, 但他們本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
select, pselect, poll, epoll
都是屬於IO設計模式Reactor的IO策略。
IO多路復用使用場景
IO多路復用是指內核一旦發現進程指定的一個或者多個IO條件准備讀取,它就通知該進程。IO多路復用適用如下場合:
- 當客戶處理多個描述符時(一般是交互式輸入和網絡套接口),必須使用I/O復用。
- 當一個客戶同時處理多個套接口時,這種情況是可能的,但很少出現。
- 如果一個TCP服務器既要處理監聽套接口,又要處理已連接套接口,一般也要用到I/O復用。
- 如果一個服務器即要處理TCP,又要處理UDP,一般要使用I/O復用。
- 如果一個服務器要處理多個服務或多個協議,一般要使用I/O復用。
select
select基本原理
select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用后select函數會阻塞,直到有描述符就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函數返回。當select函數返回后,可以通過遍歷fdset,來找到就緒的描述符。
select基本流程
select函數原型
該函數准許進程指示內核等待多個事件中的任何一個發送,並只在有一個或多個事件發生或經歷一段指定的時間后才喚醒自己。函數原型如下:
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就緒描述符的數目,超時返回0,出錯返回-1
函數參數介紹如下:
(1)第一個參數maxfdp1指定待測試的描述字個數,它的值是待測試的最大描述字加1(因此把該參數命名為maxfdp1).
描述字0、1、2...(maxfdp1-1)均將被測試(文件描述符是從0開始的)。
(2)中間的三個參數readset、writeset和exceptset指定我們要讓內核測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為空指針。
struct fd_set可以理解為一個集合,這個集合中存放的是文件描述符,可通過以下四個宏進行設置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //將一個給定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //將一個給定的文件描述符從集合中刪除
int FD_ISSET(int fd, fd_set *fdset); // 檢查集合中指定的文件描述符是否可以讀寫
(3)timeout指定等待的時間,告知內核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
這個參數有三種可能:
(1)永遠等待下去:僅在有一個描述字准備好I/O時才返回。為此,把該參數設置為空指針NULL。
(2)等待一段固定時間:在有一個描述字准備好I/O時返回,但是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。
(3)根本不等待:檢查描述字后立即返回,這稱為輪詢。為此,該參數必須指向一個timeval結構,而且其中的定時器值必須為0。
select優點
- 跨平台。(幾乎所有的平台都支持)
- 時間精度高。(ns級別)
select缺點
- 最大限制:單個進程能夠監視的文件描述符的數量存在最大限制。(基於數組存儲的趕腳)一般來說這個數目和系統內存關系很大,具體數目可以cat /proc/sys/fs/file-max察看。它由FD_SETSIZE設置,32位機默認是1024個。64位機默認是2048.
- 時間復雜度: 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低,時間復雜度O(n)。
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。
它僅僅知道有I/O事件發生了,卻並不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。所以select具有O(n)的無差別輪詢復雜度,同時處理的流越多,無差別輪詢時間就越長。 - 內存拷貝:需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時復制開銷大。
poll
改進了select最大數量限制。
poll基本原理
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd后沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。
poll基本流程
類似select
poll函數原型
函數格式如下所示:
# include <poll.h># include <arpa/inet.h>int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
(1)pollfd結構體定義如下:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 實際發生了的事件 */
} ;
每一個pollfd結構體指定了一個被監視的文件描述符。因此可以傳遞多個結構體,指示poll()監視多個文件描述符。(2)events域是監視該文件描述符的事件掩碼,由用戶來設置這個域。
POLLIN 有數據可讀。
POLLRDNORM 有普通數據可讀。
POLLRDBAND 有優先數據可讀。
POLLPRI 有緊迫數據可讀。
POLLOUT 寫數據不會導致阻塞。
POLLWRNORM 寫普通數據不會導致阻塞。
POLLWRBAND 寫優先數據不會導致阻塞。
POLLMSGSIGPOLL 消息可用。(3)revents域是文件描述符的操作結果事件掩碼,內核在調用返回時設置這個域。events域中請求的任何事件都可能在revents域中返回。
此外,revents域中還可能返回下列事件:
POLLER 指定的文件描述符發生錯誤。
POLLHUP 指定的文件描述符掛起事件。
POLLNVAL 指定的文件描述符非法。
這些事件在events域中無意義,因為它們在合適的時候總是會從revents中返回。
(4)舉個栗子:要同時監視一個文件描述符是否可讀和可寫,
我們可以設置 events 為POLLIN |POLLOUT。
在poll返回時,我們可以檢查revents中的標志,對應於文件描述符請求的events結構體。
如果POLLIN事件被設置,則文件描述符可以被讀取而不阻塞。
如果POLLOUT被設置,則文件描述符可以寫入而不導致阻塞。
這些標志並不是互斥的:它們可能被同時設置,表示這個文件描述符的讀取和寫入操作都會正常返回而不阻塞。
(5)nfds參數是數組fds元素的個數。
(6)timeout參數指定等待的毫秒數,無論I/O是否准備好,poll都會返回。
timeout指定為負數值表示無限超時,使poll()一直掛起直到一個指定事件發生;
timeout為0指示poll調用立即返回並列出准備好I/O的文件描述符,但並不等待其它的事件。
(7)返回值和錯誤代碼
成功時,poll()返回結構體中revents域不為0的文件描述符個數;
如果在超時前沒有任何事件發生,poll()返回0;
失敗時,poll()返回-1,
並設置errno為下列值之一:
EBADF 一個或多個結構體中指定的文件描述符無效。
EFAULTfds 指針指向的地址超出進程的地址空間。
EINTR 請求的事件之前產生一個信號,調用可以重新發起。
EINVALnfds 參數超出PLIMIT_NOFILE值。
ENOMEM 可用內存不足,無法完成請求。
poll優點
沒有最大連接數的限制。(基於鏈表來存儲的)
poll缺點
- 時間復雜度: 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低,時間復雜度O(n)。
它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd后沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。 - 內存拷貝:大量的fd數組被整體復制於用戶態和內核地址空間之間,而不管這樣的復制是不是有意義。
大量的fd數組被整體復制於用戶態和內核地址空間之間,而不管這樣的復制是不是有意義。 - 水平觸發:如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。
注意:select和poll都需要在返回后,通過遍歷文件描述符來獲取已經就緒的socket。
事實上,同時連接的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨着監視的描述符數量的增長,其效率也會線性下降。
epoll
epoll是在2.6內核中提出的,是之前的select和poll的增強版本。是為處理大批量句柄而作了改進的poll。
epoll使用一個文件描述符管理多個描述符,將用戶關系的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的拷貝只需要一次。``
epoll基本原理
epoll有兩大特點:
- 邊緣觸發,它只告訴進程哪些fd剛剛變為就緒態,並且只會通知一次。
- 事件驅動,每個事件關聯上fd,使用事件就緒通知方式,通過 epoll_ctl 注冊 fd,一旦該fd就緒,內核就會采用 callback 的回調機制來激活該fd,epoll_wait 便可以收到通知。
epoll基本流程
一棵紅黑樹,一張准備就緒句柄鏈表,少量的內核cache,就幫我們解決了大並發下的socket處理問題。
- 執行 epoll_create
內核在epoll文件系統中建了個file結點,(使用完,必須調用close()關閉,否則導致fd被耗盡)
在內核cache里建了紅黑樹存儲epoll_ctl傳來的socket,
在內核cache里建了rdllist雙向鏈表存儲准備就緒的事件。 - 執行 epoll_ctl
如果增加socket句柄,檢查紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內核注冊回調函數,告訴內核如果這個句柄的中斷到了,就把它放到准備就緒list鏈表里。
ps:所有添加到epoll中的事件都會與設備(如網卡)驅動程序簡歷回調關系,相應的事件發生時,會調用回調方法。 - 執行 epoll_wait
立刻返回准備就緒表里的數據即可(將內核cache里雙向列表中存儲的准備就緒的事件 復制到用戶態內存)
當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。
如果rdlist不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶。
epoll函數原型
epoll操作過程需要三個接口,分別如下:
#include <sys/epoll.h>
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);
int epoll_create(int size); /*創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。*/
這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值。 需要注意的是: 當創建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的, 所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊函數. 它不同與select()是在監聽事件時告訴內核要監聽什么類型的事件epoll的事件注冊函數,而是在這里先注冊要監聽的事件類型。
第一個參數 epfd 是epoll_create()的返回值,
第二個參數 op 表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是需要監聽的fd,
第四個參數是告訴內核需要監聽什么事,
struct epoll_event結構如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下幾個宏的集合:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉); EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來); EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產生
類似於select()調用。
參數 events用來從內核得到事件的集合,
參數 maxevents告之內核這個events有多大,這個maxevents的值不能大於創建epoll_create()時的size,
參數 timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。
該函數返回需要處理的事件數目,如返回0表示已超時。
4.4 epoll優點
- 沒有最大連接數的限制。(基於 紅黑樹+雙鏈表 來存儲的:1G的內存上能監聽約10萬個端口)
- 時間復雜度低: 邊緣觸發和事件驅動,監聽回調,時間復雜度O(1)。
只有活躍可用的fd才會調用callback函數;即epoll最大的優點就在於它只管“活躍”的連接,而跟連接總數無關,因此實際網絡環境中,Epoll的效率就會遠遠高於select和poll。 - 內存拷貝:利用mmap()文件映射內存加速與內核空間的消息傳遞,減少拷貝開銷。
4.5 epoll缺點
依賴於操作系統:Lunix
4.6 epoll應用場景
適合用epoll的應用場景:
- 對於連接特別多,活躍的連接特別少
- 典型的應用場景為一個需要處理上萬的連接服務器,例如各種app的入口服務器,例如qq
不適合epoll的場景:
- 連接比較少,數據量比較大,例如ssh
epoll 的驚群問題:
因為epoll 多用於多個連接,只有少數活躍的場景,但是萬一某一時刻,epoll 等的上千個文件描述符都就緒了,這時候epoll 要進行大量的I/O,此時壓力太大。
4.7 epoll兩種模式
epoll對文件描述符的操作有兩種模式:LT(level trigger) 和 ET(edge trigger)。LT是默認的模式,ET是“高速”模式。
- LT(水平觸發)模式下,只要有數據就觸發,緩沖區剩余未讀盡的數據會導致 epoll_wait都會返回它的事件;
- ET(邊緣觸發)模式下,只有新數據到來才觸發,不管緩存區中是否還有數據,緩沖區剩余未讀盡的數據不會導致epoll_wait返回。
1、LT模式
LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket
。在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,只要這個文件描述符還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作
。
2、ET模式
ET(edge-triggered)是高速工作方式,只支持no-block socket
。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,並且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)
。
在它檢測到有 I/O 事件時,通過 epoll_wait 調用會得到有事件通知的文件描述符,對於每一個被通知的文件描述符,如可讀,則必須將該文件描述符一直讀到空,讓 errno 返回 EAGAIN (提示你的應用程序現在沒有數據可讀請稍后再試)為止,否則下次的 epoll_wait 不會返回余下的數據,會丟掉事件。

ET模式在很大程度上減少了epoll事件被重復觸發的次數,因此效率要比LT模式高
。epoll工作在ET模式的時候,必須使用非阻塞套接口
,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
注意:1. 在select/poll中,
進程只有在調用一定的方法后,內核才對所有監視的文件描述符進行掃描
,而epoll事先通過epoll_ctl()來注冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會采用類似callback的回調機制
,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。此處去掉了遍歷文件描述符,而是通過監聽回調的的機制。這正是epoll的魅力所在。
- 如果沒有大量的idle-connection或者dead-connection,epoll的效率並不會比select/poll高很多,但是當遇到大量的idle-connection,就會發現epoll的效率大大高於select/poll。
五、select、poll、epoll區別
它們三個都是 就緒設備 通知 。
1、支持一個進程所能打開的最大連接數
select | 單個進程所能打開的最大連接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是3232,同理64位機器上FD_SETSIZE為3264),當然我們可以對進行修改,然后重新編譯內核,但是性能可能會受到影響,這需要進一步的測試。 |
---|---|
poll | poll本質上和select沒有區別,但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的 |
epoll | 雖然連接數有上限,但是很大,1G內存的機器上可以打開10萬左右的連接,2G內存的機器可以打開20萬左右的連接 |
2、FD劇增后帶來的IO效率問題
select | 因為每次調用時都會對連接進行線性遍歷,所以隨着FD的增加會造成遍歷速度慢的“線性下降性能問題”。 |
---|---|
poll | 同上 |
epoll | 因為epoll內核中實現是根據每個fd上的callback函數來實現的,只有活躍的socket才會主動調用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。 |
3、 消息傳遞方式
select | 內核需要將消息傳遞到用戶空間,都需要內核拷貝動作 |
---|---|
poll | 同上 |
epoll | epoll通過mmap把對應設備文件片斷映射到用戶空間上, 消息傳遞不通過內核, 內存與設備文件同步數據. |
總結:
綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。
1、表面上看epoll的性能最好,但是在連接數少並且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。
2、select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善
摘錄地址
聊聊IO多路復用之select、poll、epoll詳解
IO多路復用之select總結
IO多路復用之poll總結
IO多路復用之epoll總結
高並發網絡編程之epoll詳解
epoll原理詳解及epoll反應堆模型
Linux五種IO模型性能分析
LT(水平觸發)和ET(邊緣觸發)
java BIO/NIO/AIO 學習