epoll select的限制 條件觸發 邊緣觸發
結論: epoll 要優於 select , 程式設計模型基本一致;
請注意,不論是epoll 還是 select 都不是具有併發(fork,pthread)能力的伺服器,僅僅是io複用
覺得理論麻煩的,可以直接往下拉,有程式碼例子;
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);
1. epoll的描述符限制可以檢視 cat /proc/sys/fs/epoll/max_user_watches
2. 複製資料也僅僅在 epoll_ctl (...,EPOLL_CTL_ADD,...) 新增時(EPOLL_CTL_ADD),複製一次到
epoll_create 所建立的紅黑樹中
*3. 最重要的是,epoll_wait 並不會像select 去輪詢, 而是在內部給監聽的描述符一個callback.
一旦對應的事件發生(例如: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也是條件觸發);
通過修改程式碼得結論
先新增一個BUFF_SIZE:
#define BUFF_SIZE 4
修改read 這行當代 :
把read(ep_events[i].data.fd,buf,BUFSIZ) 的BUFSIZ 修改成BUFF_SIZE
然後 telnet 此伺服器 : telnet 127.0.0.1 9988 , 隨意寫一些資料給伺服器
先看結果,這是我這裡的輸出:
[email protected]:~/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);
}