IO多路复用 select、poll、epoll
什么是IO多路復用
在同一個線程里面,?通過撥開關的方式,來同時傳輸多個(socket)I/O流。
在英文中叫I/O multiplexing。這里面的 multiplexing 指的其實是在單個線程通過記錄跟蹤每一個Sock(I/O流)的狀態來同時管理多個I/O流. 發明它的原因,是盡量多的提高服務器的吞吐能力。
什么,你還沒有搞懂“一個請求到來了,nginx使用epoll接收請求的過程是怎樣的”, 多看看這個圖就了解了。提醒下,ngnix會有很多鏈接進來, epoll會把他們都監視起來,然后像撥開關一樣,誰有數據就撥向誰,然后調用相應的代碼處理。
?
處理多路IO的兩種方法
不復用同一個線程:每進來一個新的I/O流會分配一個新的進程管理。這是多路IO不復用同一個線程。復用同一個線程:單個線程,通過記錄跟蹤每個I/O流(sock)的狀態,來同時管理多個I/O流 。也就是IO多路復用。
要弄清問題 先要知道問題的出現原因
由于進程的執行過程是線性的(也就是順序執行),當我們調用低速系統I/O(read,write,accept等等),進程可能阻塞,此時進程就阻塞在這個調用上,不能執行其他操作.阻塞很正常. 接下來考慮這么一個問題一個服務器進程和一個客戶端進程通信,服務器端read(sockfd1,bud,bufsize),此時客戶端進程沒有發送數據,那么read(阻塞調用)將阻塞直到客戶端調用write(sockfd,but,size)發來數據. 在一個客戶和服務器通信時這沒什么問題,當多個客戶與服務器通信時,若服務器阻塞于其中一個客戶sockfd1,當另一個客戶的數據到達套接字sockfd2時,服務器不能處理,仍然阻塞在read(sockfd1,...)上;此時問題就現了,不能及時處理另一個客戶的服務,咋么辦?I/O多路復用來解決!
I/O多路復用:
繼續上面的問題,有多個客戶連接,sockfd1,sockfd2,sockfd3..sockfdn同時監聽這n個客戶,當其中有一個發來消息時就從select的阻塞中返回,然后就調用read讀取收到消息的sockfd,然后又循環select阻塞;這樣就不會因為阻塞在其中一個上而不能處理另一個客戶的消息。
Q:
那這樣子,在讀取socket1的數據時,如果其它socket有數據來,那么也要等到socket1讀取完了才能繼續讀取其它socket的數據吧。那不是也阻塞住了嗎?而且讀取到的數據也要開啟線程處理吧,那這和多線程IO有什么區別呢?
1.CPU本來就是線性的 不論什么都需要順序處理 并行只能是多核CPU
2.io多路復用本來就是用來解決對多個I/O監聽時,一個I/O阻塞影響其他I/O的問題,跟多線程沒關系.
3.跟多線程相比較,線程切換需要切換到內核進行線程切換,需要消耗時間和資源. 而I/O多路復用不需要切換線/進程,效率相對較高,特別是對高并發的應用nginx就是用I/O多路復用,故而性能極佳.但多線程編程邏輯和處理上比I/O多路復用簡單.而I/O多路復用處理起來較為復雜.
?
IO多路復用的幾種實現
常見的幾種IO多路復用的實現有select、poll、epoll
select
時間復雜度O(n),它僅僅知道了,有I/O事件發生了,卻并不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。所以select具有O(n)的無差別輪詢復雜度,同時處理的流越多,無差別輪詢時間就越長。
函數介紹
該函數準許進程指示內核等待多個事件中的任何一個發送,并只在有一個或多個事件發生或經歷一段指定的時間后才喚醒。函數原型如下:
#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。
poll
時間復雜度O(n),poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態,?但是它沒有最大連接數的限制,原因是它是基于鏈表來存儲的。
函數介紹
poll的機制與select類似,與select在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大文件描述符數量的限制。poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體復制于用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數量的增加而線性增大。
# include <poll.h> int poll ( struct pollfd * fds, unsigned int nfds, int timeout);函數參數介紹如下:
pollfd結構體定義如下:
struct?pollfd?{
int?fd;?????????/*?文件描述符?*/
short?events;?????????/*?等待的事件?*/
short?revents;???????/*?實際發生了的事件?*/
}?;?
每一個pollfd結構體指定了一個被監視的文件描述符,可以傳遞多個結構體,指示poll()監視多個文件描述符。每個結構體的events域是監視該文件描述符的事件掩碼,由用戶來設置這個域。revents域是文件描述符的操作結果事件掩碼,內核在調用返回時設置這個域。events域中請求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN? 有數據可讀。
POLLRDNORM? ?有普通數據可讀。
POLLRDBAND ?有優先數據可讀。
POLLPRI ?有緊迫數據可讀。
POLLOUT ? ? ?寫數據不會導致阻塞。
POLLWRNORM ?寫普通數據不會導致阻塞。
POLLWRBAND ??寫優先數據不會導致阻塞。
POLLMSGSIGPOLL? 消息可用。
此外,revents域中還可能返回下列事件:
POLLER ??指定的文件描述符發生錯誤。
POLLHUP ?指定的文件描述符掛起事件。
POLLNVAL 指定的文件描述符非法。
這些事件在events域中無意義,因為它們在合適的時候總是會從revents中返回。
使用poll()和select()不一樣,你不需要顯式地請求異常情況報告。
POLLIN?|?POLLPRI等價于select()的讀事件,POLLOUT?|POLLWRBAND等價于select()的寫事件。POLLIN等價于POLLRDNORM?|POLLRDBAND,而POLLOUT則等價于POLLWRNORM。例如,要同時監視一個文件描述符是否可讀和可寫,我們可以設置?events為POLLIN?|POLLOUT。在poll返回時,我們可以檢查revents中的標志,對應于文件描述符請求的events結構體。如果POLLIN事件被設置,則文件描述符可以被讀取而不阻塞。如果POLLOUT被設置,則文件描述符可以寫入而不導致阻塞。這些標志并不是互斥的:它們可能被同時設置,表示這個文件描述符的讀取和寫入操作都會正常返回而不阻塞。
timeout參數指定等待的毫秒數,無論I/O是否準備好,poll都會返回。timeout指定為負數值表示無限超時,使poll()一直掛起直到一個指定事件發生;timeout為0指示poll調用立即返回并列出準備好I/O的文件描述符,但并不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即返回。
返回值和錯誤代碼
成功時,poll()返回結構體中revents域不為0的文件描述符個數;如果在超時前沒有任何事件發生,poll()返回0;失敗時,poll()返回-1,并設置errno為下列值之一:
EBADF ? ? ??一個或多個結構體中指定的文件描述符無效。
EFAULTfds ?指針指向的地址超出進程的地址空間。
EINTR ?請求的事件之前產生一個信號,調用可以重新發起。
EINVALnfds 參數超出PLIMIT_NOFILE值。
ENOMEM ? ??可用內存不足,無法完成請求。
?
epoll
時間復雜度O(1),epoll可以理解為event poll,不同于忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(復雜度降低到了O(1))。
函數介紹
epoll是在2.6內核中提出的,是之前的select和poll的增強版本。相對于select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關系的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。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的事件注冊函數,它不同與select()是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。第一個參數是epoll_create()的返回值,第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是需要監聽的fd,第四個參數是告訴內核需要監聽什么事,struct epoll_event結構如下:
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表示已超時。
工作模式
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別如下:
LT模式:只要這個fd還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作,而在ET(邊緣觸發)模式中,它只會提示一次,直到下次再有數據流入之前都不會再提示了,無 論fd中是否還有數據可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀光,也就是說一直讀到read的返回值小于請求值,或者 遇到EAGAIN錯誤。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內核就會采用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。
ET模式:如果采用EPOLLLT模式的話,系統中一旦有大量你不需要讀寫的就緒文件描述符,它們每次調用epoll_wait都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率.。而采用EPOLLET這種邊沿觸發模式的話,當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩沖區太小),那么下次調用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你!
ET模式比水LT率高,系統不會充斥大量你不關心的就緒文件描述符,在很大程度上減少了epoll事件被重復觸發的次數。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
?
區別于聯系
select,poll,epoll都是IO多路復用的機制。I/O多路復用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。??epoll跟select都能提供多路I/O復用的解決方案。在現在的Linux內核里有都能夠支持,其中epoll是Linux所特有,而select則應該是POSIX所規定,一般操作系統均有實現。
select單個進程可監視的fd數量被限制,即能監聽端口的大小有限,32位機默認是1024個。64位機默認是2048。需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時復制開銷大,而且傳遞比較頻繁。
poll對比select它沒有最大連接數的限制,其他select有的缺點poll都有。
epoll沒有最大并發連接的限制,能打開的FD的上限遠大于1024(1G的內存上能監聽約10萬個端口)。效率提升,不是輪詢的方式,不會隨著FD數目的增加效率下降。只有活躍可用的FD才會調用callback函數;即Epoll最大的優點就在于它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環境中,Epoll的效率就會遠遠高于select和poll。內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減少復制開銷。
總結:
綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。
1、表面上看epoll的性能最好,但是在連接數少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。
2、select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善
?
?
參考文獻
https://www.cnblogs.com/wajika/p/6581104.html(IO多路復用是什么意思)
https://www.cnblogs.com/aspirant/p/9166944.html(select、poll、epoll)
?
轉載于:https://www.cnblogs.com/gauze/p/10572652.html
總結
以上是生活随笔為你收集整理的IO多路复用 select、poll、epoll的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 京东AI研究院何晓冬:将先进的技术和模型
- 下一篇: python 网络爬虫介绍