IO多路複用:經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做。
html
應用:適用於針對大量的io請求的狀況,對於服務器必須在同時處理來自客戶端的大量的io操做的時候,就很是適合java
與多進程和多線程技術相比,I/O多路複用技術的最大優點就是系統開銷小,系統沒必要建立進程/線程,也沒必要維護這些進程/線程,從而大大減少了系統的開銷。linux
該函數准許進程指示內核等待多個事件中的任何一個發送,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒本身。函數原型以下:編程
#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。
1. 跨平臺。(幾乎全部的平臺都支持)設計模式
2. 時間精度高。(ns級別)數組
1. 最大限制:單個進程可以監視的文件描述符的數量存在最大限制。(基於數組存儲的趕腳)
緩存
通常來講這個數目和系統內存關係很大,具體數目能夠cat /proc/sys/fs/file-max察看。它由FD_SETSIZE設置,32位機默認是1024個。64位機默認是2048.服務器
2.時間複雜度: 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低,時間複雜度O(n)。網絡
改進了select最大數量限制。數據結構
函數格式以下所示:
# 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 可用內存不足,沒法完成請求。
1. 沒有最大鏈接數的限制。(基於鏈表來存儲的)
1. 時間複雜度: 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低,時間複雜度O(n)。
注意:select和poll都須要在返回後,經過遍歷文件描述符來獲取已經就緒的socket。事實上,同時鏈接的大量客戶端在一時刻可能只有不多的處於就緒狀態,所以隨着監視的描述符數量的增加,其效率也會線性降低。
epoll是在2.6內核中提出的,是以前的select和poll的加強版本。是爲處理大批量句柄而做了改進的poll。
epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的拷貝只須要一次。
epoll有兩大特色:
1. 邊緣觸發,它只告訴進程哪些fd剛剛變爲就緒態,而且只會通知一次。
2. 事件驅動,每一個事件關聯上fd,使用事件就緒通知方式,經過 epoll_ctl 註冊 fd,一旦該fd就緒,內核就會採用 callback 的回調機制來激活該fd,epoll_wait 即可以收到通知。
一棵紅黑樹,一張準備就緒句柄鏈表,少許的內核cache,就幫咱們解決了大併發下的socket處理問題。
1. 執行 epoll_create
內核在epoll文件系統中建了個file結點,(使用完,必須調用close()關閉,不然致使fd被耗盡)
在內核cache裏建了紅黑樹存儲epoll_ctl傳來的socket,
在內核cache裏建了rdllist雙向鏈表存儲準備就緒的事件。
2. 執行 epoll_ctl
若是增長socket句柄,檢查紅黑樹中是否存在,存在當即返回,不存在則添加到樹幹上,而後向內核註冊回調函數,告訴內核若是這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。
ps:全部添加到epoll中的事件都會與設備(如網卡)驅動程序簡歷回調關係,相應的事件發生時,會調用回調方法。
3. 執行 epoll_wait
馬上返回準備就緒表裏的數據便可(將內核cache裏雙向列表中存儲的準備就緒的事件 複製到用戶態內存)
當調用epoll_wait檢查是否有事件發生時,只須要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素便可。
若是rdlist不爲空,則把發生的事件複製到用戶態,同時將事件數量返回給用戶。
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);
(1) int epoll_create(int size);
/*建立一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。*/
這個參數不一樣於select()中的第一個參數,給出最大監聽的fd+1的值。
須要注意的是:
當建立好epoll句柄後,它就是會佔用一個fd值,在linux下若是查看/proc/進程id/fd/,是可以看到這個fd的,
因此在使用完epoll後,必須調用close()關閉,不然可能致使fd被耗盡。 (2)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隊列裏
(3) 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表示已超時。
1. 沒有最大鏈接數的限制。(基於 紅黑樹+雙鏈表 來存儲的:1G的內存上能監聽約10萬個端口)
2. 時間複雜度低: 邊緣觸發和事件驅動,監聽回調,時間複雜度O(1)。
只有活躍可用的fd纔會調用callback函數;即epoll最大的優勢就在於它只管「活躍」的鏈接,而跟鏈接總數無關,所以實際網絡環境中,Epoll的效率就會遠遠高於select和poll。
3. 內存拷貝:利用mmap()文件映射內存加速與內核空間的消息傳遞,減小拷貝開銷。
1. 依賴於操做系統:Lunix
適合用epoll的應用場景:
對於鏈接特別多,活躍的鏈接特別少
典型的應用場景爲一個須要處理上萬的鏈接服務器,例如各類app的入口服務器,例如qq
不適合epoll的場景:
鏈接比較少,數據量比較大,例如ssh
epoll 的驚羣問題:
由於epoll 多用於多個鏈接,只有少數活躍的場景,可是萬一某一時刻,epoll 等的上千個文件描述符都就緒了,這時候epoll 要進行大量的I/O,此時壓力太大。
epoll對文件描述符的操做有兩種模式:LT(level trigger) 和 ET(edge trigger)。LT是默認的模式,ET是「高速」模式。
LT(水平觸發)模式下,只要有數據就觸發,緩衝區剩餘未讀盡的數據會致使 epoll_wait都會返回它的事件;
ET(邊緣觸發)模式下,只有新數據到來才觸發,無論緩存區中是否還有數據,緩衝區剩餘未讀盡的數據不會致使epoll_wait返回。
LT(level triggered)是缺省的工做方式,而且同時支持block和no-block socket
。在這種作法中,內核告訴你一個文件描述符是否就緒了,而後你能夠對這個就緒的fd進行IO操做。
若是你不做任何操做,內核仍是會繼續通知你的,只要這個文件描述符還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操做
。
ET(edge-triggered)是高速工做方式,只支持no-block socket
。在這種模式下,當描述符從未就緒變爲就緒時,內核經過epoll告訴你。而後它會假設你知道文件描述符已經就緒,而且不會再爲那個文件描述符發送更多的就緒通知,直到你作了某些操做致使那個文件描述符再也不爲就緒狀態了(好比,你在發送,接收或者接收請求,或者發送接收的數據少於必定量時致使了一個EWOULDBLOCK 錯誤)。
可是請注意,若是一直不對這個fd做IO操做(從而致使它再次變成未就緒),內核不會發送更多的通知(only once)
。
ET模式在很大程度上減小了epoll事件被重複觸發的次數,所以效率要比LT模式高
。epoll工做在ET模式的時候,
必須使用非阻塞套接口
,以免因爲一個文件句柄的阻塞讀/阻塞寫操做把處理多個文件描述符的任務餓死。
注意:1. 在select/poll中,進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描
,而epoll事先經過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用相似callback的回調機制
,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。此處去掉了遍歷文件描述符,而是經過監聽回調的的機制。這正是epoll的魅力所在。
2. 若是沒有大量的idle-connection或者dead-connection,epoll的效率並不會比select/poll高不少,可是當遇到大量的idle-connection,就會發現epoll的效率大大高於select/poll。
它們三個都是 就緒設備 通知 。
一、支持一個進程所能打開的最大鏈接數
select |
單個進程所能打開的最大鏈接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是32*32,同理64位機器上FD_SETSIZE爲32*64),固然咱們能夠對進行修改,而後從新編譯內核,可是性能可能會受到影響,這須要進一步的測試。 |
poll |
poll本質上和select沒有區別,可是它沒有最大鏈接數的限制,緣由是它是基於鏈表來存儲的 |
epoll |
雖然鏈接數有上限,可是很大,1G內存的機器上能夠打開10萬左右的鏈接,2G內存的機器能夠打開20萬左右的鏈接 |
二、FD劇增後帶來的IO效率問題
select |
由於每次調用時都會對鏈接進行線性遍歷,因此隨着FD的增長會形成遍歷速度慢的「線性降低性能問題」。 |
poll |
同上 |
epoll |
由於epoll內核中實現是根據每一個fd上的callback函數來實現的,只有活躍的socket纔會主動調用callback,因此在活躍socket較少的狀況下,使用epoll沒有前面二者的線性降低的性能問題,可是全部socket都很活躍的狀況下,可能會有性能問題。 |
三、 消息傳遞方式
select |
內核須要將消息傳遞到用戶空間,都須要內核拷貝動做 |
poll |
同上 |
epoll |
epoll經過mmap把對應設備文件片段映射到用戶空間上, 消息傳遞不經過內核, 內存與設備文件同步數據. |
總結:
綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特色。
一、表面上看epoll的性能最好,可是在鏈接數少而且鏈接都十分活躍的狀況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制須要不少函數回調。
二、select低效是由於每次它都須要輪詢。但低效也是相對的,視狀況而定,也可經過良好的設計改善