面試的時候常常被問的一個很蛋疼的問題,常常被問,可是知識很零散,難記憶,看完就忘php
能夠監視文件描述符是否能夠讀寫,要求監視的文件描述符是非阻塞的html
產生與上個世紀80年代的UNIX系統,到1993年寫入POSIX1.b規範(一個操做系統的編程接口的規範,你要是寫個操做系統想被兼容得遵照這個規範)。因爲那個年代尚未多線程(2年後線程相關的內容才寫入POSIX1.c規範),尚未什麼C10K問題,因此在設計select的時候體現了那個年代的特色。linux
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds 是參數2,3,4中最大的文件描述符 + 1nginx
readfds 是要檢測fd的可讀事件,當fd可讀的時候select就返回面試
writefds 是要檢測fd的寫事件,當fd可寫的時候select就返回編程
excpetfds 是要檢測fd的出錯事件,當fd出錯的時候select就返回,當是NULL的時候,就是檢測readfds和writefds的出錯事件,因此通常都寫NULLwindows
timeout 是一個納秒級的超時時間api
想使用這個函數須要設置個fd_set結構,因此就用到void FD_SET(int fd, fd_set *set);這個宏,用來設置fd_set數組
fd_set fd_in, fd_out; struct timeval tv; // 初始化fd_set FD_ZERO( &fd_in ); FD_ZERO( &fd_out ); // 把網絡IO複製到fd_set FD_SET( sock1, &fd_in ); FD_SET( sock1, &fd_out ); // 用戶初始化select int largest_sock = sock1 > sock2 ? sock1 : sock2; // 設置select的超時時間 tv.tv_sec = 10; tv.tv_usec = 0; // 調用select 並阻塞在這裏等待 IO事件 int ret = select( largest_sock, &fd_in, &fd_out, NULL, &tv ); // 檢查返回值的狀態 if ( ret == -1 ) // 異常狀況 else if ( ret == 0 ) // 超時或者沒有能夠監控的fd else { // 檢測每一個IO事件是否能夠讀寫 if ( FD_ISSET( sock1, &fd_in ) ) // IO可讀 if ( FD_ISSET( sock2, &fd_out ) ) // IO可寫 }
能夠看到使用select的時候,每一個fd對應一個fd_set結構,而後調用FD_SET,調用select之後進入polling,等返回之後經過FD_ISSET對每一個fd_set檢測是否可讀可寫。服務器
是否是會兒尚未如今nginx幾萬併發的場景,select只能對1024個fd進行監控
select 函數會修改fd_set,因此每次調用完select之後須要從新經過FD_SET設置fd_set
select 返回之後並不知道具體哪一個fd能夠讀寫,須要使用FD_ISSET把全部的fd檢測一遍才知道具體是哪一個可讀可寫
那年代估計不像如今這麼普遍的用多線程,因此select中的fd_set在調用select的時候至關於被獨佔的
使用簡單,POSIX標準因此跨平臺比較好
功能和select相同,可是主要解決select的一些限制
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
fds 是一個pollfd的數組,和select的fd_set差很少,下面具體解釋
nfds 是fds數組的長度,能夠看到沒有select還須要求一個fd最大值再加1那麼麻煩
timeout 是超時的毫秒數
pollfd的結構
struct pollfd { int fd; /* 文件描述符 */ short events; /* 須要監聽的事件 */ short revents; /* 返回的事件 */ };
對比一下select,接口上更加優雅,首先,pollfd 經過單獨的events來區分了監控的是什麼樣的事件,而不是像select那樣經過參數來區分。
// 建立pollfd struct pollfd fds[2]; // 設置pollfd的fd和要監控的事件,sock1監控讀,sock2監控寫 fds[0].fd = sock1; fds[0].events = POLLIN; fds[1].fd = sock2; fds[1].events = POLLOUT; // 10秒超時,開始等待sock1和sock2上的事件 int ret = poll( &fds, 2, 10000 ); // 有事件返回 if ( ret == -1 ) // 出錯了 else if ( ret == 0 ) // 超時 else { // 對每一個pollfd檢測是否有就緒的事件 if ( pfd[0].revents & POLLIN ) pfd[0].revents = 0; // 可讀 if ( pfd[1].revents & POLLOUT ) pfd[1].revents = 0; // 可寫 }
與select相同,都是建立結構,設置,開始polling,逐個檢測事件
對於能夠監控fd的數量沒有限制,而不是像select那樣最大才1024個
每次poll以後不須要從新設置pollfd,而不像fd_set須要從新設置
vista以前的windows上沒有poll
#if defined (WIN32) static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); } #endif
linux平臺上最新的polling技術,出現與linux2.6版本,linux2.6發佈是在2003年(竟然epoll出現已經12年了)。
int epoll_create(int size);
用於建立一個size大小的epoll,返回一個epfd的描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
修改某個文件描述符的狀態
epfd 是建立的epoll
op 是修改的操做類型,能夠是EPOLL_CTL_ADD 或者 EPOLL_CTL_DEL,表明添加和刪除
fd 是要操做的文件描述符
event 是文件描述符fd上掛的一個context,是一個epoll_event結構體,下面是epoll_event的結構體內容:
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
其中epoll_event.events 和 pollfd中的events 差很少,不過事件更加豐富,data是對於文件描述符上能夠掛的衛星數據,也更加靈活。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epdf 是epoll_create時候返回的
events 是polling 返回的結果會賦值在events
maxevents 是一次通知用戶最大的events數量,通常就是events的數組長度
timeout 是超時時間
相比select/poll,epoll_wait 只返回可讀寫的事件,而不是所有返回
關於幾個size的理解
epoll_create 時候的size是指 epoll在覈心態監控fd的最大數量
epoll_wait 時候的events 是指一次通知的數據,這個數量認爲是一次批量,確定是小於等於epoll_create時候的大小,比這個再大也沒用了
epoll_wait 時候的maxevents 是爲了防止一次通知events溢出的一個邊界,若是設置的比events的數組長度小,那就至關於批量變小,比這個大會溢出,因此應該是相等就能夠了
// 建立 int pollingfd = epoll_create( 0xCAFE ); // 建立一個epoll_event 用來一下子epoll_ctl的時候EPOLL_CTL_ADD用 struct epoll_event ev = { 0 }; // 假設sock1是個網絡鏈接 int sock1 = pConnection1->getSocket(); // 給這個sock1掛一點衛星數據,這裏能夠是任意的東西,咱們就放個他的connection ev.data.ptr = pConnection1; // 來監控sock1的可讀事件 ev.events = EPOLLIN | EPOLLONESHOT; // 把設置好的epoll_event 添加到建立的epollfd if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, sock1, &ev ) != 0 ) // 出錯 // 建立一些epoll_event用來在用戶態來接收 struct epoll_event pevents[ 20 ]; // 等待可讀事件 int ready = epoll_wait( pollingfd, pevents, 20, 10000 ); if ( ret == -1 ) // 出錯 else if ( ret == 0 ) // 超時 else { // ret是返回了多少個能夠讀寫的事件 for ( int i = 0; i < ret; i++ ) { // 判斷通知到用戶這個究竟是個什麼事件 if ( pevents[i].events & EPOLLIN ) { // 取到當初咱們掛在上面的衛星數據 Connection * c = (Connection*) pevents[i].data.ptr; // 對這個socket進行一些操做 c->handleReadEvent(); } } }
epoll的使用過程仍是比select和poll複雜很多的,首先你得建立一個epoll,而後建立和設置epoll_event,再經過epoll_ctl添加到epoll,最後epoll_wait,遍歷通知過來的events
最大的改進就是不須要在遍歷全部事件了,不須要FD_ISSET,也不須要遍歷全部pollfd.revents,取而代之的是,內核幫咱們把active的fd賦值到epoll_wait的events上
pollfd封裝了一個event,而不是像select的fd_set只有一個fd屬性,epoll_event 比pollfd又多了一個data的衛星數據,能夠聽任意的東西上去
select和poll一旦進入polling階段,就無法對fd作修改了,可是epoll_ctl能夠在任意的線程裏在任意事件動態的添加,刪除epoll_event
改變epoll中fd的監聽事件類型須要epoll_ctl的系統調用,而在poll中只須要在用戶態作BITMASK
只能在linux上用,雖然有libevent這種東西
epoll的api比select和poll複雜
若是鏈接數很低小於1024,epoll對比select和poll是沒有性能提高的,選擇select仍是poll就看我的喜愛了,通常select就行,好比fpm,epoll早就出了fpm也沒改,PHP不多有人能worker開1000以上
若是鏈接是短鏈接,常常accept出一些fd添加到epoll中的系統調用開銷須要考慮,具體性能還須要再綜合考慮,好比nginx,雖然都是短鏈接,可是有高併發,幾萬併發select每次遍歷一遍全部fd更耗
若是是長鏈接,而且都是idle的,例如一些聊天的服務器,一個鏈接,半天才說一句話,都是掛機的,可是鏈接幾十萬,那有我的說句話,服務區須要讀,你遍歷幾十萬個fd就不值了
若是你的應用是多線程來處理網絡的,那麼爲了利用多線程仍是使用epoll比較好,能夠用多線程配合邊緣觸發(若是可讀只通知一次,無論讀完沒讀完,水平觸發沒讀完就一直通知,因此效率會比邊緣觸發低一些),這也是邊緣觸發推薦的使用方式。
簡單來講是這樣的select和poll當檢測到fd就緒之後,就通知到用戶態了,函數也就返回了。而epoll在add的時候就開始監聽,發現他就緒之後就放到一個就緒表裏,epoll_wait只是定時查看一下這個就緒表裏的數據。
參考文章
https://cs.uwaterloo.ca/~brecht/papers/getpaper.php?file=ols-2004.pdf
http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/
http://www.unix.org/what_is_unix/history_timeline.html
http://en.wikipedia.org/wiki/Asynchronous_I/O