欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

epoll 机制 (条件触发和边缘触发)

程序员文章站 2022-06-14 11:27:46
...

epoll理解及应用

基于select的I/O复用技术速度慢的原因

--调用select函数后常见的针对所有文件描述符的循环语句。

--每次调用select函数时都需要向该函数传递监视对象信息。(即每次调用select函数时向操作系统传递监视对象信息)

 

select的优点

如果需要满足如下两个条件,则可以选用select模型:

--服务器端接入者少

--程序应具有兼容性

 

epoll只在Linux下提供支持。

 

实现epoll时必要的函数和结构体

epoll的优点:

--无需编写以监视状态变化为目的的针对所有文件描述符的循环语句。

--调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息。

 

epoll服务器端实现需要的3个函数:

--epoll_create: 创建保存epoll文件描述符的空间

--epoll_ctl: 向空间注册并注销文件描述符

--epoll_wait: 与select函数类似,等待文件描述符发生变化

 

select方式中用fd_set变量负责保存监视对象文件描述符,而epoll方式由操作系统保存,因此需要向操作系统请求创建保存文件描述符的空间,使用epoll_create函数。

 

select方式通过fd_set变量监视对象的状态变化,而epoll方式中通过如下结构体epoll_event将发生变化的文件描述符单独集中到一起:

struct epoll_event
{
    __unit32_t events;
    epoll_data_t data;
}

typedef union epoll_data
{
    void *ptr;
    int fd;
    __unit32_t u32;
    _-unit64_t u64;
}epoll_data_t;

声明足够大的epoll_event结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符信息将被填入该数组。

 

epoll_create函数

epoll 机制 (条件触发和边缘触发)

调用epoll_create函数时创建的文件描述符保存空间称为"epoll例程"。size本该决定epoll例程的大小,但Linux 2.6.8之后的内核将完全忽略size参数,内核会根据情况调整epoll例程的大小。

epoll_create返回的文件描述符主要用于区分epoll例程。

 

epoll_ctl函数

生成epoll例程后,应在其内部注册监视对象文件描述符,此时使用epoll_ctl函数。

epoll 机制 (条件触发和边缘触发)

例:

1
epoll_ctl(A,EPOLL_CTL_ADD,B,C);

此语句的含义:epoll例程A中注册文件描述符B(EPOLL_CTL_ADD),主要目的是监视参数C中的事件。

 


1
epoll_ctl(A,EPOLL_CTL_DEL,B,NULL);

含义: 从epoll例程A中删除文件描述符B

 

第二个参数传递的常量及含义:

epoll 机制 (条件触发和边缘触发)

 

第四个参数epoll_event结构体指针:

epoll_event结构体用于保存发生事件的文件描述符集合。但也可以在epoll例程中注册文件描述符时,用于注册关注的事件。

struct epoll_event event;
...
event.events = EPOLLIN;     //发生需要读取数据的情况
event.data   = sockfd; 
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
...

上述代码将sockfd注册到epoll例程epfd中,并在需要读取数据的情况下产生相应事件。

 

epoll_event的成员events中可以保存的常量及所指的事件类型:

epoll 机制 (条件触发和边缘触发)

 

epoll_wait函数

epoll 机制 (条件触发和边缘触发)


该函数调用方式如下:第二个参数所指缓冲需要动态分配

int event_cnt;
struct epoll_event * ep_events;
.....
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);          //EPOLL_SIZE是常量
.....
event_cnt = epoll_wait(epfd,ep_events,EOPOLL_SZIE,-1);
.....

调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。

 

 

基于epoll的回声服务器端

