1. 程式人生 > >epoll EPOLLL、EPOLLET模式與阻塞、非阻塞

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。