poll
相比於select函數
,poll的跨平臺移植性不如select,因爲poll函數只能在linux環境中使用,也是採用輪詢
遍歷的方式。
與select函數的不同之處:
- poll不會限制文件描述符的個數
- 文件描述符對應一個事件結構,這個結構中有兩個事件,一個是要監控的文件描述符,另一個是這個文件描述符所對應的事件
函數接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- fds:事件結構數組
- nfds:fds數組中有效的元素個數
- timeout:超時時間
struct pollfd 結構體
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
其中:
- fd:該結構體所關心的文件描述符數值
- events:所關心這個文件描述符的那些事件,事件與事件之間採用按位或的方式連接
- revents:當關心的文件描述符產生對應的關心的事件時,返回給調用者發生的事件(每次監控的時候,就會被初始化爲空)
events 和 revents的取值 :
事件 | 描述 | 是否可以作爲輸入 | 是否可以作爲輸出 |
---|---|---|---|
POLLIN | 普通或優先級帶數據可讀 | 是 | 是 |
POLLRDNORM | 普通數據可讀 | 是 | 是 |
POLLRDBAND | 優先級帶數據可讀(linux不支持) | 是 | 是 |
POLLPRI | 高優先級數據可讀,比如TCP帶外數據 | 是 | 是 |
POLLOUT | 數據可寫(普通數據和優先數據) | 是 | 是 |
POLLWRNORM | 普通數據可寫 | 是 | 是 |
POLLWRBAND | 優先級帶數據可寫 | 是 | 是 |
POLLERR | 發生錯誤 | 否 | 是 |
POLLHUP | 發生掛起,管道的寫端關閉後,讀端描述符將受到POLLHUP事件 | 否 | 是 |
POLLNVAL | 描述字不是一個打開的文件 | 否 | 是 |
編程實例
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <poll.h>
using namespace std;
int main()
{
//創建一個struct pollfd結構體,關心0號文件描述符,標準輸入
//監視可讀事件
struct pollfd poll_fd;
poll_fd.fd = 0;
poll_fd.events = POLLIN;
//輪詢遍歷
while(1)
{
int ret = poll(&poll_fd,1,1000);
if(ret < 0)//poll出錯
{
perror("poll");
sleep(3);
continue;
}
else if(ret == 0)//監控超時
{
cout<<"poll timeout"<<endl;
continue;
}
else if(poll_fd.revents == POLLIN)//當發送的事件時可讀事件時
{
//讀取數據
char buf[1024] = {'\0'};
read(0,buf,sizeof(buf) - 1);
cout<<"input : "<<buf<<endl;
}
}
return 0;
}
如果我們一次所傳入的數據超過了一次可以接受的數據大小時,就會別分批進行接收
poll 的優缺點:
優點:
- poll採用了
事件結構
的方式,簡化了程序,並且他不限制文件描述符的個數
- 不需要我們重新添加文件描述符到事件的結構數組當中
缺點:
- poll也是需要
輪詢遍歷
事件結構數組,那麼當文件描述符增多的時候,性能可能不會那麼好了 - poll
不支持跨平臺
操作 - poll不會告訴我們哪一個文件描述符就緒了,想要找到這個就緒的文件描述符,就需要我們進行遍歷操作
- poll在操作的過程中,也是需要時間將結構數組從用戶空間拷貝到內核空間中,再將內核空間拷貝到用戶空間
epoll
在man
手冊中,對於epoll的描述是:
The epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if
I/O is possible on any of them.
也就是:
epoll API執行與poll(2)類似的任務:監視多個文件描述符,看看是否存在
I/O是可能的任何一個
epoll是爲了處理大量的文件描述符而改進的poll函數
函數接口
創建epoll操作句柄:
#include <sys/epoll.h>
int epoll_create(int size);
- size:定義的是epoll中最大可以監控的文件描述符個數,但是在
linux2.6.8
之後,size參數是被忽略的,變成了可以擴容的方式。(size 不可以傳入一個負數)
在使用完之後,必須使用close
函數進行關閉
epoll 的操作:
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
- epfd:之前使用
epoll_create
函數返回的epoll操作句柄 - op:想讓epoll_stl函數所做的哪些事
- fd:告訴epoll函數,我們所關心的文件描述符
- event:他是一個
struct epoll_event
類型的結構體,表示的是epoll事件的結構
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_PACKED;
struct epoll_event :
-
第一個參數:
uint32_t events
,是我們想讓文件描述符關心的事件集合
其中,EPOLLIN,是可讀事件;EPOLLOUT,是可寫事件 -
第二個參數
epoll_data_t data
,他是一個epoll_data
類型的聯合結構體
typedef union epoll_data :
- void* ptr:這裏可以傳遞的信息,就是當epoll監控的文件描述符就緒的時候,等到函數返回,我們可以通過ptr拿到這些信息;其在使用的時候,傳入一個結構體,
(自定義結構體)struct my_epoll_data{int fd}
,必須在結構體中包含文件描述符 - int fd:我們所關心的文件描述符,可以當做文件描述符中的事件就緒之後,返回給我們時查看;其取值爲文件描述符的數值
- ptr和fd,兩者在使用的時候,只能選其中的一個。
監控:
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
- epfd:epoll操作句柄
- events:epoll事件結構數組。作爲出參,他返回的是就緒的事件結構(每一個事件結構都對應一個文件描述符)
- maxevents:最大可以拷貝的事件結構數量
- timeout:超時時間
epoll 的工作原理:
- 當某一進程調用epoll_create方法的時候,linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員與epoll使用方式相關,分別是紅黑樹和雙向鏈表
- 那些沒有就緒的事件,用於存放通過
epoll_ctl
方法向epoll對象
中添加進來的事件,都會被掛載在紅黑樹中(這樣就可以識別出重複的事件,並且還讓查找效率提高) - 當紅黑樹中某個事件就緒後,就會被拷貝到雙向鏈表中(這是一個內核行爲)。所以說,雙向鏈表中的數據都是就緒的
- 當調用epoll_wait函數的時候,雙向鏈表向參數中進行拷貝的方式,是通過改變頁表,直接指向物理內存中的雙向鏈表中就緒事件所佔用的物理內存
epoll的兩種工作方式
水平觸發 LT模式
在這種模式下,當epoll
中檢測到了等待觸發的事件就緒後,可以不立即進行處理,而是隻處理一部分。等到第二次調用epoll_wait
函數的時候,可以接着操作剛纔沒有處理完的數據。他支持阻塞讀寫與非阻塞讀寫
這是epoll
函數的默認工作方式,另外select 和 poll
都是水平觸發
-
對於可讀事件,只要接收緩衝區中的存在數據,就會一直觸發該接收該可讀事件,直到沒有數據可讀
-
對於可寫事件,只要發送緩衝區中存在數據,就會一直觸發該可寫事件就緒,直到發送緩衝區中沒有數據
函數模擬
/*================================================================
* Copyright (C) 2020 Sangfor Ltd. All rights reserved.
*
* 文件名稱:test.cpp
* 創 建 者:dcl
* 創建日期:2020年06月20日
* 描 述:
*
================================================================*/
#include <unistd.h>
#include <cstdio>
#include <sys/epoll.h>
#include <iostream>
using namespace std;
int main()
{
int epollfd = epoll_create(10);
if(epollfd < 0)
{
perror("epoll_create");
return 0;
}
struct epoll_event ev;
ev.events = EPOLLIN;//可讀事件
ev.data.fd = 0;
//int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//添加一個文件描述符到紅黑樹中
int ret = epoll_ctl(epollfd,EPOLL_CTL_ADD,0,&ev);
if(ret < 0)
{
perror("epoll_ctl");
return 0;
}
while(1)
{
struct epoll_event fd_arr[10];
//int epoll_wait(int epfd, struct epoll_event *events,
// int maxevents, int timeout);
int ret = epoll_wait(epollfd,fd_arr,sizeof(fd_arr) / sizeof(fd_arr[0]),3000);
if(ret < 0)
{
perror("epoll_wait");
continue;
}
else if(ret == 0)
{
cout<<"timeout"<<endl;
sleep(1);
continue;
}
cout<<"---"<<ret<<endl;
//文件描述符被觸發
for(int i = 0; i < ret; i++)
{
if(fd_arr[i].data.fd == 0)
{
char buf[3] = {'\0'};
read(fd_arr[i].data.fd,buf,sizeof(buf) - 1);
cout<<"input : "<<buf<<endl;
}
}
}
return 0;
}
在設置接收數據的大小的時候,我們給數組設置的比較小,當我們發送大量的數據,就會出現下面這種情況
邊緣觸發 ET模式
在ET模式下,如果我們一次只是處理了部分數據,那麼剩下的數據在下一次觸發的時候纔會被處理的。
也就是說,ET模式下,當文件描述符就緒後,就只有一次處理文件中數據的機會,所以我們需要使用循環的方式進行接收數據。他支持非阻塞讀寫
程序模擬:
/*================================================================
* Copyright (C) 2020 Sangfor Ltd. All rights reserved.
*
* 文件名稱:test.cpp
* 創 建 者:dcl
* 創建日期:2020年06月20日
* 描 述:
*
================================================================*/
#include <errno.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <cstdio>
#include <iostream>
#include <string>
using namespace std;
inline void slove(struct epoll_event* fd_arr,int ret)
{
for(int i = 0; i < ret; i++)
if(fd_arr[i].data.fd == 0)
{
string ans = "";
while(1)
{
char buf[3] = {'\0'};
ssize_t readsize = read(0,buf,sizeof(buf) - 1);
if(readsize < 0)
{
//說明數據讀完了
if(errno == EAGAIN || errno == EWOULDBLOCK)
break;
perror("read");
break;
}
ans += buf;
break;
if(readsize < (ssize_t)sizeof(buf) - 1)
break; //數據讀完了
}
if(ans != "") cout<<"read data: "<<ans<<endl;
}
}
int main()
{
//設置文件描述符屬性爲非阻塞
//int fcntl(int fd, int cmd, ... /* arg */ );
int flag = fcntl(0,F_GETFL);
fcntl(0,F_SETFL,flag | O_NONBLOCK);
//創建epoll操作句柄
int epoll_fd = epoll_create(10);
if(epoll_fd < 0)
{
perror("epoll_create");
return 0;
}
//添加文件描述符
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = 0;
int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,0,&ev);
if(ret < 0)
{
perror("epoll_ctl");
return 0;
}
while(1)
{
struct epoll_event fd_arr[10];
ret = epoll_wait(epoll_fd,fd_arr,sizeof(fd_arr) / sizeof(fd_arr[0]),-1);
if(ret < 0)
{
perror("epoll_wait");
continue;
}
slove(fd_arr,ret);
}
return 0;
}
沒有循環接收數據時的情況
數據的接收過程:
在循環接收的過程中,當某一次我們接收的數據的大小小於我們的最大接收能力的時候,就說明數據讀取完了
還有一種意外情況,就是當我們所發送的數據正好是我們最大接收能力的整數倍時,比如說:我們發送一個 123\r\n
,我們一次只能接收兩個數據,那麼當最後一次接收到數據的時候,其實數據讀取完畢了,但是read函數還是處於一個等待數據的階段,就會卡死,造成一個飢餓的情況
- ET模式,在加上循環後很容易造成飢餓狀態
解決方法:將文件描述符設置爲非阻塞狀態,我們可以調用fcntl函數
int fcntl(int fd, int cmd, ... /* arg */ );
- fd:所要操作的哪一個文件描述符
- cmd:fcntl函數需要做的事情
F_GETFL
,獲取文件描述符屬性,文件描述符通過返回值返回給我們,arg不需要設置
F_SETFD
,設置文件描述符屬性,arg就是文件描述符需要設置的屬性,類型爲int