echo_epollserv.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t adr_sz;
	int str_len, i;
	char buf[BUF_SIZE];
	
	struct epoll_event *ep_events;
	struct epoll_event event;
	int epfd, event_cnt;

	if (argc != 2)
	{
		printf("Usage: %s <port> \n",argv[0]);
		exit(1);
	}

	serv_sock = socket(PF_INET,SOCK_STREAM,0);
	memset(&serv_adr,0,sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_adr.sin_port = htons(atoi(argv[1]));

	if (bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
		error_handling("bind() error!");
	if (listen(serv_sock,5) == -1)
		error_handling("listen() error!");

	epfd = epoll_create(EPOLL_SIZE);		//创建epoll例程文件描述符
	ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

	event.events = EPOLLIN;
	event.data.fd = serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);	//在epfd内注册监视serv__sock,监视其读取数据的情况

	while(1)
	{
		event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);	//成功返回发生事件的文件描述符数。失败返回-1,发生事件的文件描述符保存到events所指的结构体中
		if (event_cnt == -1)
		{
			puts("epoll_wait() error!");
			break;
		}

		for (i = 0; i < event_cnt; i++)		
		{
			if (ep_events[i].data.fd == serv_sock)	//服务器端套接字接收连接
			{
				adr_sz = sizeof(clnt_adr);
				clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);
				event.events = EPOLLIN;
				event.data.fd = clnt_sock;
				epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,&event);	//注册监视连接客户端套接字clnt_sock,监视其读取数据的情况 
				printf("connected client: %d \n",clnt_sock);
			}
			else			//连接客户端套接字读取数据
			{
				str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
				if (str_len == 0)		//close request
				{
					epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
					close(ep_events[i].data.fd);
					printf("closed client: %d \n",ep_events[i].data.fd);
				}
				else 
				{
					write(ep_events[i].data.fd, buf, str_len);		//echo!
				}
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

void error_handling(char *message)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

运行结果:

epoll 机制 (条件触发和边缘触发)

 

 

条件触发和边缘触发

条件触发和边缘触发的区别在于发生事件的时间点

观察如下对话理解条件触发事件的特点:

epoll 机制 (条件触发和边缘触发)

 

 

 

儿子从收到压岁钱开始一直向妈妈报告,这就是条件触发的原理。

将儿子的钱包比作输入缓冲,压岁钱换成输入数据,儿子的报告换成事件,则可以发现条件触发的特性:

                    “条件触发方式中,只要输入缓冲有数据就会一直通知该事件。”

条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册。

 

边缘触发:

epoll 机制 (条件触发和边缘触发)

从上述对话可以看出,边缘触发中输入缓冲收到数据时仅注册1次该事件。

 

掌握条件触发的事件特性

下面通过示例了解条件触发的事件注册方式 

/* 条件触发 */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>

#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t adr_sz;
	int str_len, i;
	char buf[BUF_SIZE];
	
	struct epoll_event *ep_events;
	struct epoll_event event;
	int epfd, event_cnt;

	if (argc != 2)
	{
		printf("Usage: %s <port> \n",argv[0]);
		exit(1);
	}

	serv_sock = socket(PF_INET,SOCK_STREAM,0);
	memset(&serv_adr,0,sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_adr.sin_port = htons(atoi(argv[1]));

	if (bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
		error_handling("bind() error!");
	if (listen(serv_sock,5) == -1)
		error_handling("listen() error!");

	epfd = epoll_create(EPOLL_SIZE);		//创建epoll例程文件描述符
	ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

	event.events = EPOLLIN;
	event.data.fd = serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);	//在epfd内注册监视serv__sock,监视其读取数据的情况

	while(1)
	{
		event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);	//成功返回发生事件的文件描述符数。失败返回-1.发生事件的文件描述符保存到events所指的结构体中
		if (event_cnt == -1)
		{
			puts("epoll_wait() error!");
			break;
		}

		puts("return epoll_wait");			//验证epoll_wait调用次数
		for (i = 0; i < event_cnt; i++)		
		{
			if (ep_events[i].data.fd == serv_sock)	//服务器端套接字接收连接
			{
				adr_sz = sizeof(clnt_adr);
				clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);
				event.events = EPOLLIN;
				event.data.fd = clnt_sock;
				epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,&event);	//注册监视连接客户端套接字clnt_sock,监视其读取数据的情况 
				printf("connected client: %d \n",clnt_sock);
			}
			else			//连接客户端套接字读取数据
			{
				str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
				if (str_len == 0)		//close request
				{
					epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
					close(ep_events[i].data.fd);
					printf("closed client: %d \n",ep_events[i].data.fd);
				}
				else 
				{
					write(ep_events[i].data.fd, buf, str_len);		//echo!
				}
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

void error_handling(char *message)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

与echo_epollserv.c之间的差异如下:
--将调用read函数使用的缓冲大小缩减为4个字节  (第10行)

--插入验证epoll_wait函数调用次数的语句  (59行)

 

减少缓冲大小是为了阻止服务器端一次性读取完接收的数据。调用read函数后,输入缓冲中仍有数据需要读取。因此会注册新的事件并从epoll_wait函数返回时将循环输出"return epoll_wait"字符串。

 

运行结果:

echo_EPLTserv.c

 epoll 机制 (条件触发和边缘触发)

 

echo_client One:

 epoll 机制 (条件触发和边缘触发)

 

echo_client Two:

 epoll 机制 (条件触发和边缘触发)

 

从运行结果看出,每当收到客户端数据时,都会注册该事件,因此多次调用epoll_wait函数

 

将上例改成边缘触发方式:

只要将66行的event.events = EPOLLIN 改为 event.events = EPOLLIN | EPOLLET;

 

边缘触发的服务器端实现中比知的两点

--通过errno变量验证错误原因

--为了完成非阻塞I/O,更改套接字特性。

 

errno提供发生错误的额外信息。随后严时errno的使用方法。

 

套接字改为非阻塞方式的方法:

epoll 机制 (条件触发和边缘触发)

 

 若希望套接字改为非阻塞,需要2条语句:

1
2
intflag=fcntl(fd,F_GETFL,0);
fcntl(fd,F_SETFL,flag|O_NONBLOCK);

 通过第一条语句获取值属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。

 

实现边缘触发的回声服务器端

为何需要errno确认错误原因?
因为边缘触发方式中,接收数据时仅注册1次该事件。一旦发生输入相关事件,就应该读取输入缓冲中的全部数据。因此需要验证输入缓冲是否为空。read函数返回-1,变量errno中的值为EAGAIN时,说明没有数据可读。

非阻塞模式可以防止read & write函数造成服务器端的长时间停顿。

 

 边缘触发的回声服务器端: 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<errno.h>
#define BUF_SIZE 4			//验证边缘触发的工作方式,将缓冲设置为4字节
#define EPOLL_SIZE 50
void error_handling(char *message);
void setnonblockingmode(int fd);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t adr_sz;
	int str_len, i;
	char buf[BUF_SIZE];
	
	struct epoll_event *ep_events;
	struct epoll_event event;
	int epfd, event_cnt;

	if (argc != 2)
	{
		printf("Usage: %s <port> \n",argv[0]);
		exit(1);
	}

	serv_sock = socket(PF_INET,SOCK_STREAM,0);
	memset(&serv_adr,0,sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_adr.sin_port = htons(atoi(argv[1]));

	if (bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
		error_handling("bind() error!");
	if (listen(serv_sock,5) == -1)
		error_handling("listen() error!");

	epfd = epoll_create(EPOLL_SIZE);		//创建epoll例程文件描述符
	ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

	setnonblockingmode(serv_sock);
	event.events = EPOLLIN;
	event.data.fd = serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);	//在epfd内注册监视serv__sock,监视其读取数据的情况

	while(1)
	{
		event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);	//成功返回发生事件的文件描述符数。失败返回-1.发生事件的文件描述符保存到events所指的结构体中
		if (event_cnt == -1)
		{
			puts("epoll_wait() error!");
			break;
		}
		
		puts("return epoll_wait");
		for (i = 0; i < event_cnt; i++)		
		{
			if (ep_events[i].data.fd == serv_sock)	//服务器端套接字接收连接
			{
				adr_sz = sizeof(clnt_adr);
				clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);
				setnonblockingmode(clnt_sock);			//改为非阻塞
				event.events = EPOLLIN | EPOLLET;		//套接字事件注册方式改为边缘触发
				event.data.fd = clnt_sock;
				epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,&event);	//注册监视连接客户端套接字clnt_sock,监视其读取数据的情况 
				printf("connected client: %d \n",clnt_sock);
			}
			else			//连接客户端套接字读取数据
			{
				while(1)	//边缘触发发生事件时,需要读取输入缓冲中的所有数据,因此需要循环调用read函数
				{
					str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
					if (str_len == 0)		//close request
					{
						epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
						close(ep_events[i].data.fd);
						printf("closed client: %d \n",ep_events[i].data.fd);
						break;
					}
					else if(str_len < 0) {
						if (errno == EAGAIN)
							break;
					}
					else 
					{
						write(ep_events[i].data.fd, buf, str_len);	//echo!
					}
				}
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

void setnonblockingmode(int fd)
{
	int flag = fcntl(fd,F_GETFL,0);
	fcntl(fd,F_SETFL,flag|O_NONBLOCK);
}

void error_handling(char *message)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

 运行结果:echo_EPETserv.c

epoll 机制 (条件触发和边缘触发)

运行结果:echo_client.c

epoll 机制 (条件触发和边缘触发) 

 

上述运行结果中需要注意的是,客户端发送消息次数和服务器端epoll_wait函数调用次数。客户端从请求连接到断开连接发送4次数据。服务器端也产生4个事件。

 

条件触发和边缘触发的优劣 

 边缘触发方式:可以分离接收数据和处理数据的时间点!

 epoll 机制 (条件触发和边缘触发)