上一篇介紹了select的基本用法,接著來學習一下poll和epoll的基本用法。首先來看poll:
#include <sys/poll.h> int poll (struct pollfd *fds, unsigned int nfds, int timeout);
poll() 採用了struct pollfd 結構陣列來儲存關心的檔案描述符,而不是像select一樣使用三個fd_set ,pollfd結構體定義如下:
struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ };
每一個pollfd結構體指定了一個被監視的檔案描述符,fds陣列中可以存放多個pollfd結構,而且數量不會像select的FD_SETSIZE一樣被限制在1024或者2048 。陣列中每個pollfd結構體的events域是監視該檔案描述符的事件掩碼,由使用者來設定這個域。revents域是檔案描述符的操作結果事件掩碼,系統呼叫返回時設定這個域。events域中請求的任何事件都可能在revents域中返回。我們可以設定如下事件:
POLLIN:有資料可讀。
POLLRDNORM:有普通資料可讀。
POLLRDBAND:有優先資料可讀。
POLLPRI:有緊迫資料可讀。
------------------------------------------------------------
POLLOUT:寫資料不會導致阻塞。
POLLWRNORM:寫普通資料不會導致阻塞。
POLLWRBAND:寫優先資料不會導致阻塞。
此外,revents域中還可能返回下列事件:
POLLERR:指定的檔案描述符發生錯誤。
POLLHUP:指定的檔案描述符掛起事件。
POLLNVAL:指定的檔案描述符非法。
注意:只能作為描述字的返回結果儲存在revents中,而不能作為測試條件用於events中。
其中POLLIN | POLLPRI等價於select()的讀事件,POLLOUT | POLLWRBAND等價於select()的寫事件。POLLIN等價於POLLRDNORM | POLLRDBAND,而POLLOUT則等價於POLLWRNORM。假如,要同時監視一個檔案描述符是否可讀和可寫,我們可以設定events為POLLIN | POLLOUT。在poll返回時,我們可以檢查revents中的標誌,對應於檔案描述符請求的events結構體。如果POLLIN事件被設定,則檔案描述符可以被讀取而不阻塞。如果POLLOUT被設定,則檔案描述符可以寫入而不導致阻塞。這些標誌並不是互斥的:它們可能被同時設定,表示這個檔案描述符的讀取和寫入操作都會正常返回而不阻塞。
timeout引數指定等待的毫秒數,無論I/O是否準備好,超時時間一到poll都會返回。timeout指定為負數值表示無限超時,UNPv1 中使用的INFTIM 巨集貌似現在已經廢棄,因此如果要設定無限等待,直接將timeout賦值為-1;timeout為0指示poll呼叫立即返回並列出準備好I/O的檔案描述符,但並不等待其它的事件。
成功時,poll()返回結構體中revents域不為0的檔案描述符個數;如果在超時前沒有任何事件發生,poll()返回0;失敗時,poll()返回-1。
//pollEcho.cpp #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <vector> #include <string.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <poll.h> #include <stropts.h> #include <netdb.h> #define PORT 1314 #define MAX_LINE_LEN 1024 int main() { struct sockaddr_in cli_addr, server_addr; socklen_t addr_len; int one,flags,nrcv,nwrite,nready; int listenfd,connfd; char buf[MAX_LINE_LEN],addr_str[INET_ADDRSTRLEN]; std::vector<struct pollfd> pollfdArray; struct pollfd pfd; bzero(&server_addr, sizeof server_addr); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); listenfd = socket(AF_INET, SOCK_STREAM, 0); if( listenfd < 0) { printf("listen error: %s \n", strerror(errno)); exit(1); } one = 1; setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR, &one, sizeof one); flags = fcntl(listenfd,F_GETFL,0); fcntl(listenfd, F_SETFL, flags | O_NONBLOCK); if(bind(listenfd,reinterpret_cast<struct sockaddr *>(&server_addr),sizeof(server_addr)) < 0) { printf("bind error: %s \n", strerror(errno)); exit(1); } listen(listenfd, 100); pfd.fd = listenfd; pfd.events = POLLIN; pollfdArray.push_back(pfd); while(1) { nready = poll(&(*pollfdArray.begin()), pollfdArray.size(), -1); if( nready < 0) { printf("poll error: %s \n", strerror(errno)); } if( pollfdArray[0].revents & POLLIN) { addr_len = sizeof cli_addr; connfd = accept(listenfd, reinterpret_cast<struct sockaddr *>(&cli_addr), &addr_len); if( connfd < 0) { if( errno != ECONNABORTED || errno != EWOULDBLOCK || errno != EINTR) { printf("accept error: %s \n", strerror(errno)); continue; } } printf("recieve from : %s at port %d\n", inet_ntop(AF_INET,&cli_addr.sin_addr,addr_str,INET_ADDRSTRLEN),cli_addr.sin_port); flags = fcntl(connfd, F_GETFL, 0); fcntl(connfd,F_SETFL, flags | O_NONBLOCK); bzero(&pfd, sizeof pfd); pfd.fd = connfd; pfd.events = POLLIN; pollfdArray.push_back(pfd); if(--nready < 0) { continue; } } for( unsigned int i = 1; i < pollfdArray.size(); i++) // i from 1 not 0 { pfd = pollfdArray[i]; if(pfd.revents & (POLLIN | POLLERR)) { memset(buf, 0, MAX_LINE_LEN); if( (nrcv = read(pfd.fd, buf, MAX_LINE_LEN)) < 0) { if(errno != EWOULDBLOCK || errno != EAGAIN || errno != EINTR) { printf("read error: %s\n",strerror(errno)); } } else if( 0 == nrcv) { close(pfd.fd); pollfdArray.erase(pollfdArray.begin() + i); } else { printf("nrcv: %s\n",buf); nwrite = write(pfd.fd, buf, nrcv); if( nwrite < 0) { if(errno != EAGAIN || errno != EWOULDBLOCK) printf("write error: %s\n",strerror(errno)); } printf("nwrite = %d\n",nwrite); } } } } return 0; }
以上程式碼操作的檔案描述符都設定成為了非阻塞的狀態,這也是為了更好的配合I/O multiplexing 的執行,試想如果read 或者 write 阻塞在某個描述符上,I/O multiplexing 就失去了真正的意義了,因為此時select/poll 函式就無法處理其它描述符產生的事件了。但是隻要設定為非阻塞就夠了嗎? 這顯然是還不夠的,後面會專門寫一篇文章對非阻塞的I/O multiplexing 進行完善。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
epoll是Linux下多路複用IO介面select/poll的增強版本,它能顯著減少程式在大量併發連線中只有少量活躍的情況下的系統CPU利用率,因為它不會複用檔案描述符集合來傳遞結果而迫使開發者每次等待事件之前都必須重新準備要被偵聽的檔案描述符集合,另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被核心IO事件非同步喚醒而加入Ready佇列的描述符集合就行了。
epoll的使用與select/poll不同,它是由一組系統呼叫組成:
#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);
第一個函式 epoll_create() 建立一個epoll的控制程式碼,size用來告訴核心這個監聽的數目一共有多大,其實size引數核心不會用到,只是開發者自己提醒自己的一個標記。epoll對監聽的描述符數目沒有限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關係很大,在我的機器上這個值為:149197.
第二個函式 epoll_ctl() 是epoll的事件註冊函式,它不同於select()是在監聽事件時告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別。第一個引數是epoll_create()的返回值,第二個參數列示動作,用三個巨集來表示:
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 */ };
其中epoll_data_t 結構如下:
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t;
注意這是一個union 結構。
events可以是以下幾個巨集的集合:
EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被結束通話;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡
這裡介紹一下邊沿觸發和水平觸發(epoll預設使用水平觸發):
LT 電平觸發(高電平觸發):
EPOLLIN 事件
核心中的某個socket接收緩衝區 為空 低電平
核心中的某個socket接收緩衝區 不為空 高電平
EPOLLOUT 事件
核心中的某個socket傳送緩衝區 不滿 高電平
核心中的某個socket傳送緩衝區 滿 低電平
ET 邊沿觸發:
低電平 -> 高電平 觸發
高電平 -> 低電平 觸發
推薦使用預設的水平觸發。
第三個函式epoll_wait() 等待事件的產生,類似於select()呼叫。引數events用來從核心得到事件的集合,返回的事件都儲存在該events陣列中,需要通過判斷該陣列中各個元素的狀態來決定該如何處理,該maxevents告之核心這個events陣列的大小。該函式返回需要處理的事件數目,如返回0表示已超時。
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <vector> #include <string.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <sys/epoll.h> using namespace std; #define PORT 1314 #define MAX_LINE_LEN 1024 #define EPOLL_EVENTS 1024 int main() { struct sockaddr_in cli_addr, server_addr; socklen_t addr_len; int one,flags,nrcv,nwrite,nready; int listenfd,epollfd,connfd; char buf[MAX_LINE_LEN],addr_str[INET_ADDRSTRLEN]; struct epoll_event ev; std::vector<struct epoll_event> eventsArray(16); bzero(&server_addr, sizeof server_addr); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); listenfd = socket(AF_INET, SOCK_STREAM, 0); if( listenfd < 0) { perror("socket open error! \n"); exit(1); } one = 1; setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR, &one, sizeof one); flags = fcntl(listenfd,F_GETFL,0); fcntl(listenfd, F_SETFL, flags | O_NONBLOCK); if(bind(listenfd,reinterpret_cast<struct sockaddr *>(&server_addr),sizeof(server_addr)) < 0) { perror("Bind error! \n"); exit(1); } listen(listenfd, 100); epollfd = epoll_create(EPOLL_EVENTS); if(epollfd < 0) { printf("epoll_create error: %s \n",strerror(errno)); exit(1); } ev.events = EPOLLIN; ev.data.fd = listenfd; if(epoll_ctl(epollfd, EPOLL_CTL_ADD,listenfd,&ev) < 0) { printf("register listenfd failed: %s",strerror(errno)); exit(1); } while(1) { nready = epoll_wait(epollfd,&(*eventsArray.begin()),static_cast<int>(eventsArray.size()),-1); if(nready < 0) { printf("epoll_wait error: %s \n",strerror(errno)); } for( int i = 0; i < nready; i++) { if(eventsArray[i].data.fd == listenfd) { addr_len = sizeof cli_addr; connfd = accept(listenfd, reinterpret_cast<struct sockaddr *>(&cli_addr),&addr_len); if( connfd < 0) { if( errno != ECONNABORTED || errno != EWOULDBLOCK || errno != EINTR) { printf("accept socket aborted: %s \n",strerror(errno)); continue; } } flags = fcntl(connfd, F_GETFL, 0); fcntl(connfd,F_SETFL, flags | O_NONBLOCK); ev.events = EPOLLIN; ev.data.fd = connfd; if(epoll_ctl(epollfd,EPOLL_CTL_ADD,connfd,&ev) < 0) { printf("epoll add error: %s",strerror(errno)); } printf("recieve from : %s at port %d\n", inet_ntop(AF_INET,&cli_addr.sin_addr,addr_str,INET_ADDRSTRLEN),cli_addr.sin_port); if(--nready < 0) { continue; } } else { ev = eventsArray[i]; printf("fd = %d \n",ev.data.fd); memset(buf,0,MAX_LINE_LEN); if( (nrcv = read(ev.data.fd, buf, MAX_LINE_LEN)) < 0) { if(errno != EWOULDBLOCK || errno != EAGAIN || errno != EINTR) { printf("read error: %s\n",strerror(errno)); } } else if( 0 == nrcv) { close(ev.data.fd); printf("close: %d fd \n",ev.data.fd); eventsArray.erase(eventsArray.begin() + i); } else { printf("nrcv, content: %s\n",buf); nwrite = write(ev.data.fd, buf, nrcv); if( nwrite < 0) { if(errno != EAGAIN || errno != EWOULDBLOCK) printf("write error: %s\n",strerror(errno)); } printf("nwrite = %d\n",nwrite); } } } } return 0; }
客戶端的測試程式碼還是可以用前一篇文章提到的:nc localhost 1314 的方式來測試。
I/O multiplexing 有三個方式可以完成,這三種方式的優劣和適用場合不同,後面會專門分析。