epoll EPOLLL、EPOLLET模式與阻塞、非阻塞
EPOLLLT,EPOLLET是epoll兩種不同的模式,前面已經講過他們的區別:觸發的時機不一致。讀取資料的方式因此也不一樣,下面我們分別討論。
在EPOLLLT(水平觸發)模式下,也就是預設的模式,epoll_wait返回可讀事件,表明socket一定收到了資料,我們可以呼叫read函式來讀取資料。如果指定讀取的資料大於緩衝區資料,無論socket是阻塞還是非阻塞的,read不會阻塞,read返回讀取的真實資料。在read之後再次呼叫read,如果socket是阻塞的,read將阻塞,再次收到資料read才返回。此時如果指定讀取的資料大於緩衝區,epoll_wait則不再觸發,否則epoll_wait將再次觸發,因為還有未讀完的資料在緩衝區。
在EPOLLET(電平觸發)模式下,只有新的資料來到時才會觸發,因此在這種情況下,有資料時必須迴圈讀取資料直到read返回-1,並且錯誤碼為EAGAIN,才算讀取了全部的緩衝區資料。
我突然想到一個問題,就是使用epoll時一定要將socket設定為非阻塞嗎?正好知乎上有關於和這個的討論:使用epoll時需要將socket設為非阻塞嗎
發現看來看去仍然得不到正解。俗話說的好,紙上得來終覺淺,要知此事須躬行。自己實現一遍,答案自然有了。以下是我驗證後畫的思維導圖,很能夠說明各種模式下sokcet的動作:
上面的再次read指epoll觸發後呼叫一次read後再呼叫一次,在具體的情況中可以看作while 迴圈讀取資料。
通過上面的圖,我們可以得出結論:
我覺得只有邊沿觸發才必須設定為非阻塞。
邊沿觸發的問題:
1. sockfd 的邊緣觸發,高併發時,如果沒有一次處理全部請求,則會出現客戶端連線不上的問題。不需要討論 sockfd 是否阻塞,因為 epoll_wait() 返回的必定是已經就緒的連線,所以不管是阻塞還是非阻塞,accept() 都會立即返回。
2. 阻塞 connfd 的邊緣觸發,如果不一次性讀取一個事件上的資料,會干擾下一個事件,所以必須在讀取資料的外部套一層迴圈,這樣才能完整的處理資料。但是外層套迴圈之後會導致另外一個問題:處理完資料之後,程式會一直卡在 recv() 函式上,因為是阻塞 IO,如果沒資料可讀,它會一直等在那裡,直到有資料可讀。但是這個時候,如果用另一個客戶端去連線伺服器,伺服器就不能受理這個新的客戶端了。
3. 非阻塞 connfd 的邊緣觸發,和阻塞版本一樣,必須在讀取資料的外部套一層迴圈,這樣才能完整的處理資料。因為非阻塞 IO 如果沒有資料可讀時,會立即返回,並設定 errno。這裡我們根據 EAGAIN 和 EWOULDBLOCK 來判斷資料是否全部讀取完畢了,如果讀取完畢,就會正常退出迴圈了。
總結一下:
1. 對於監聽的 sockfd,最好使用水平觸發模式,邊緣觸發模式會導致高併發情況下,有的客戶端會連線不上。如果非要使用邊緣觸發,可以用 while 來迴圈 accept()。
2. 對於讀寫的 connfd,水平觸發模式下,阻塞和非阻塞效果都一樣,因為在阻塞模式下,如果資料讀取不完全則返回繼續觸發,反之讀取完則返回繼續等待。全建議設定非阻塞。
3. 對於讀寫的 connfd,邊緣觸發模式下,必須使用非阻塞 IO,並要求一次性地完整讀寫全部資料。
附上程式碼:
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#define MAX_LINE 10
#define MAX_EVENTS 500
#define MAX_LISTENFD 5
int createAndListen() {
int on = 1;
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
fcntl(listenfd, F_SETFL, O_NONBLOCK);
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(5859);
if (-1 == bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) {
printf("bind errno, errno : %d \n", errno);
}
if (-1 == listen(listenfd, MAX_LISTENFD)) {
printf("listen error, errno : %d \n", errno);
}
printf("listen in port 5859 !!!\n");
return listenfd;
}
int main(int argc, char const *argv[])
{
struct epoll_event ev, events[MAX_EVENTS];
int epollfd = epoll_create(1); //這個引數已經被忽略,但是仍然要大於
if (epollfd < 0) {
printf("epoll_create errno, errno : %d\n", errno);
}
int listenfd = createAndListen();
ev.data.fd = listenfd;
ev.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
for ( ;; ) {
int fds = epoll_wait(epollfd, events, MAX_EVENTS, -1); //時間引數為0表示立即返回,為-1表示無限等待
if (fds == -1) {
printf("epoll_wait error, errno : %d \n", errno);
break;
}
else {
printf("trig %d !!!\n", fds);
}
for (int i = 0; i < fds; i++) {
if (events[i].data.fd == listenfd) {
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(struct sockaddr_in);
int connfd = accept(listenfd, (sockaddr*)&cliaddr, (socklen_t*)&clilen);
if (connfd > 0) {
printf("new connection from %s : %d, accept socket fd: %d \n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), connfd);
}
else {
printf("accept error, connfd : %d, errno : %d \n", connfd, errno);
}
fcntl(connfd, F_SETFL, O_NONBLOCK);
ev.data.fd = connfd;
ev.events = EPOLLIN | EPOLLET;
if (-1 == epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev)) {
printf("epoll_ctl error, errno : %d \n", errno);
}
}
else if (events[i].events & EPOLLIN) {
int sockfd;
if ((sockfd =events[i].data.fd) < 0) {
printf("EPOLLIN socket fd < 0 error \n");
continue;
}
char szLine[MAX_LINE + 1] ;
int readLen = 0;
bzero(szLine, MAX_LINE + 1);
if ((readLen = read(sockfd, szLine, MAX_LINE)) < 0) {
printf("readLen is %d, errno is %d \n", readLen, errno);
if (errno == ECONNRESET) {
printf("ECONNRESET closed socket fd : %d \n", events[i].data.fd);
close(sockfd);
}
}
else if (readLen == 0) {
printf("read 0 closed socket fd : %d \n", events[i].data.fd);
//epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd , NULL);
//close(sockfd);
}
else {
printf("read %d content is %s \n", readLen, szLine);
}
bzero(szLine, MAX_LINE + 1);
if ((readLen = read(sockfd, szLine, MAX_LINE)) < 0) {
printf("readLen2 is %d, errno is %d , ECONNRESET is %d \n", readLen, errno, ECONNRESET);
if (errno == ECONNRESET) {
printf("ECONNRESET2 closed socket fd : %d \n", events[i].data.fd);
close(sockfd);
}
}
else if (readLen == 0) {
printf("read2 0 closed socket fd : %d \n", events[i].data.fd);
}
else {
printf("read2 %d content is %s \n", readLen, szLine);
}
}
}
}
return 0;
}
再補充一個關於EPOLLONESHOT的選項的問題,該選項是指epoll觸發一次之後再也不會觸發,即使水平模式下沒有完全讀取緩衝區的資料,再也不會有觸發,更別提電平模式下了。
在水平模式下添加了寫事件,只要寫緩衝還有空間,那麼會一直觸發。一般來說寫緩衝區不會滿,所以導致連線的socket一直觸發寫事件,這點會不會有損效率?因為我看redis原始碼中,連線的socket一直觸發了寫事件,雖然寫的回撥函式會判斷沒有要寫的資料,但是這仍然會空轉cpu。這是我學習redis原始碼想到的一個問題,不知大家怎麼看。
在recv讀取資料時,指定要讀取的大小大於緩衝區資料大小時,即使是阻塞socket也會返回。如果想要讀取指定大小資料才讓返回,recv函式必須加一個標誌MSG_WAITALL。
還有就是epoll_wait函式的阻塞與在其佇列中socket是否阻塞沒有關係,即新增在epoll中的socket全為阻塞,epoll_wait也不會阻塞,除非有事件觸發或timeout。