IO复用解述
I/O多路復用
select
select 允許進程指示內核等待多個事件中的任何一個發生,并只在有一個或多個事件發生或指定時間后返回它。
select函數原型
#include <sys/select.h> #include <sys/time.h> int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);返回值:監聽到有事件發生的文件描述符的個數,超時為0,錯誤為?-1.
1.當監視的相應的文件描述符集中滿足條件時,比如說讀文件描述符集中有數據到來時,內核(I/O)根據狀態修改文件描述符集,并返回一個大于0的數。
2.當沒有滿足條件的文件描述符,且設置的timeval監控時間超時時,select函數會返回一個為0的值。
3.當select返回負值時,發生錯誤。
參數:
maxfd:是需要監視的最大的文件描述符值+1;
rdset、wrset、exset:是傳入傳出參數,fd_set類型,分別對應于需要檢測的可讀文件描述符的集合、可寫文件描述符的集合、異常文件描述符的集合。若對其中任何參數條件不感興趣,則可將其設為NULL。
timeout:設置超時時間,指定select在返回前沒有接收事件時應該等待的時間。
timeval 結構體
struct timeval{long tv_sec; // 秒long tv_usec; // 微秒 }描述符集合fd_set操作函數
系統提供了4個宏對描述符集進行操作:
#include <sys/select.h> #include <sys/time.h> void FD_SET(int fd, fd_set *fdset); // 設置文件描述符集fdset中對應于文件描述符fd的位(設置為1) void FD_CLR(int fd, fd_set *fdset); // 清除文件描述符集fdset中對應于文件描述符fd的位(設置為0) void FD_ISSET(int fd, fd_set *fdset); // 檢測文件描述符集fdset中對應于文件描述符fd的位是否被設置 void FD_ZERO(fd_set *fdset); // 清除文件描述符集fdset中的所有位(既把所有位都設置為0)理解select模型
理解select模型的關鍵在于理解fd_set,為說明方便,取fd_set長度為1字節,fd_set中的每一bit可以對應一個文件描述符fd。則1字節長的fd_set最大可以對應8個fd。
(1)執行fd_set set,FD_ZERO(&set),則set用位表示是0000,0000。
(2)若fd=5,執行FD_SET(fd,&set),后set變為0001,0000(第5位置為1)
(3)若再加入fd=2,fd=1,則set變為0001,0011
(4)執行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變為0000,0011。注意:沒有事件發生的fd=5被清空。
select模型的描述符集合,內部實現是位圖,這些參數指明了我們關心哪些描述符,和需要滿足什么條件(可寫,可讀,異常)。fd_set類型變量每一位代表了一個描述符。我們也可以認為它只是一個由很多二進制位構成的數組。
select的第一個參數是最大的描述符+1,表示描述符大小的范圍,此時僅將描述符當做一個數看待,它會遍歷從0到maxfd+1個位置
將感興趣的描述符加入對應的集合中,調用select,它會遍歷maxfd+1個描述符,如果有條件滿足,內核(I/O)根據狀態修改文件描述符集,并返回有事件發生的描述符的個數。此時描述符集合fd_set中的描述符被修改了,集合中都是有事件發生的。
select模型特點
基于上面的討論,可以輕松得出select模型的特點
(1)可監控的文件描述符個數取決與fd_set的值。一般為1024,每bit表示一個文件描述符,則支持的最大文件描述符是1024。可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低。
(2)每次調用 select(),都需要把描述符集合從用戶態拷貝到內核態,這個開銷在 fd 很多時會很大,同時每次調用 select() 都需要在內核中輪詢最大描述符數+1個描述符,這個開銷在 fd 很多時也很大。將文件描述符fd加入到事件集合中,還需要用一個數組,將文件描述符fd保存起來。一是,用于在select返回之后,fd_set參數中已經被修改為都是有事件發生的文件描述符位,這個數組中的文件描述符可以用FD_ISSET來輪詢對發生事件后的集合中的描述符判斷;二是,select返回后會把以前加入的但并無事件發生的fd的位清空,下一次開始 select 前要重新從數組中取得文件描述符逐個加入到 fd_set 中( FD_ZERO 最先),掃描數組的同時取得文件描述符的最大值 maxfd ,用于 select 的第一個參數。
(3)返回后的集合,需要輪詢數組中保存的描述符的每一個與集合中進行FD_ISSET操作,排查當文件描述符個數很多時,效率很低
poll
poll 和 select 系統調用的本質一樣,poll 的機制與 select 類似,與 select 在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理。
poll函數原型
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);返回值:
返回值 < 0,表示出錯 返回值 == 0,表示poll函數等待超時 返回值 > 0,表示poll由于監聽的文件描述符就緒返回,并且返回結果就是就緒的文件描述符的個數。參數:
fds:一個結構數組,struct pollfd結構如下:
struct pollfd{int fd; //要監聽的文件描述符short events; //需要監聽的事件(讀、寫、異常)short revents; //調用poll后的結果事件,內核在調用返回時設置這個事件 };events & revents的取值如下:
| POLLIN | 普通或優先級帶數據可讀 |
| POLLRDNORM | 普通數據可讀 |
| POLLRDBAND | 優先級帶數據可讀 |
| POLLPRI | 高優先級數據可讀 |
| POLLOUT | 普通數據可寫 |
| POLLWRNORM | 普通數據可寫 |
| POLLWRBAND | 優先級帶數據可寫 |
| POLLERR | 發生錯誤 |
| POLLHUP | 發生掛起 |
| POLLNVAL | 描述字不是一個打開的文件 |
?
nfds:要監視的描述符的數目。經過測試,如果監聽了兩個fd,但是nfds==1的情況下,只有fdarray0.fd能被監聽到。
timeout:用毫秒表示的時間,是指定poll在返回前沒有接收事件時應該等待的時間。
?
| -1 | 永遠等待 |
| 0 | 立即返回,不阻塞進程 |
| >0 | 等待指定數目的毫秒數 |
?
poll模型的特點
(1)poll沒有最大連接數的限制,原因是它是基于鏈表來存儲的。在select中,被監聽集合和返回集合是一個集合,在poll中將監聽和返回的事件都在結構體中不同的成員中,它們互補干擾,poll 中將有事件發生的文件描述符設置其結構體的revents,不需要向select一樣用一個數組存儲原來的文件描述符。
(2)poll函數中fds數組中元素是pollfd結構體,該結構體保存描述符的信息,每增加一個文件描述符就向數組中結構體加入一個描述符,結構體只需要拷貝一次到內核態。poll解決了select重復初始化的問題。但輪尋檢查事件發生的問題仍然未解決。
(3)與select一樣,poll返回后,需要輪詢每個pollfd結構體的revents來獲取就緒的描述符,這樣會使性能下降?,poll會遍歷到數組已使用的最大下標,如果同時連接的大量客戶端在一時刻可能只有很少的就緒狀態,就是最大下標很大,而只有幾個描述符發生事件,因此隨著監視的描述符數量的增長,其效率也會線性下降。
?
epoll
epoll 是之前的 select 和 poll 的增強版本。相對于 select 和 poll 來說,epoll更加靈活,沒有描述符限制。epoll使用一個epoll句柄管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。
epoll相關函數
int epoll_create(int size); //創建內核事件表創建一個epoll文件描述符,相當于一個句柄,size用來告訴內核這個監聽的數目一共有多大。這個參數不同于select()中的第一個參數。需要注意的是,當創建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。
返回值:返回一個文件描述符fd,可以理解為指向內核中的一顆紅黑樹的樹根,size就是創建紅黑樹的大小。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注冊函數,它在這里注冊要監聽的事件類型。參數?epfd是epoll_create()的返回值的描述符;參數?op?表示動作,用三個宏來表示,控制某個epoll監聽的文件描述符上的事件:添加、修改、刪除。相當于在紅黑樹上操作。參數?fd?是需要監聽事件的文件描述符,參數?event?是告訴內核需要監聽什么事件。
返回值:成功返回0,不成功返回1
事件注冊函數的第二個參數的動作,有三個宏表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中 EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件 EPOLL_CTL_DEL:從epfd中刪除一個fdstruct epoll_event結構如下:
struct epoll_event {__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */ }; typedef union epoll_data {void *ptr;int fd;__uint32_t u32;__uint64_t u64; } epoll_data_t;events可以是以下幾個宏的集合:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉); EPOLLOUT:表示對應的文件描述符可以寫; EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來); EPOLLERR:表示對應的文件描述符發生錯誤; EPOLLHUP:表示對應的文件描述符被掛斷; EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相`對于水平觸發(Level Triggered)來說的。 EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里. #include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)參數?fd?是epoll_create返回的文件描述符;參數?events?是一個數組,傳入傳出參數;參數?maxevents?告之內核這個events數組有多大(數組成員的個數),這個 maxevents 的值不能大于創建epoll_create()時的size;參數?timeout?設置超時時間(-1 阻塞,0 立即返回,非阻塞,>0 指定毫秒)。
返回值:?成功返回有多少文件描述符就緒,時間到時返回0,出錯返回-1。返回的有事件發生的描述符都在 events 數組中,數組中實際存放的成員個數是函數的返回值個。
epoll模型的特點
(1)本身沒有最大并發連接的限制,僅受系統中進程能打開的最大文件數目限制。
(2)基于事件就緒通知方式:一旦被監聽的某個文件描述符就緒,內核會采用類似于callback的回調機制,迅速激活這個文件描述符,這樣隨著文件描述符數量的增加,也不會影響判定就緒的性能。不會像select/poll中輪詢檢測每個描述符是否就緒。
(3)當文件描述符就緒,就會被放到一個數組中,這樣調用epoll_weit獲取就緒文件描述符的時候,只要取數組中的返回的個數個元素即可,不需要全部做輪詢檢測。
(4)內存拷貝是利用mmap()文件映射內存的方式加速與內核空間的消息傳遞,減少復制開銷。(內核與用戶空間共享一塊內存)
epoll工作模式
epoll對文件描述符的操作有兩種模式:水平觸發LT(level trigger)和邊緣觸發ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別如下:
LT模式:當epoll_wait檢測到描述符事件發生并將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序并通知此事件。 ET模式:當epoll_wait檢測到描述符事件發生并將此事件通知應用程序,應用程序必須立即處理該事件。
如果不處理,下次調用epoll_wait時,不會再次響應應用程序并通知此事件。
ET模式在很大程度上減少了epoll事件被重復觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
二者的主要差異在于level-trigger模式下只要某個socket處于readable/writable狀態,無論什么時候進行epoll_wait都會返回該socket;而edge-trigger模式下只有某個socket從unreadable變為readable或從unwritable變為writable時,epoll_wait才會返回該socket。
描述符就緒條件
?可讀條件
(1) “監聽socket”:該套接字是一個監聽套接字且已完成的連接數不為0。而這樣的套接字處于可讀狀態,是因為套接字收到了對方的connect請求,執行了三次握手的第一步:對方發送SYN請求過來,使該方監聽套接字處于可讀狀態;通常情況下,對這樣的套接字執行accept操作不會阻塞;
(2)“已連接socket”:該套接字的接收緩沖區中的數據字節大于等于該套接字的接收緩沖區低水位標記的當前大小。對這樣的套接字執行讀操作不會阻塞并返回一個大于0的值(也就是返回準備好讀入的數據)。可以用SO_RCVLOWAT套接字選項設置該套接字的低水位標記。對于TCP和UDP套接字而言,其缺省值為1,這意味著,默認情況下,只要緩沖區中有數據,那就是可讀的。
(3)“已連接socket”:該連接的讀半部關閉(也就是接收了FIN的TCP連接)。對這樣的套接字的讀操作將不阻塞并返回0(也就是返回EOF),此時必須且一直會返回0;
(4)“已連接socket”:其上有一個套接字錯誤待處理。對這樣的套接字的讀操作將不會阻塞并返回-1(即返回一個錯誤),同時把errno設置成確切的錯誤條件。這些待處理錯誤(pending error)也可通過指定SO_ERROR套接字選項調用getsockopt獲取并清除。
可寫條件
(1)“已連接socket/UDP socket”:該套接字發送緩沖區中的可用空間字節數大于等于該套接字的發送緩沖區低水位標記的當前大小(對于TCP的已連接socket或者UDP socket均可)。對這樣的套接字的寫操作將不阻塞并返回一個大于0的值(也就是返回準備好寫入的數據)。可以用SO_SNDLOWAT套接字選項設置該套接字的低水位標記。對于TCP和UDP套接字而言,低水位默認值為2048,發送緩沖區默認大小為8K,這意味著,默認情況下,一個套接字連接成功后,總是可寫的;
(2)“已連接socket”:該連接的寫半部關閉(主動發送了FIN的TCP連接)。對這樣的套接字的寫操作將產生SIGPIPE信號,該信號的缺省行為是終止進程;
(3)“已連接socket”:其上有一個套接字錯誤待處理。對這樣的套接字的寫操作將不會阻塞并且返回-1(即返回一個錯誤),同時把errno設置成確切的錯誤條件。這些待處理的錯誤也可以通過指定SO_ERROR套接字選項調用getsockopt函數來取得并清除;
(4)使用非阻塞式connect的套接字已建立連接,或者connect已經以失敗告終,即connect已經完成。
異常條件
該套接字存在帶外數據或者仍處于帶外標記
?
參考鏈接:https://www.cnblogs.com/WindSun/p/11491395.html
轉載于:https://www.cnblogs.com/single-dont/p/11520478.html
總結
- 上一篇: 大型互联网公司分布式ID方案总结
- 下一篇: 1、Tensorflow 之 saver