結論: epoll 要優於 select , 編程模型基本一致;編程
請注意,不管是epoll 仍是 select 都不是具備併發能力的服務器,僅僅是io複用
題外話: 在io複用中把監聽套接字設爲非阻塞ubuntu
以爲理論麻煩的,能夠直接往下拉,有代碼例子;segmentfault
select 的缺陷:服務器
1.在sys/select.h 中 __FD_SETSIZE 爲1024 , 意味默認狀況最多監控1024個描述符,固然能夠修改這個文件從新編譯 2.select 的實現 ,在fs/select.c 中:
int do_select(int n, fd_set_bits *fds, s64 *timeout) { //略 for (i = 0; i < n; ++rinp, ++routp, ++rexp) { //略 } }
在實現中 , 每次將通過 n-1 次循環, 隨着描述符越多,性能線性降低 3.select 的3個 fd_set(read,write,except) 都是值-結果: 例如:
fd_set rset,allset; while(1){ rset = allset; //每次須要重置 select(maxfd+1, rset, ....) }
這種每次重置意味不斷的從用戶空間往內核空間中複製數據, 操做系統一旦輪詢完成後,再將數據置位,而後複製到用戶空間, 就是這種不斷的複製形成了性能降低,還不得不這麼幹;
epoll 解決了select缺陷;
epoll 給每一個須要監聽的描述符都設置了一個或多個 event :併發
struct epoll_event event; event.events = EPOLLIN; //讀操做 ,如要監控多個能夠 EPOLLIN|EPOLLOUT; event.data.fd = listensock; //賦值要監聽的描述符, 就像 FD_SET(listensock,&rset);
*3. 最重要的是,epoll_wait 並不會像select 去輪詢, 而是在內部給監聽的描述符一個callback.socket
一旦對應的事件發生(例如:EPOLLIN) 就將此事件添加到一個鏈表中. epoll_wait(此函數相似select) 的功能就是去鏈表中收集發生事件相應的 struct epoll_event;
總的來講:
epoll_create 在操做系統中建立一個用於存放struct epoll_event 的空間
epoll_ctl 在空間內 添加,修改,刪除 struct epoll_event (內含描述符);
epoll_wait 收集已經發生事件的描述符;
close 關閉(減小引用) epoll函數
一個相似 select 的echo 服務器修改版本:性能
echo.c操作系統
#define EPOLL_SIZE 100 int listensock = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in serv_addr, cli_addr; socklen_t socklen = sizeof(serv_addr); memset(&serv_addr,0,socklen); memset(&cli_addr,0,socklen); serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(PORT); serv_addr.sin_family = AF_INET; if(bind(listensock,(SA*)&serv_addr,sizeof(serv_addr)) < 0){ perror("bind"); return 0; } if(listen(listensock,BACKLOG) < 0){ perror("listen"); return 0; } //向操做系統請求 建立一個 epoll , 參數看man int epfd = epoll_create(EPOLL_SIZE); if(epfd < 0){ perror("epoll_create"); return 0; } //監聽listensock ,相似 FD_SET(listensock, &rset) struct epoll_event event; event.events = EPOLLIN; //讀取 . man中有詳細解釋 event.data.fd = listensock; if(epoll_ctl(epfd,EPOLL_CTL_ADD,listensock,&event) < 0) //把listensock 註冊到epfd,用於監聽event事件 { perror("epoll ctl"); return 0; } //分配一塊內存. 在 epoll_wait 返回後將存放發生事件的結構體 struct epoll_event * ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); if(ep_events == NULL){ perror("malloc"); return 0; } puts("server is running"); int n = 0; int client_fd = 0; char buf[BUFSIZ]; int len = 0,return_bytes = 0, tmp_fd = 0; while(1){ //收集已經發生事件的描述符 , 返回值與select一致 n = epoll_wait(epfd,ep_events,EPOLL_SIZE,-1); if( n < 0){ perror("epoll_wait"); break; } printf("%d events returned\n", n); for ( int i = 0; i < n ; ++i){ //若是有人鏈接 if( listensock == ep_events[i].data.fd){ socklen = sizeof(cli_addr); client_fd = accept(listensock,(SA*)&cli_addr,&socklen); //若是有人鏈接,則加入epoll event.events = EPOLLIN; //讀取 event.data.fd = client_fd; if(epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&event) < 0){ perror("epoll add failed"); continue; } printf("accepted,client_fd:%d,ip:%s,port:%d\n", client_fd,inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port)); } else { tmp_fd =ep_events[i].data.fd; readagain: len = read(ep_events[i].data.fd,buf,BUFSIZ); //若是出錯 if(len < 0) { if(errno == EINTR) goto readagain; else{ return_bytes = snprintf(buf,BUFSIZ-1, "clientfd:%d errorno:%d\n",tmp_fd,errno); buf[return_bytes] = 0; epoll_ctl(epfd,EPOLL_CTL_DEL,tmp_fd,NULL); //從epoll中移除 close(tmp_fd); //close(socket) perror(buf); } } else if( 0 == len){ // 對端斷開 return_bytes = snprintf(buf,BUFSIZ-1, "clientfd:%d closed\n",tmp_fd); buf[return_bytes] = 0; epoll_ctl(epfd,EPOLL_CTL_DEL,tmp_fd,NULL); close(tmp_fd); puts(buf); } else{ //echo write(tmp_fd,buf,len); } } } }
條件觸發: epoll 的默認行爲就是條件觸發(select也是條件觸發);code
經過修改代碼得結論
先添加一個BUFF_SIZE
#define BUFF_SIZE 4
把read(ep_events[i].data.fd,buf,BUFSIZ) 的BUFSIZ 修改爲BUFF_SIZE
而後 telnet 此服務器 : telnet 127.0.0.1 9988 , 隨意寫一些數據給服務器
先看結果,這是我這裏的輸出:
fuck@ubuntu:~/sockettest$ ./epoll_serv server is running 1 events returned accepted,client_fd:5,ip:127.0.0.1,port:54404 1 events returned 1 events returned 1 events returned 1 events returned 1 events returned 1 events returned
讓read 最多隻能讀4個字節的惟一緣由是 ,證實什麼是條件觸發
經過結果獲得結論: 只要此緩衝區內還有數據, epoll_wait 將不斷的返回, 這個就是默認狀況下epoll的條件觸發;
邊緣觸發:這個須要先作個實驗才能理解,純文字估計不太好理解;
惟一須要修改的代碼是在accept下面的一行:
event.events = EPOLLIN | EPOLLET; // EPOLLET 就是邊緣觸發
注意 read 那行代碼,最多接受的字節最好在 1~4 之間 : read(...,..., 4);不然效果不明顯
接着telnet ,嘗試一下每次寫給服務器超過 5個字節
我這裏就不貼服務器輸出了 , 能夠看到這時 epoll_wait 不管怎麼樣只會返回一次了 ;
先給結論: 只有當客戶端寫入(write,send,...) , epoll_wait 纔會返回且只返回一次;
注意與條件觸發的不一樣: 條件觸發狀況下只要接受緩衝區有數據即返回, 邊緣觸發不會;
因爲這種只返回一次的特性 , EPOLLET 通常狀況下都將採用非阻塞 O_NONBLOCK 的方式來讀取;
下面修改上面代碼:
對於上面的代碼修改只修改幾個地方:
1.全部的套接字全改爲非阻塞;
static int setnonblock(int fd){ int flag = fcntl(fd,F_GETFL,0); return fcntl(fd,F_SETFL,flag|O_NONBLOCK); }
2.因爲套接字是非阻塞的,因此在 read 時須要無限循環, 以及判斷返回的錯誤,直到 errno == EAGAIN 才證實緩衝區已空;
echo.serv.c
static int setnonblock(int fd){ int flag = fcntl(fd,F_GETFL,0); return fcntl(fd,F_SETFL,flag|O_NONBLOCK); } int main(){ int listensock = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in serv_addr, cli_addr; socklen_t socklen = sizeof(serv_addr); memset(&serv_addr,0,socklen); memset(&cli_addr,0,socklen); serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(PORT); serv_addr.sin_family = AF_INET; if(bind(listensock,(SA*)&serv_addr,sizeof(serv_addr)) < 0){ perror("bind"); return 0; } if(listen(listensock,BACKLOG) < 0){ perror("listen"); return 0; } int epfd = epoll_create(EPOLL_SIZE); if(epfd < 0){ perror("epoll_create"); return 0; } setnonblock(listensock); struct epoll_event event; event.events = EPOLLIN; event.data.fd = listensock; if(epoll_ctl(epfd,EPOLL_CTL_ADD,listensock,&event) < 0) { perror("epoll ctl"); return 0; } struct epoll_event * ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); if(ep_events == NULL){ perror("malloc"); return 0; } puts("server is running"); int n = 0; int client_fd = 0; char buf[BUFSIZ]; int len = 0,return_bytes = 0, tmp_fd = 0; while(1){ n = epoll_wait(epfd,ep_events,EPOLL_SIZE,-1); if( n < 0){ perror("epoll_wait"); break; } printf("%d events returned\n", n); for ( int i = 0; i < n ; ++i){ if( listensock == ep_events[i].data.fd){ socklen = sizeof(cli_addr); client_fd = accept(listensock,(SA*)&cli_addr,&socklen); setnonblock(client_fd); //add in epoll event.events = EPOLLIN | EPOLLET; event.data.fd = client_fd; if(epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&event) < 0){ perror("epoll add failed"); continue; } printf("accepted,client_fd:%d,ip:%s,port:%d\n", client_fd,inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port)); } else { tmp_fd =ep_events[i].data.fd; while(1) { readagain: len = read(tmp_fd, buf, BUFF_SIZE); if (len < 0) { if (errno == EINTR) { puts("EINTR readagain"); goto readagain; } else if(errno == EAGAIN){ printf("no more data !! cliendfd:%d ,read return:%d\n",tmp_fd,len); break; } else { return_bytes = snprintf(buf, BUFSIZ - 1, "clientfd:%d errorno:%d \t", tmp_fd, errno); buf[return_bytes] = 0; epoll_ctl(epfd, EPOLL_CTL_DEL, tmp_fd, NULL); close(tmp_fd); perror(buf); break; } } else if (0 == len) { return_bytes = snprintf(buf, BUFSIZ - 1, "clientfd:%d closed\n", tmp_fd); buf[return_bytes] = 0; epoll_ctl(epfd, EPOLL_CTL_DEL, tmp_fd, NULL); close(tmp_fd); puts(buf); break; } else { return_bytes = write(tmp_fd, buf, len); printf("write %d bytes , readnbytes:%d , buf:%s\n", return_bytes, len,buf); } } } } } printf("serv down errno:%d\n",errno); close(epfd); close(listensock); }