《TCP/IP网络编程》第17章

epoll

select速度慢的原因

  • select函数遍历所有文件描述符
  • 每次都需要向select函数传递监视对象信息

每次循环遍历所有监视对象,找出发生变化的文件描述符。

每次调用select函数时都需要向操作系统传递监视对象信息,实际上select函数是监视套接字变化的函数,而套接字由操作系统管理。

通过“仅向操作系统传递一次监视对象,监视范围或内容发生变化时只通知发生变化的事项。”的方式可以弥补select函数的缺点。

需要操作系统的支持,Linux是epoll,Windows是IOCP。

select优点

  • 服务器端接入者少
  • 程序具有兼容性(多数操作系统都支持select函数)

epoll相关函数和结构体

epoll从Linux2.5.44内核开始引入。

cat /proc/sys/kernel/osrelease

epoll函数优点:

  1. 无需循环以监视所有文件描述符的状态变化
  2. 调用对应select函数的epoll_wait函数时无需每次传递监视对象信息
  • epoll_event
    epoll_event结构体将发生变化(事件)的文件描述符单独集中在一起。
struct epoll_event {
	__uint32_t events;
	epoll_data_t data;
}
typedef union epoll_data {
	void *ptr;
	int fd;
	__uint32_t u32;
	__uint64_t u64;
}
  • epoll_create
    epoll方式下,操作系统负责保存监视对象文件描述符,因此需要使用epoll_create向操作系统请求创建保存epoll文件描述符的空间。
#include <sys/epoll.h>

//size是向操作系统提供建议值
//成功返回epoll例程文件描述符,失败-1
//Linux2.6.8后size无效,系统动态调整epoll例程大小
int epoll_create(int size);
  • epoll_ctl
    通过epoll_ctl请求操作系统添加和删除监视对象文件描述符
#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll(A, EPOLL_CTL_ADD, B, C);
epoll例程A注册文件描述符B,监视C中事件。

epoll(A, EPOLL_CTL_ADD, B, NULL);
epoll例程A删除文件描述符B。

  1. epfd,epoll例程文件描述符。

  2. op用于指定监视对象的添加、删除、更改等操作。
    EPOLL_CTL_ADD:注册文件描述符
    EPOLL_CTL_DEL:删除文件描述符(次数事件为NULL)
    EPOLL_CTL_MOD:更改文件描述符关注事件

  3. event,关注的事件

struct epoll_event event;

event.events=EPOLLIN; //位或运算,多个事件
event.data.fd=sockfd;
epoll(epfd, EPOLL_CTL_ADD, sockfd, &event);

EPOLLIN:需要读取数据
EPOLLOUT:输出缓冲为空,可以立即发送数据
EPOLLPRI:收到OOB数据
EPOLLRDHUP:断开连接或半关闭,边缘触发有用
EPOLLERR:发生错误
EPOLLET:边缘触发得到事件通知
EPOLLONESHOT:发生一次事件后,相应文件描述符不在收到事件通知,需要EPOLL_CTL_MOD再次设置事件

  • epoll_wait
    类似select,等待文件描述符发生变化
#include <sys/epoll.h>

//成功返回发生事件的文件描述符数,失败-1
//timeout,1/1000秒,-1阻塞等待
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int event_cnt;
struct epoll_event *events;

events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //EPOLL_SIZE宏常量
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);

epoll回声服务器端

echo_epollserver.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;
	int clnt_sock;

	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	socklen_t addr_size;

	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);
	if(serv_sock==-1)
		error_handling("socket() error");

	addr_size = sizeof(struct sockaddr_in);
	
	memset(&serv_addr, 0, serv_addr_size);
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*)&serv_addr, serv_addr_size)==-1)
		error_handling("bind() error");
		
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	epfd=epoll_create(EPOLL_SIZE);
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

	event.events=EPOLLIN;//读取事件
	eventdata.fd=serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while(1) {
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if(event_cnt==-1) {
			puts("epoll_wait() error!");
			break;
		}

		for(i=0; i<event_cnt; i++) {
			if(ep_events[i].data.fd==serv_sock) {
				clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size);
				event.events=EPOLLIN;//读取事件
				eventdata.fd=clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %dn", clnt_sock);
			} else {
				str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
				if(str_len==0) {
					epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
					close(ep_events[i].data.fd);
					printf("closed client: %dn", ep_events[i].data.fd);
				} else {
					write(ep_events[i].data.fd, buf, str_len);
				}
			}
		}	
	}
	
	close(serv_sock);
	close(epfd);
	return 0;
}


void error_handling(char *message) {
	fputs(message, stderr);
	fputc('n', stderr);
	exit(1);
}
gcc echo_epollserver.c -o epollserver
./epollserver 9190

条件触发和边缘触发

条件触发(Level Trigger)和边缘触发(Edge Trigger)的区别在于发生事件的时间点。

条件触发特性,条件触发方式中,只要输入缓冲有数据就会一直通知该事件,多次注册事件。

边缘触发中输入缓冲收到数据时仅注册1次该事件。

条件触发,满足条件就触发(比如输入缓冲有数据)。
边缘触发,状态变化才触发(比如无缓冲状态变为有缓冲状态)。

条件触发的事件特性

epoll默认以条件触发方式工作。

echo_EPLTserver.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 4
#define EPOLL_SIZE 50
void error_handling(char *message);

int main(int argc, char *argv[]) {
	int serv_sock;
	int clnt_sock;

	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	socklen_t addr_size;

	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);
	if(serv_sock==-1)
		error_handling("socket() error");

	addr_size = sizeof(struct sockaddr_in);
	
	memset(&serv_addr, 0, serv_addr_size);
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*)&serv_addr, serv_addr_size)==-1)
		error_handling("bind() error");
		
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	epfd=epoll_create(EPOLL_SIZE);
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

	event.events=EPOLLIN;//读取事件
	eventdata.fd=serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while(1) {
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		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) {
				clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size);
				event.events=EPOLLIN;//读取事件
				eventdata.fd=clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %dn", clnt_sock);
			} else {
				str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
				if(str_len==0) {
					epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
					close(ep_events[i].data.fd);
					printf("closed client: %dn", ep_events[i].data.fd);
				} else {
					write(ep_events[i].data.fd, buf, str_len);
				}
			}
		}	
	}
	
	close(serv_sock);
	close(epfd);
	return 0;
}


void error_handling(char *message) {
	fputs(message, stderr);
	fputc('n', stderr);
	exit(1);
}
gcc echo_EPLTserver.c -o EPLTserver
./EPLTserver 9190

多次输出return epoll_wait


clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size);
后面
event.events=EPOLLIN;
修改为
event.events=EPOLLIN|EPOLLET;
仅输出一次return epoll_wait

select以条件触发方式工作。

边缘触发服务器必知两点

  • errno变量验证错误原因
  • 套接字特性设为非阻塞(Non-blocking)I/O

read函数发现缓冲中无数据可读时返回-1,同时在errno中保存EAGAIN常量。

#include <fcntl.h>
int fcntl(int filedes, int cmd, ...);
int flag = fcntl(fd, F_GETFL, 0);//获取文件描述符属性

fcntl(fd, F_SETFL, flag|O_NONBLOCK);//设置文件描述符属性

边缘触发服务器

边缘触发方式中,仅注册一次事件。
一旦发生输入事件,就应该读取输入缓冲中全部数据。

read返回-1,同时在errno值为EAGAIN,说明无数据可读。

边缘触发方式中,阻塞方式工作的read&write函数可能引起服务器端的长时间停顿,边缘触发方式中一定要采用非阻塞read&write函数。

echo_EPETserver.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>
#include <fcntl.h>
#include <errno.h>

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

int main(int argc, char *argv[]) {
	int serv_sock;
	int clnt_sock;

	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	socklen_t addr_size;

	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);
	if(serv_sock==-1)
		error_handling("socket() error");

	addr_size = sizeof(struct sockaddr_in);
	
	memset(&serv_addr, 0, serv_addr_size);
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*)&serv_addr, serv_addr_size)==-1)
		error_handling("bind() error");
		
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	epfd=epoll_create(EPOLL_SIZE);
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

	setnonblockingmode(serv_sock);
	event.events=EPOLLIN;//读取事件
	eventdata.fd=serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while(1) {
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		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) {
				clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size);
				setnonblockingmode(clnt_sock);
				event.events=EPOLLIN|EPOLLET;//读取事件且边缘触发
				eventdata.fd=clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %dn", clnt_sock);
			} else {
				while(1) {
					str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
					if(str_len==0) {
						epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
						close(ep_events[i].data.fd);
						printf("closed client: %dn", 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);
					}
				}
			}
		}	
	}
	
	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);
}
gcc echo_EPETserver.c -o EPETserver
./EPETserver 9190

条件触发与边缘触发优劣

边缘触发相对于条件触发的优点:可以分离接收数据和处理数据的时间点。

从实现模型的角度看,边缘触发更有可能带来高性能,但不能简单地认为只要使用边缘触发就一定能提高速度。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>