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源码想到的一个问题,不知大家怎么看


猜你喜欢

转载自blog.csdn.net/zxm342698145/article/details/80524331
今日推荐