epoll 机制 (条件触发和边缘触发)
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_create函数时创建的文件描述符保存空间称为"epoll例程"。size本该决定epoll例程的大小,但Linux 2.6.8之后的内核将完全忽略size参数,内核会根据情况调整epoll例程的大小。
epoll_create返回的文件描述符主要用于区分epoll例程。
epoll_ctl函数
生成epoll例程后,应在其内部注册监视对象文件描述符,此时使用epoll_ctl函数。
例:
1
epoll_ctl(A,EPOLL_CTL_ADD,B,C);
此语句的含义:epoll例程A中注册文件描述符B(EPOLL_CTL_ADD),主要目的是监视参数C中的事件。
含义: 从epoll例程A中删除文件描述符B
第二个参数传递的常量及含义:
第四个参数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_wait函数
该函数调用方式如下:第二个参数所指缓冲需要动态分配
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);
}
运行结果:
条件触发和边缘触发
条件触发和边缘触发的区别在于发生事件的时间点
观察如下对话理解条件触发事件的特点:
儿子从收到压岁钱开始一直向妈妈报告,这就是条件触发的原理。
将儿子的钱包比作输入缓冲,压岁钱换成输入数据,儿子的报告换成事件,则可以发现条件触发的特性:
“条件触发方式中,只要输入缓冲有数据就会一直通知该事件。”
条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册。
边缘触发:
从上述对话可以看出,边缘触发中输入缓冲收到数据时仅注册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
echo_client One:
echo_client Two:
从运行结果看出,每当收到客户端数据时,都会注册该事件,因此多次调用epoll_wait函数
将上例改成边缘触发方式:
只要将66行的event.events = EPOLLIN 改为 event.events = EPOLLIN | EPOLLET;
边缘触发的服务器端实现中比知的两点
--通过errno变量验证错误原因
--为了完成非阻塞I/O,更改套接字特性。
errno提供发生错误的额外信息。随后严时errno的使用方法。
套接字改为非阻塞方式的方法:
若希望套接字改为非阻塞,需要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
运行结果:echo_client.c
上述运行结果中需要注意的是,客户端发送消息次数和服务器端epoll_wait函数调用次数。客户端从请求连接到断开连接发送4次数据。服务器端也产生4个事件。
条件触发和边缘触发的优劣
边缘触发方式:可以分离接收数据和处理数据的时间点!
上一篇: Flume 的安装与使用
下一篇: 鱼类有哪些?品种可以说是很多了