epoll函数的原理和select函数类似,但是select是创建了一个文件描述符表,而epoll函数是创建了一个树用来存放文件描述符和需要检测的状态,并且在返回时不仅仅返回需要处理的文件描述符个数,还可以返回所有的文件描述符。
epoll接口总共3个:
int epoll_create(int size);//该函数生成一个专用的文件描述符,也就是epoll的根节点
int size:epoll树能存储的最大描述符个数,如果实际应用中超过可以自动扩大,所以这个参数意义不大。
返回值是根节点的文件描述符epfd。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//该函数对epoll树进行添加,删除,更改操作
int epfd:就是create创建的根节点
int op:包括以下三个宏
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;(使用删除宏,最后一个参数event可以填NULL)
int fd:要处理的文件描述符
struct epoll_event *event:就是将这个结构体挂到epoll树上
struct epoll_event该结构体就是构成epoll的基本单元
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;//联合体,就是这几个参数公用一块内存,每次定义只能定义联合体中的一个成员,常用fd
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//events可以是以下几个宏的集合:
//EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭),表示有数据在读缓冲区;
//EPOLLOUT:表示对应的文件描述符可以写,表示写数据缓冲区数据还没有满,可以写入;
//EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
//EPOLLERR:表示对应的文件描述符发生错误;
//EPOLLHUP:表示对应的文件描述符被挂断;
//EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
//EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到epoll中
//events和select函数中的fd_set *readfds, fd_set *writefds, fd_set *exceptfds三个函数作用类似
事件宏更通俗的解释如下:
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误; EPOLLHUP: 表示对应的文件描述符被挂断;注意:这个事件是默认会关注的,不需要你特意加入epoll队列,所有加入epoll的文件描述符都会关注这个事件。一般这个事件发生在管道或者socket通信,表示对端关闭了自己这边,比如客户端使用了shutdown(sockfd, SHUT_WR),关闭自己的写端,就会触发服务端的EPOLLHUP事件。
EPOLLET: 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
返回值:成功操作就返回0,错误返回-1
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//等待事件的产生,类似于select函数
int epfd:就是create创建的根节点
struct epoll_event * events:这边应该传入epoll_event结构体的数组,用来传出需要处理的文件描述符
int maxevents:告诉内核events数组的大小
int timeout:超时时间(单位毫秒,0会立即返回,-1是阻塞等待,大于0是阻塞该数值时间)。
个人理解
epoll总共三个函数如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll的底层是由红黑树实现的,所以epoll_create函数是创造一个树,并返回根节点,也就是efd文件描述符。epoll_ctl是对这棵树的操作,可以实现增加节点,删除节点,修改节点等操作。而epoll_wait则是监视树上的节点,是否发生对应需要监视的事件,如果发生了,就返回。
这里重点讲解一下增加节点的过程。当想要增加节点时,使用epoll_ctl函数,并且op填写EPOLL_CTL_ADD宏,fd就是要监视的文件描述符,最重要的是epoll_event结构体,注意一个文件描述符对应一个epoll_event结构体。这个结构体有两个成员,events表示需要epoll监视的事件,并且可以多选。意思就是如果当对应的文件描述符发生events中的事件时,epoll_wait会解除阻塞,并返回该文件描述符对应的epoll_event结构体。epoll_event结构体的另一个成员data是一个联合体,这个data对于epoll函数并没有作用,只是当epoll_wait得到活跃文件描述符时,就会得到文件描述符对应的epoll_event,根据data做相关操作。
现假设已经添加一个节点,然后使用epoll_wait后成功返回了,这时,epoll_event数组中就是一系列监听到的活跃的文件描述符对应的epoll_event结构体。活跃的文件描述符就是指在增加节点时,让epoll关注的事件发生了。在这些结构体中,events成员不再是当时增加节点的events了,而是本次epoll_wait被监听到的事件。
注意:写事件和读事件触发的条件
水平触发
1. 对于读操作
只要内核缓冲区内容不为空,LT模式返回读就绪。
2. 对于写操作
只要内核缓冲区还不满,LT模式会返回写就绪。
边缘触发
1. 对于读操作
(1)当内核缓冲区由不可读变为可读的时候,即内核缓冲区由空变为不空的时候。
(2)当有新数据到达时,即内核缓冲区中的待读数据变多的时候。
(3)当内核缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。
2. 对于写操作
(1)当内核缓冲区由不可写变为可写时。
(2)当有旧数据被发送走,即内核缓冲区中的内容变少的时候。
(3)当内核缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h>
using namespace std;
int main(int argc, const char *argv[])
{
if (argc<2)
{
cout<<"input :./a.out port";
exit(1);
}
int sfd, cfd;
int port=atoi(argv[1]);
char buf[256];
int n;
struct sockaddr_in addr_server;
addr_server.sin_port=htons(port);
addr_server.sin_addr.s_addr=htonl(INADDR_ANY);
addr_server.sin_family=AF_INET;
struct sockaddr_in addr_client;
socklen_t addrlen=sizeof(addr_client);
sfd=socket(AF_INET, SOCK_STREAM, 0);
bind(sfd, (struct sockaddr*) &addr_server, sizeof(addr_server));
listen(sfd, 20);
cout<<"start to accept......\n";
struct epoll_event all[300];//创建epoll_event数组,作为epoll_wait的参数
struct epoll_event ev;//作为epoll_ctl的参数
int epfd=epoll_create(300);//创建一个根节点文件描述符是epfd,最大存放量为300的epoll树
ev.events=EPOLLIN;
ev.data.fd=sfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);//将用于接收客户端的文件描述符存入树种
while (1)
{
int sol=epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);//阻塞等待事件发生
for (int i=0; i<sol; i++)//sol是事件发生个数,具体的文件描述符存储在all数组中每个结构体的fd中
{
int fd=all[i].data.fd;
if (fd==sfd)//如果是有客户端需要加入
{
cfd=accept(sfd, (struct sockaddr*) &addr_client, &addrlen);
if (cfd==-1)
{
cout<<"accept is error!";
exit(1);
}
ev.events=EPOLLIN;
ev.data.fd=cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);//把新的文件描述符挂到树上
char ip[64];
cout<<"the client ip is"<<inet_ntop(AF_INET, &addr_client.sin_addr.s_addr, ip, sizeof(ip))<<endl;
cout<<"the client port is"<<ntohs(addr_client.sin_port)<<endl;
}
else
{
if(!all[i].events&EPOLLIN) continue;//如果不是监督读状态,就省略
int n=read(fd, buf, sizeof(buf));
if (n==0)
{
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);//有客户端断开链接了,就删去文件描述符
}else if (n==-1)
{
cout<<"read is error!";
exit(1);
}else
{
for (int i=0; i<n; i++)
buf[i]=toupper(buf[i]);
cout<<"receive: "<<buf<<endl;
write(fd, buf, n);
}
}
}
}
close(sfd);
return 0;
}
epoll的边沿非阻塞触发模式
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr);
// 创建epoll树根节点
int epfd = epoll_create(2000);
// 初始化epoll树
struct epoll_event ev;
// 设置边沿触发
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event all[2000];
while(1)
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
printf("================== epoll_wait =============\n");
// 遍历all数组中的前ret个元素
for(int i=0; i<ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd == lfd)
{
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
// 设置文件cfd为非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 将新得到的cfd挂到树上
struct epoll_event temp;
// 设置边沿触发
temp.events = EPOLLIN | EPOLLET;
temp.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息
char ip[64] = {0};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
}
else
{
// 处理已经连接的客户端发送过来的数据
if(!all[i].events & EPOLLIN)
{
continue;
}
// 读数据
char buf[5] = {0};
int len;
// 循环读数据
while( (len = recv(fd, buf, sizeof(buf), 0)) > 0 )
{
// 数据打印到终端
write(STDOUT_FILENO, buf, len);
// 发送给客户端
send(fd, buf, len, 0);
}
if(len == 0)
{
printf("客户端断开了连接\n");
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -1)
{
perror("epoll_ctl - del error");
exit(1);
}
close(fd);
}
else if(len == -1)
{
if(errno == EAGAIN)
{
printf("缓冲区数据已经读完\n");
}
else
{
printf("recv error----\n");
exit(1);
}
}
}
}
}
close(lfd);
return 0;
}
其他:
在看muduo库时,我很好奇在客户端的文件描述符被关闭以后,epoll队列中会发生什么事件,网上众说纷纭,有的说会触发EPOLLIN和EPOLLRDHUP事件,有的说会触发EPOLLIN和EPOLLRDHUP,那到底触发什么呢。用下面的例子来测试
服务端程序:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h>
using namespace std;
int main(int argc, const char *argv[])
{
if (argc<2)
{
cout<<"input :./a.out port";
exit(1);
}
int sfd, cfd;
int port=atoi(argv[1]);
char buf[256];
int n;
int count=0;
struct sockaddr_in addr_server;
addr_server.sin_port=htons(port);
addr_server.sin_addr.s_addr=htonl(INADDR_ANY);
addr_server.sin_family=AF_INET;
struct sockaddr_in addr_client;
socklen_t addrlen=sizeof(addr_client);
sfd=socket(AF_INET, SOCK_STREAM, 0);
bind(sfd, (struct sockaddr*) &addr_server, sizeof(addr_server));
listen(sfd, 20);
cout<<"start to accept......\n";
struct epoll_event all[300];//创建epoll_event数组,作为epoll_wait的参数
struct epoll_event ev;//作为epoll_ctl的参数
int epfd=epoll_create(300);//创建一个根节点文件描述符是epfd,最大存放量为300的epoll树
ev.events=EPOLLIN;
ev.data.fd=sfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);//将用于接收客户端的文件描述符存入树种
while (1)
{
count++;
int sol=epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);//阻塞等待事件发生
for (int i=0; i<sol; i++)//sol是事件发生个数,具体的文件描述符存储在all数组中每个结构体的fd中
{
int fd=all[i].data.fd;
if (fd==sfd)//如果是有客户端需要加入
{
cfd=accept(sfd, (struct sockaddr*) &addr_client, &addrlen);
if (cfd==-1)
{
cout<<"accept is error!";
exit(1);
}
ev.events=EPOLLIN|EPOLLHUP|EPOLLRDHUP;//关注这三个事件
//注释2:ev.events=EPOLLIN|EPOLLHUP;
ev.data.fd=cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);//把新的文件描述符挂到树上
char ip[64];
cout<<"the client ip is"<<inet_ntop(AF_INET, &addr_client.sin_addr.s_addr, ip, sizeof(ip))<<endl;
cout<<"the client port is"<<ntohs(addr_client.sin_port)<<endl;
}
else
{
cout<<"--------event:"<<all[i].events<<endl;
if(all[i].events&EPOLLIN) cout<<"---EPOLLIN--\n";
if(all[i].events&EPOLLHUP) cout<<"---EPOLLHUP--\n";
if(all[i].events&EPOLLRDHUP) cout<<"---EPOLLRDHUP--\n";
int n=read(fd, buf, sizeof(buf));
if (n==0)
{
cout<<"close";
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);//有客户端断开链接了,就删去文件描述符
}else if (n==-1)
{
cout<<"read is error!";
exit(1);
}else
{
for (int i=0; i<n; i++)
buf[i]=toupper(buf[i]);
cout<<"receive: "<<buf<<endl;
write(fd, buf, n);
}
}
cout<<"轮次:"<<count<<endl;
}
}
close(sfd);
return 0;
}
客户端程序:
int main(int argc, const char* argv[])
{
if (argc<2)//需要传入端口号
{
cout<<"input ./a.out port"<<endl;
exit(1);
}
int sfd;//客户端只需要一个负责读写的文字描述符就够了
int port=atoi(argv[1]);//传入的是char*类型的端口号,转换成int型
/*定义sockaddr_in结构体*/
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
//将ip转换成int型存入sockaddr_in结构体中
socklen_t addrlen;
sfd=socket(AF_INET, SOCK_STREAM, 0);
connect(sfd, (struct sockaddr*) &addr, sizeof(addr));
char buf[1024];
fgets(buf, sizeof(buf), stdin);//从终端读取字符串
write(sfd, buf, strlen(buf));
int n=read(sfd, buf, sizeof(buf));
if (n==-1)
{
cout<<"error"<<endl;
exit(1);
}
else if (n>0)
cout<<buf;
close(sfd);
//注释2:shutdown(sfd, SHUT_WR);
}
最后显示结果是:
是触发事件EPOLLIN和EPOLLRDHUP
然后网上也有传言说如果客户端使用shutdown关闭写端,就会触发EPOLLHUP事件,我使用注释2的地方重新测试了:,结果是
并没有EPOLLHUP事件发生,个人理解EPOLLHUP事件是在服务端如果出现关闭等事件,才会出现吧,EPOLLHUP事件发生的环境有待于发掘。
客户端的输入都是一样的如下图所示: