poll
poll函數接口
#include <poll.h>
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
//參數1 結構體指針(結構體數組的首地址)
//參數2 數組長度
//參數3 poll函數的超時時間(同select)
//返回值小於0,出錯,等於0,poll函數等待超時
//大於0,表示poll由於監聽的文件描述符就緒返回。
struct pollfd{
int fd;//文件描述符
short events;//輸入參數 監聽的事件集合(常用事件 POLLIN POLLOUT)
short revents;//輸出參數 返回的事件集合
};
poll的優點(同select比較)
不同與select使⽤三個位圖來表⽰三個fdset的⽅式,poll使⽤⼀個pollfd的指針實現
- pollfd結構包含了要監視的event和發⽣的event,不再使⽤select“參數-值”傳遞的⽅式. 接⼝使⽤⽐select更⽅便.
- poll並沒有最⼤數量限制 (但是數量過⼤後性能也是會下降)
poll的缺點
- 和select函數⼀樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符.
- 每次調⽤poll都需要把⼤量的pollfd結構從⽤戶態拷⻉到內核中.
- 同時連接的⼤量客戶端在⼀時刻可能只有很少的處於就緒狀態, 因此隨着監視的描述符數量的增⻓,其效率也會線性下降
//使用epoll監控標準輸入
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
int main(){
struct pollfd fds;
fds.fd = 0;//0標準輸入
fds.events = POLLIN;
while(1){
int ret = poll(&fds,1,-1);//-1 永久阻塞
if(ret<0){
perror("poll");
return 1;
}
char buf[1024] = {0};
ssize_t read_size = read(0,buf,sizeof(buf)-1);
if(read_size < 0){
perror("read");
return 1;
}
if(read_size == 0){
printf("read done\n");
return 0;
}
buf[read_size] = '\0';
printf("rep = %s\n",buf);
}
return 0;
}
epoll
epoll的相關函數
創建一個epoll的句柄(驗證其爲文件描述符的方式:連接服務器時,第一個client爲5,不是4),既然是文件描述符,用完要關閉。
int epoll_create(int size);
//size爲任意值,無意義
epoll的事件註冊函數
int epoll_ctl (int epfd,int op,int fd,struct epoll_event *event);
//參數1 epoll_create() 的返回值(句柄)
//參數2 表示處理事件的方式(3種)
//參數3 需要監聽的文件描述符
//參數4結構體指針(操作方式)
參數2的三種方法
- EPOLL_CTL_ADD 註冊新的fd到epfd;
- EPOLL_CTL_MOD 修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL 從epfd中刪除一個fd;
struct epoll_event
epoll_event結構體裏邊有兩個參數,一個events(相當於位圖)其常用操作有EPOLLIN,EPOLLOUT。第二個參數爲一個聯合,其能更好的操作一個未知甚至更復雜的結構(含有一個void*的指針)。
events常見操作:
- EPOLLIN : 表⽰對應的⽂件描述符可以讀 (包括對端SOCKET正常關閉);
- EPOLLOUT : 表⽰對應的⽂件描述符可以寫;
- EPOLLPRI : 表⽰對應的⽂件描述符有緊急的數據可讀 (這⾥應該表⽰有帶外數據到來);
- EPOLLERR : 表⽰對應的⽂件描述符發⽣錯誤;
- EPOLLHUP : 表⽰對應的⽂件描述符被掛斷;
- EPOLLET : 將EPOLL設爲邊緣觸發(Edge Triggered)模式, 這是相對於⽔平觸發(Level Triggered)來說的.(默認爲LT模式1)
- EPOLLONESHOT:只監聽⼀次事件, 當監聽完這次事件之後, 如果還需要繼續監聽這個socket的話, 需要再次把這個socket加⼊到EPOLL隊列⾥
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);
//參數1 epoll句柄
//參數2 結構體數組首地址(同epoll_ctl的 *event)
//參數3 數組的大小
//參數4 超時時間
參數events是分配好的epoll_event結構體數組.
epoll將會把發⽣的事件賦值到events數組中 (events不可以是空指針,內核只負責把數據複製到這個events數組中,不會去幫助我們在⽤戶態中分配內存).
maxevents告之內核這個events有多⼤,這個 maxevents的值不能⼤於創建epoll_create()時的size.
參數timeout是超時時間 (毫秒,0會⽴即返回,-1是永久阻塞).
如果函數調⽤成功,返回對應I/O上已準備好的⽂件描述符數目,如返回0表⽰已超時, 返回⼩於0表⽰函數失敗
epoll的使用場景
對於多連接, 且多連接中只有⼀部分連接⽐較活躍時, ⽐較適合使⽤epoll
epoll的工作原理(底層紅黑樹存儲,就緒存放於鏈表)
- 當某⼀進程調⽤epoll_create⽅法時,Linux內核會創建⼀個eventpoll結構體,這個結構體中有兩個成員與epoll的使⽤⽅式密切相關
- 每⼀個epoll對象都有⼀個獨⽴的eventpoll結構體,⽤於存放通過epoll_ctl⽅法向epoll對象中添加進來的事件.
- 這些事件都會掛載在紅⿊樹中,如此,重複添加的事件就可以通過紅⿊樹⽽⾼效的識別出來(紅⿊樹的插⼊時間效率是lgn,其中n爲樹的⾼度).
- ⽽所有添加到epoll中的事件都會與設備(網卡)驅動程序建⽴回調關係,也就是說,當響應的事件發⽣時會調⽤這個回調⽅法.
這個回調⽅法在內核中叫eppollcallback,它會將發⽣的事件添加到rdlist雙鏈表中.
在epoll中,對於每⼀個事件,都會建⽴⼀個epitem結構體當調⽤epoll_wait檢查是否有事件發⽣時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可.
- 如果rdlist不爲空,則把發⽣的事件複製到⽤戶態,同時將事件數量返回給⽤戶. 這個操作的時間複雜度是O(1)
epoll的工作方式
- 假如有一個文件描述符就緒,攜帶1K的數據,epoll_wait返回,假設開始讀數據,一次只能讀0.5K的數據,那麼緩衝區還留下0.5K,那麼此時epoll_wait立刻再次返回,告訴服務器該文件描述符就緒,讓其讀完剩餘的0.5K數據,這樣的操作爲水平觸發。
- 邊緣觸發則是如上例如果1K的字節只讀了0.5K,epoll_wait不會立刻返回,而是等到下次這個文件就緒後,返回,此時,服務器纔有可能讀取上次的緩存。故ET模式強制我們將緩衝區的數據一次性全部讀取。所以只能是非阻塞讀寫。
水平觸發(LT)
- 支持阻塞和非阻塞讀寫
邊緣觸發(ET)(效率高)
- 只支持非阻塞讀寫(ET(邊緣觸發)數據就緒只會通知⼀次,也就是說,如果要使⽤ET模式,當數據就緒時,需要⼀直read,直到出錯或完成爲⽌.但倘若當前fd爲阻塞(默認),那麼在當讀完緩衝區的數據時,如果對端並沒有關閉寫端,那麼該read函數會⼀直阻塞。)(想象僵持狀態)
epoll的優點(同select比較)
- ⽂件描述符數目⽆上限: 通過epoll_ctl()來註冊⼀個⽂件描述符, 內核中使⽤紅⿊樹的數據結構來管理所有需要監控的⽂件描述符.
- 基於事件的就緒通知⽅式: ⼀旦被監聽的某個⽂件描述符就緒, 內核會採⽤類似於callback的回調機制, 迅速激活這個⽂件描述符. 這樣隨着⽂件描述符數量的增加, 也不會影響判定就緒的性能;
- 維護就緒隊列: 當⽂件描述符就緒, 就會被放到內核中的⼀個就緒隊列中. 這樣調⽤epoll_wait獲取就緒⽂件描述符的時候, 只要取隊列中的元素即可, 操作的時間複雜度是O(1);
- 從接口使用的角度上講:不必每次都重新設置要監控的文件描述符,使接口使用方便,也能夠避免用戶態和內核態之間,來回的拷貝文件描述符結構。
LT
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
typedef struct epoll_event epoll_event;
void ProcessListenScok(int epoll_fd, int listen_sock){
sockaddr_in peer;
socklen_t len;
int new_sock = accept(listen_sock,(sockaddr*)&peer,&len);
if(new_sock <0 ){
perror("accept");
return;
}
//把 new_sock 加入到epoll之中
epoll_event event;
event.events = EPOLLIN;
event.data.fd = new_sock;
int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&event);
if(ret < 0){
perror("epoll_ctl ADD");
return;
}
printf("[client %d] connected\n",new_sock);
return;
}
int ServerInit(const char* ip,short port){
int listen_sock = socket(AF_INET,SOCK_STREAM,0);
if(listen_sock < 0){
perror("socket");
return -1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
int ret = bind(listen_sock,(sockaddr*)&addr,sizeof(addr));
if(ret < 0 ){
perror("bind");
return -1;
}
ret = listen(listen_sock,10);
if(ret < 0){
perror("listen");
return -1;
}
return listen_sock;
}
void ProcessNewSock(int epoll_fd,int new_sock){
char buf[1024] = {0};
ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
if(read_size < 0){
perror("read");
return;
}
//讀到返回值爲0,對端關閉了文件描述符。
//本端也應該關閉文件描述符,並且把文件描述符從epoll之中刪除掉
if(read_size == 0){
close(new_sock);
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,new_sock,NULL);
printf("[client %d] disconnected\n",new_sock);
return;
}
buf[read_size] = '\0';
printf("[client %d]say:%s\n",new_sock,buf);
write(new_sock,buf,strlen(buf));
return;
}
int main(int argc,char* argv[]){
if(argc != 3){
printf("Usage: ./server_epoll [ip] [port]\n");
return 1;
}
int listen_sock = ServerInit(argv[1],atoi(argv[2]));
if( listen_sock < 0){
perror("ServerInit");
return 1;
}
//創建並初始化epoll;
int epoll_fd = epoll_create(9);
if(epoll_fd < 0){
perror("epoll_create");
return 1;
}
//把listen_sock放置到epoll
epoll_event event;
event.events = EPOLLIN;
event.data.fd = listen_sock;
int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&event);
if(ret < 0){
perror("epoll_ctl");
return -1;
}
printf("ServerInit OK!\n");
while(1){
epoll_event output_event[100];
int nfds = epoll_wait(epoll_fd,output_event,100,-1);
if(nfds < 0){
perror("epoll_wait");
continue;
}
//output_event[]裏邊保存着準備好的文件描述符
int i = 0;
for(;i < nfds;++i ){
if(listen_sock == output_event[i].data.fd){//讀
ProcessListenScok(epoll_fd,listen_sock);
}
else{
ProcessNewSock(epoll_fd,output_event[i].data.fd);//增加
}
}//for
}//while
return 0;
}
ET