目錄
- 1. 五種高階IO模型
- 1.1 阻塞IO
- 1.2 非阻塞IO
- 1.3 信號驅動IO
- 1.4 多路轉接IO
- 1.5 異步IO
- 2. 多路轉接技術(select、poll、epoll)
- 2.1 select函式
- 2.1.1 select 函式詳解
- 2.1.2 select函式優缺點
- 2.2 poll函式
- 2.2.1 poll函式詳解
- 2.2.2 poll函式優缺點
- 2.3 epoll函式
- 2.3.1 epoll相關函式詳解
- 2.3.2 epoll作業原理
- 2.3.3 epoll的優點
- 2.3.4 LT模式和ET模式
- 3. select、poll、epoll的代碼驗證
- 3.1 select代碼驗證
- 3.1.1 select 簡單驗證
- 3.1.2 使用select函式解決TCP單行程阻塞問題
- 3.1.3 在多執行緒下呼叫select函式
- 3.2 poll代碼驗證
- 3.3 epoll代碼驗證
- 3.3.1 epoll的簡單驗證
- 3.3.2 使用epoll函式解決TCP單行程的阻塞問題
- 3.3.3 epoll函式的ET模式的驗證和解決
1. 五種高階IO模型
首先我們要明確的是在任何IO操作中,均包含兩個步驟,等待和拷貝,而在實際的業務中,等待所消耗的時間往往大于拷貝的時間,因此,讓IO操作更高效,核心的方法就是將等待的時間縮短,
低階IO:是指類似于將用戶輸入的內容讀取到某個變數中,將變數中的值列印在螢屏上等等,簡單來說就是對C庫自己所維護的緩沖區進行I/O操作,
高階IO:通常應用于網路Socket編程,對UDP(TCP)所維護的發送緩沖區和接識訓沖區進行I/O操作,并且高階IO分為同步IO和異步IO,同步IO又分為阻塞IO、非阻塞IO、信號驅動IO和多路轉接IO
1.1 阻塞IO
阻塞IO:在內核將資料準備好之前(等待+拷貝),系統呼叫會一直進行等待,并且所有的套接字默認均是阻塞方式,
1.2 非阻塞IO
非阻塞IO:若當前內核沒有將資料準備好,則系統呼叫會直接回傳,并回傳一個EWOULDBLOCK錯誤碼,
非阻塞IO一般都是搭配回圈來使用的(也叫輪詢),這對系統資源是較大的浪費,一般都是在特定的場景下使用,
擴展:將一個檔案描述符設定為非阻塞屬性,
int fcntl(int fd, int cmd, ... /* arg */ ),處于#include <fcntl.h>
- 首先通過給cmd傳入
F_GETFL宏來獲取到當前檔案描述符的屬性- 其次再通過獲得的屬性按位或上
O_NONBLOCK屬性,將檔案描述符設定為非阻塞屬性
代碼如下:
int f1 = fcntl(fd,F_GETFL);
if(f1 < 0)
{
perror("fcntl");
return ;
}
fcntl(fd,F_SETFL, f1 | O_NONBLOCK);
1.3 信號驅動IO
信號驅動IO:當內核將資料準備好之后,或者說告訴應用行程何時才可以開始拷貝資料,會給應用行程發送一個SIGIO的信號,通知其進行IO操作,當應用程式接收到該信號之后,證明資料已經準備好了,接下來就會呼叫系統呼叫函式對其進行相應的IO操作
1.4 多路轉接IO
多路轉接IO:在流程上與阻塞IO類似,但是其本質上最核心的地方就是可以一次性等待多個檔案描述符的就緒狀態,
1.5 異步IO
異步IO:當內核將資料拷貝好之后,通知應用行程進行相關的IO操作,需要注意的是他和信號驅動IO很像,但是信號驅動IO是內核將資料準備好后才通知,而異步IO是內核將資料拷貝好后才通知,
注:為了性能和效率的優先,C++默認采用的是異步IO的方式,
2. 多路轉接技術(select、poll、epoll)
首先我們要知道多路復用函式的作用是什么,其本質上就是讓內核幫助程式員監控多個檔案描述符的IO事件,一旦監控的某個檔案描述符對應的事件產生(IO就緒),就會通知呼叫者,也就是說可以并行的處理多條客戶端的請求,換句話說就是實作了高并發,
2.1 select函式
作用:監控多個檔案描述符,就緒之后,通知呼叫者,
2.1.1 select 函式詳解
- nfds:select監控事件集合(fd_set)的范圍,范圍是從[0,1023]之間去進行選擇范圍,nfds的取值為:監控的最大檔案描述符數值+1
- fd_set:事件集合型別
readfds:可讀事件結合
writefds:可寫事件集合
exceptfds:例外事件集合
并且內核在使用該陣列的時候采用的是位圖的方式,因此總共有1024個位元位,
- timeout:
NULL:阻塞,
0 :非阻塞,
傳遞一個struct timeval:代表著帶有超時時間的方式,
回傳值:
- 監控成功:回傳就緒的檔案描述符個數
監控成功會回傳就緒的檔案描述符個數,并且在回傳的時候,會將事件集合中未就緒的檔案描述符去除掉
- 監控失敗:回傳-1
引數含義補充:
- 如果關心某個檔案描述符對應的某種事件,則將檔案描述符添加到對應的事件集合當中去,例如:關心3號檔案描述符的可讀事件,則將3號檔案描述符添加到對應的readfds事件中,
- 添加檔案描述符到事件集合的時候,就是將檔案描述符對應的位元位設定為1,
- 如果一個檔案描述符關心多種事件(可讀、可寫、例外),則將檔案描述符添加到不同的事件集合當中去,
- select的監控效率會隨著檔案描述符的增多而下降,本質原因就是由于監控輪詢的范圍變大了,
- 對相應事件集合的操作:
2.1.2 select函式優缺點
優點:
- According to POSIX.1-2001,select遵循的是POISX標準,說明select函式是一個跨平臺的函式,既可以在linux中運行,也可以在win中運行,
- select在帶有超時時間監控的時候,超時時間單位可以是微秒,
缺點:
- 監控檔案描述符的個數最多為1024個
- 隨著監控檔案描述符的增多,監控的效率在逐漸的下降(本質上是select在輪詢的進行監控)
- 可讀、可寫、例外這些事件需要添加到單獨的添加到不同的事件集合中
- 當select監控成功之后,會從事件集合當中去除未就緒的檔案描述符,這表明程式在下一次運行的時候,需要重新添加檔案描述符,
- 在每次select進行監控的時候,都會將準備好的事件集合拷貝到內核空間,select回傳的時候,都會相應的事件集合從內核空間再拷貝到用戶空間,
2.2 poll函式
前提:均是監控多個檔案描述符,就緒之后,然后通知呼叫者,與select相比,不支持跨平臺,與epoll相比,效率沒有epoll高,
2.2.1 poll函式詳解
struct pollfd*: 事件集合結構
想讓poll監控幾個元素,只需要在定義事件結構資料的時候,多傳遞幾個元素,
nfds:事件結構陣列中的有效元素的個數timeout:>0:帶有超時事件,單位秒
==:非阻塞
<0:阻塞
回傳值:回傳就緒的檔案描述符個數
2.2.2 poll函式優缺點
優點:
- 提出了事件結構的方式,在給poll函式傳遞引數的時候,不需要分別添加到事件集合當中,
- 事件結構陣列的大小可以根據程式員自己進行定義,并沒有上限的要求,
- 不用再監控到就緒之后,重新添加檔案描述符,
缺點:
- 不支持跨平臺,
- 內核對事件結構陣列進行監控的時候也采用的是輪詢遍布的方式,
2.3 epoll函式
epoll函式是目前世界上公認在Linux下,多路轉接監控效率最高的模型
2.3.1 epoll相關函式詳解
① 創建epoll操作句柄
size:自從Liunx2.6.8之后,size引數是被忽略的,但是不要傳遞一個小于0的數字,回傳值:回傳epoll的操作句柄,
② 注冊待要監控的檔案描述符
epfd:epoll操作句柄op:
fd:待處理(添加、修改、洗掉)的檔案描述符event:檔案描述符對應的事件結構
回傳值:
③ epoll的等待介面
epfd:epoll的操作句柄events:型別同 epoll_ctl 中一樣,只不過這里的是事件集合陣列,從epoll當中獲取就緒的事件結構maxevents:最多一次獲取多少個事件結構timeout:>0:帶有超時事件,單位秒
==:非阻塞
<0:阻塞回傳值:就緒的檔案描述符個數
2.3.2 epoll作業原理
當某一個行程呼叫epoll_create函式時,LInux內核會創建一個eventpoll的結構體,這個結構體中有兩個成員與epoll的使用方式密切相關,
簡單概述一下:
每當呼叫epoll_create函式的時候,內核就會創建一個
eventpoll的結構體,在該結構體中有兩個成員,型別分別為紅黑樹和雙向鏈表,當呼叫epoll_ctl函式進行添加、修改或洗掉時,本質上就是對該紅黑樹進行添加、修改和洗掉,而每一個添加進來的紅黑樹結點均會和設備(網卡)驅動程式建立一個回呼關系(這個回呼函式為ep_poll_callback),當某個檔案描述符就緒之后,他會呼叫這個回呼函式將該就緒的事件結構添加到雙向鏈表當中,而當呼叫epoll_wait進行監控的時候,如果雙向鏈表為空,則表明當前沒有就緒的事件發生,如果不為空,則將雙向鏈表中的內容復制到用戶態,并回傳將事件數量回傳給用戶,
注意:這里的雙向鏈表其實實作的是一個佇列,雖然是一個雙向鏈表,但是他只支持先進先出(FIFO),是佇列的特性,
2.3.3 epoll的優點
- 沒有數量的限制,檔案描述符的數量為內核所支持的最大上限(65536)
- 事件回呼機制,當檔案描述符就緒之后,會呼叫回呼函式將事件結構復制到雙向鏈表中,呼叫epoll_wait回傳時,直接訪問就緒佇列就可以知道有多少檔案描述符就緒,這個操作的時間復雜度為O(1),即使檔案運算子很多,也不會受到影響,
- 資料拷貝是輕量的,只有在合適的時候呼叫
EPOLL_CTL_ADD將對應的檔案描述符介面拷貝到內核中,這個操作并不頻繁(select/poll每次都要回圈的進行拷貝),
select、poll、epoll對比
2.3.4 LT模式和ET模式
舉個例子:當你在中午飯點玩游戲的時候,如果這個時候飯剛好做好了,
LT:家里人第一次通知的時候,你沒有管,那他們還會通知第二次、第三次…
ET:家里人在第一次通知的時候,你沒有管,那么他們就不會在通知你了,
① LT(Level Triggered) 水平觸發作業模式
在LT模式下,當epoll檢測到事件就緒的時候,可以不處理或處理一部分,但是可以連續多次呼叫epoll_wait對事件進行處理,簡單點來說的話就是如果事件來了,不管來了幾個,只要仍然有未處理的事件,epoll都會通知你,
② ET(Edge Triggered) 邊緣觸發作業模式
在ET模式下,當epoll檢測到事件就緒的時候,會立即進行處理,并且只會處理一次,換句話說就是檔案描述符上的事件就緒之后,只有一次處理機會, 簡單來說就是如果事件來了,不管來了幾個,你若不處理或者沒有處理完,除非下一個事件到來,否則epoll將不會再通知你,
LT模式存在的問題:
如果可讀或者可寫事件未進行處理,會頻繁反復的激活未處理事件
解決:
在不想處理某個事件的時候就將它從epoll中移除,需要時再添加上
ET模式存在的問題:
如果可讀或者可寫事件沒有全部處理,會有老資料殘留,需要等待新資料的到來才會被處理
解決:
- 回圈讀取或者寫入資料,直至回傳值未EAGAIN或者EWOULDBLOCK(回圈呼叫)
- 讀取或寫入資料后,通過epoll_ctl設定EPOLL_CTL_MOD,激活未處理事件(相當于將當前未處理事件設定未新事件)
3. select、poll、epoll的代碼驗證
3.1 select代碼驗證
3.1.1 select 簡單驗證
先舉個簡單的例子,利用select函式對系統的0號檔案描述符(讀緩沖區)進行監控,一旦監控到讀的事件,則將其讀入的內容列印到螢屏上,
#include <unistd.h>
#include <sys/select.h>
#include <iostream>
using namespace std;
#define nfds 1
int main()
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(0,&readfds);
while(1)
{
fd_set tmp = readfds;
int ret = select(nfds,&tmp,NULL,NULL,NULL);
if(ret < 0)
{
cout << "select failed" << endl;
return 0;
}
for(int i = 0; i < nfds; ++i){
if(FD_ISSET(0,&tmp))
{
char buf[1024] = {0};
read(0,buf,sizeof(buf)-1);
cout << buf << endl;
}
}
}
return 0;
}
結果驗證
3.1.2 使用select函式解決TCP單行程阻塞問題
在TCP單行程的條件下,存在于將accept函式放到while回圈內部或外面所造成的問題
內部:每次接收都是新的連接,和每個客戶端只能聊一次(accept函式阻塞)
外部:只能接收一個客戶端的連接(recv函式阻塞)
更為詳細的請看:Linux:TCP Socket編程(代碼實戰),
解決代碼如下:
為了更方便使用,我們將selecct函式的相關操作封裝為一個類
#include <sys/select.h>
#include <stdio.h>
#include <iostream>
#include <vector>
using namespace std;
class SocketSelect{
public:
SocketSelect()
{
FD_ZERO(&readfds_);
nfds_ = 0;
}
void SetFd(int fd)
{
FD_SET(fd,&readfds_);
if(fd > nfds_)
nfds_ = fd;
}
void RmFd(int fd)
{
FD_CLR(fd,&readfds_);
for(int i = nfds_; i >= 0; ++i)
{
if(FD_ISSET(i,&readfds_))
{
nfds_ = i;
break;
}
}
}
int SelectWait(vector<int>& iv)
{
fd_set tmp = readfds_;
int ret = select(nfds_+1,&tmp,NULL,NULL,NULL);
if(ret < 0)
{
printf("select failed\n");
return -1;
}
for(int i = 0; i <= nfds_; ++i)
{
if(FD_ISSET(i,&tmp))
iv.push_back(i);
}
return ret;
}
~SocketSelect()
{
FD_ZERO(&readfds_);
}
private:
int nfds_;
fd_set readfds_;
};
服務端代碼:
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "ssocket.hpp"
using namespace std;
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
{
cout << "socket failed" << endl;
return 0;
}
sockaddr_in addr;
addr.sin_port = htons(18989);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
if(ret < 0)
{
close(sockfd);
cout << "bind failed" << endl;
}
ret = listen(sockfd,5);
if(ret < 0)
{
close(sockfd);
cout << "listen failed" << endl;
}
SocketSelect ss;
ss.SetFd(sockfd);
while(1)
{
vector<int> iv;
ret = ss.SelectWait(iv);
if(ret < 0)
{
continue;
}
for(size_t i = 0; i < iv.size(); ++i)
{
if(iv[i] == sockfd)
{
//new con
int newsockfd = accept(iv[i],NULL,NULL);
if(newsockfd < 0)
{
cout << "accept failed" << endl;
continue;
}
ss.SetFd(newsockfd);
}
else
{
//recv
char buf[1024] = {0};
ssize_t recv_size = recv(iv[i],buf,sizeof(buf)-1,0);
if(recv_size < 0)
{
cout << "recv failed" << endl;
continue;
}
else if(recv_size == 0)
{
cout << "peer shutdown" << endl;
ss.RmFd(iv[i]);
close(iv[i]);
continue;
}
else
{
cout << buf << endl;
}
}
}
}
close(sockfd);
return 0;
}
客戶端代碼:
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <iostream>
using namespace std;
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
{
cout << "socket failed" << endl;
return 0;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(18989);
addr.sin_addr.s_addr = inet_addr("118.89.67.215");
int ret = connect(sockfd,(struct sockaddr *)&addr,sizeof(addr));
if(ret < 0)
{
cout << "connect failed" << endl;
}
while(1)
{
//char buf[1024] = "i am client1\n";
char buf[1024] = "i am clientxxxxx\n";
ssize_t send_size = send(sockfd,buf,strlen(buf),0);
if(send_size < 0)
{
cout << "send failed" << endl;
continue;
}
sleep(1);
}
return 0;
}
運行結果:
完美解決了單行程下兩個阻塞的問題,
3.1.3 在多執行緒下呼叫select函式
在多執行緒下呼叫select函式(是多個執行緒共同偵聽同一個可讀事件集合)
#include <unistd.h>
#include <sys/select.h>
#include <pthread.h>
#include <iostream>
using namespace std;
void* PthreadEntry(void* arg)
{
pthread_detach(pthread_self());
fd_set *readfds = (fd_set*) arg;
while(1)
{
int ret = select(1,readfds,NULL,NULL,NULL);
if(ret < 0)
{
cout << "select failed" << endl;
continue;
}
if(FD_ISSET(0,readfds))
{
char buf[1024] = {0};
read(0,buf,sizeof(buf)-1);
cout << buf << endl;
}
}
return NULL;
}
int main()
{
pthread_t pid;
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(0,&readfds);
for(int i = 0; i < 2; ++i)
{
int ret = pthread_create(&pid,NULL,PthreadEntry,(void *)&readfds);
if(ret < 0)
{
cout << "pthread_create failed" << endl;
continue;
}
}
while(1)
{
sleep(1);
}
return 0;
}
結果說明,沒有必要,因為不管是阻塞還是非阻塞,當前緩沖區只能由一個執行緒進行讀,在讀完之后,由于檔案描述符默認是阻塞屬性,因此就會阻塞,其實這是一種驚群效應,當一個fd事件就緒的時候,所有等待這個fd的執行緒都會被喚醒,但是最終只會有一個執行緒會讀到資料,這就是驚群效應,它會造成極大的性能和資源的浪費,
3.2 poll代碼驗證
利用poll函式對系統的0號檔案描述符(讀緩沖區)進行監控,一旦監控到讀的事件,則將其讀入的內容列印到螢屏上,
#include <unistd.h>
#include <poll.h>
#include <iostream>
using namespace std;
int main()
{
struct pollfd pollfd[1];
pollfd[0].fd = 0;
pollfd[0].events = POLLIN;
while(1)
{
int res = poll(pollfd,1,-1);
if(res < 0)
{
cout << "poll failed" << endl;
return 0;
}
for(int i = 0; i < 1; ++i)
{
if(pollfd[i].revents == POLLIN)
{
char buf[1024] = {0};
read(pollfd[i].fd,buf,sizeof(buf)-1);
cout << buf << endl;
}
}
}
return 0;
}
運行結果:
3.3 epoll代碼驗證
3.3.1 epoll的簡單驗證
對0號檔案描述符進行監控,當發生讀事件的時候,則從緩沖區中讀資料并列印到螢屏上,
#include <unistd.h>
#include <sys/epoll.h>
#include <iostream>
#define MAXSIZE 1
using namespace std;
int main()
{
int handle = epoll_create(5);
if(handle < 0)
{
cout << "epoll_create failed" << endl;
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = 0;
int res = epoll_ctl(handle,EPOLL_CTL_ADD,0,&ee);
if(res < 0)
{
cout << "epoll_ctl failed" << endl;
return 0;
}
while(1)
{
struct epoll_event arr[MAXSIZE];
res = epoll_wait(handle,arr,MAXSIZE,-1);
if(res < 0)
{
cout << "epoll_wait failed" << endl;
continue;
}
for(int i = 0; i < MAXSIZE; ++i)
{
if(arr[i].events == EPOLLIN)
{
char buf[1024] = {0};
read(arr[i].data.fd,buf,sizeof(buf)-1);
cout << buf << endl;
}
}
}
return 0;
}
結果驗證:
3.3.2 使用epoll函式解決TCP單行程的阻塞問題
#include <iostream>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
#define MAXSIZE 10
#define Judge(x) if(x<0) {cout << "failed" << endl; return 0;}
int main()
{
int lisnfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(lisnfd < 0)
{
cout << "socket failed" << endl;
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(18989);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(lisnfd,(struct sockaddr *)&addr,sizeof(addr));
Judge(ret);
ret = listen(lisnfd,5);
Judge(ret);
int epoll_handle = epoll_create(2);
Judge(epoll_handle);
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = lisnfd;
ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,lisnfd,&ee);
Judge(ret);
while(1)
{
struct epoll_event arr[MAXSIZE];
ret = epoll_wait(epoll_handle,arr,MAXSIZE,-1);
if(ret < 0)
{
cout << "epoll_wait failed" << endl;
return 0;
}
for(int i = 0; i < MAXSIZE; ++i)
{
if(arr[i].data.fd == lisnfd)
{
int sockfd = accept(lisnfd,NULL,NULL);
if(sockfd < 0)
{
cout << "accept failed" << endl;
continue;
}
struct epoll_event tmp;
tmp.events = EPOLLIN;
tmp.data.fd = sockfd;
ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,sockfd,&tmp);
if(ret < 0)
{
cout << "epoll_ctl failed" << endl;
continue;
}
}
else
{
char buf[1024] = {0};
ssize_t recv_size = recv(arr[i].data.fd,buf,sizeof(buf)-1,0);
if(recv_size < 0)
{
cout << "recv failed" << endl;
continue;
}
else if(recv_size == 0)
{
cout << "Peer Shutdown" << endl;
epoll_ctl(epoll_handle,EPOLL_CTL_DEL,arr[i].data.fd,&arr[i]);
close(arr[i].data.fd);
continue;
}
else
cout << buf << endl;
}
}
}
close(lisnfd);
return 0;
}
客戶端代碼和上面select一樣
運行結果:
3.3.3 epoll函式的ET模式的驗證和解決
首先,我們需要知道的是將epoll設定為ET模式,只需要在epoll_ctl的時候,將其對應的events設定為EPOLLET即可,
我們使用epoll函式監控0號檔案描述符,當要進行讀取資料的時候,若epoll監控到該檔案描述符就緒之后,則對緩沖區中的內容進行讀操作,我們在這里規定一次讀2個Byte的資料,而我們輸入的資料則要大于2Byte,
首先來看LT模式下,它的運行情況:
#include <unistd.h>
#include <sys/epoll.h>
#include <iostream>
using namespace std;
int main()
{
int epoll_handle = epoll_create(2);
if(epoll_handle < 0)
{
cout << "epoll_create failed" << endl;
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = 0;
int ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,0,&ee);
if(ret < 0)
{
cout << "epoll_ctl failed" << endl;
return 0;
}
while(1)
{
struct epoll_event arr[2];
ret = epoll_wait(epoll_handle,arr,2,-1);
if(ret < 0)
{
cout << "epoll_wait failed" << endl;
return 0;
}
for(int i = 0; i < 2; ++i)
{
if(arr[i].data.fd == 0)
{
char buf[3] = {0};
read(arr[i].data.fd,buf,sizeof(buf)-1);
cout << buf << endl;
}
}
}
return 0;
}
運行結果:
一次只讀兩個位元組,當發現沒有讀完,則會立即重復的進行讀,說明這里的epoll_wait會一直的進行通知,這也就說明在LT模式下,只要當前事件未處理完,則epoll函式會一直進行通知,
ET模式下的運行情況
#include <unistd.h>
#include <sys/epoll.h>
#include <iostream>
using namespace std;
int main()
{
int epoll_handle = epoll_create(2);
if(epoll_handle < 0)
{
cout << "epoll_create failed" << endl;
return 0;
}
struct epoll_event ee;
//設定對當前檔案描述符的監控為可讀事件和ET模式
ee.events = EPOLLIN | EPOLLET;
ee.data.fd = 0;
int ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,0,&ee);
if(ret < 0)
{
cout << "epoll_ctl failed" << endl;
return 0;
}
while(1)
{
struct epoll_event arr[2];
ret = epoll_wait(epoll_handle,arr,2,-1);
if(ret < 0)
{
cout << "epoll_wait failed" << endl;
return 0;
}
for(int i = 0; i < 2; ++i)
{
if(arr[i].data.fd == 0)
{
char buf[3] = {0};
read(arr[i].data.fd,buf,sizeof(buf)-1);
cout << buf << endl;
}
}
}
return 0;
}
運行情況:
我們可以清晰的看到,ET模式下當對應事件就緒之后,不管有沒有處理完,均只會通知一次,除非等待下一個事件進來才會再次進行通知,也就是說如果一次未處理完,在下一次處理的時候,要首先處理上次遺留下來的老資料,
ET模式問題的解決
ET模式所帶來的問題就是如果不能一次將未處理的事件處理完,則會遺留老資料未進行處理,因此,我們只需要對代碼進行控制,使其能夠一次性將其處理完即可,
解決思路:
當第一次監控到檔案描述符就緒之后,我們可以用一個回圈來不停的讀取緩沖區當中的值,直到讀完為止,又因為檔案描述符的讀寫默認屬性是阻塞屬性,因此,我們要將檔案描述符的屬性設定為非阻塞屬性(呼叫
fcntl函式)
這里也有需要注意的地方,當呼叫fcntl函式將檔案描述符的屬性設定為非阻塞屬性時,每當進行讀操作的時候,都會回傳一個值,該值代表了讀取的位元組數,當它回傳一個小于0的數時,不一定是讀取失敗了,而是有可能將資料讀完了,這時候就看它回傳的錯誤碼,如果回傳的錯誤是EAGAIN或者EWOULDBLOCK,則代表著將資料讀完了,
代碼如下:
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <iostream>
using namespace std;
int main()
{
//設定檔案描述符的讀寫屬性為非阻塞屬性
int flag = fcntl(0,F_GETFL);
if(flag < 0)
{
cout << "fcntl failed" << endl;
return -1;
}
//設定非阻塞屬性
fcntl(0,F_SETFL,flag | O_NONBLOCK);
int epoll_handle = epoll_create(2);
if(epoll_handle < 0)
{
cout << "epoll_create failed" << endl;
return 0;
}
struct epoll_event ee;
//設定對當前檔案描述符的監控為可讀事件和ET模式
ee.events = EPOLLIN | EPOLLET;
ee.data.fd = 0;
int ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,0,&ee);
if(ret < 0)
{
cout << "epoll_ctl failed" << endl;
return 0;
}
while(1)
{
struct epoll_event arr[2];
ret = epoll_wait(epoll_handle,arr,2,-1);
if(ret < 0)
{
cout << "epoll_wait failed" << endl;
return 0;
}
for(int i = 0; i < 2; ++i)
{
if(arr[i].data.fd == 0)
{
string res = "";
while(1)
{
char buf[3] = {0};
int t = read(arr[i].data.fd,buf,sizeof(buf)-1);
if(t < 0)
{ //注意:這里當回傳值小于0的時候,不一定是發生了錯誤
//由于檔案描述符是非阻塞屬性,因此,每次讀的時候都會回傳一個值
//若回傳的錯誤碼是EAGAIN或者EWOULDBLOCK的時候,說明此時已經將緩沖區中的內容讀完了
if(errno == EAGAIN || errno == EWOULDBLOCK)
break;
}
res += buf;
}
cout << res << endl;
}
}
}
return 0;
}
結果驗證:
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/293218.html
標籤:其他































