虽然poll 模型没有最大数量限制,但是监视的文件描述符数量过多会导致效率降低。因此 epoll 可以看作是为了处理大量句柄而做了改进的poll。与select、poll模型一样,epoll只负责等待;不一样的是,epoll模型的实现需要依赖三个接口函数。
目录
一、认识相关接口
1、epoll_create
epoll_create的作用是创建一个 epoll模型,这个模型在被创建的时候,内部会新增三个东西:一棵红黑树、回调策略、就绪队列。红黑树用于管理被监视的文件描述符及其对应事件;回调策略是事件就绪时的相关策略;就绪队列用于告知用户哪些文件描述符的哪些事件就绪。
相关内容可以参考:epoll模型的作用过程
epoll_create系统调用的声明如下:
epoll_create的参数size早在2.6.8版本以后,就被无视了,因此在使用的时候,可以填任意整型。
返回值:返回的是一个文件描述符 epfd
2、epoll_ctl
epoll_ctl 的作用是 新增 / 修改 / 移除 某个被关注的文件描述符。(只要调用一次,内核就永远记住了,下一次循环无需重新设置)
epoll_ctl 的函数声明如下:
(1) 第一个参数epfd
epoll模型可以有很多个,选择你要操作的epoll模型,即epoll_create的返回值
(2) 第二个参数op
代表你要对上述文件描述符作什么操作(新增/修改/移除)。可选值如下:
宏 | 宏解析 |
EPOLL_CTL_ADD | 注册新的fd到epfd中 |
EPOLL_CTL_MOD | 修改某个fd的监听事件 |
EPOLL_CTL_DEL | 从epfd中删除一个fd(取消对某个fd的监视) |
(3) 第三个参数fd
代表你要对哪个文件描述符进行 新增/修改/移除 监视的操作。
(4) 第四个参数 event
这是一个输入型参数,代表需要添加 / 修改的监听事件。下面我们需要认识两个内容,第一个是第四个参数的结构体类型,第二个是如何添加监听事件。
结构体类型
结构体类型如下:
首先是events成员,这个成员用于告诉我们有哪些事件就绪了,但是需要我们自己去判断,判断方法放在epoll_wait中介绍;
其次是data成员,现在我们知道有哪些事件就绪了,但是到底是哪个文件描述符的事件就绪了呢?因此我们可以通过data.fd来获取文件描述符。
添加监听事件
无论是新增还是修改监听事件,这些事件站在C语言的角度来看都是宏,可以通过按位与的方式来添加事件。
宏 | 描述 |
EPOLLIN | 数据(普通数据或者优先数据) 可读 |
EPOLLOUT | 数据(普通数据或者优先数据) 可写 |
EPOLLPRI | 文件描述符有紧急数据可读(有带外数据) |
EPOLLERR | 文件描述符发生错误 |
EPOLLHUP | 文件描述符被挂断(对端关闭连接) |
EPOLLET | 将EPOLL设为边缘触发(epoll默认是水平触发模式) |
EPOLLONESHOT | 只监听一次事件,监听完这次以后,如果需要再次监听需要重新添加 |
比如我们要添加事件:
struct epoll_event epevent;
epevent.events |= EPOLLIN; //添加监听读事件
//epevent.events = EPOLLIN | EPOLLOUT; //既监听读事件又监听写事件
(5) 返回值
成功返回0,失败返回-1 。
3、epoll_wait
epoll_wait 的作用是告知你有文件已经就绪(返还就绪数组的首地址),但是不会具体告知你哪些事件就绪。函数声明如下:
(1) 参数和返回值解析
第一个参数epfd:epoll模型的文件描述符,即epoll_create的返回值
第二个参数events:这是一个输出型参数,我们填入一个空数组的首地址,这个函数会将xx结构体拷贝到这个数组,该结构体包含文件描述符以及文件描述符上的就绪事件(结构体类型参考上面的epoll_ctl)
第三个参数maxevents:输入型参数,限制每次最多可以返回多少个结构体,即接收返回事件的数组的最大容量。如果有较多文件描述符事件就绪,超过了我们限制的数目,超出部分是否会被舍弃呢??答案是不会!拿完一次,如果没拿完,会通过循环再拿第二次。
第四个参数timeout:代表超时时间,单位是毫秒。
- timeout = -1代表永久阻塞,即在有时间就绪之前epoll_wait函数就不返回;
- timeout =0代表非阻塞等待;
- timeout > 0 代表先阻塞等待timeout毫秒,然后变为非阻塞等待,此时epoll_wait会返回,并继续执行这之后的代码。
返回值:有三种情况
- 返回值 < 0 ,代表epoll_wait调用出错;
- 返回值 = 0,代表等待超时;
- 返回值 > 0,代表有事件就绪的文件描述符数目;
(2) 判断事件是否就绪
我们可以通过 第二个参数events来获取哪个文件描述符上的哪些事件就绪。
#define NUM 64
struct epoll_event revents[NUM]; //就绪数组
int num = epoll_wait(epfd, revents, NUM, 1000);
if(num > 0){
for (size_t i = 0; i < num; i++) //以轮询的方式获取到就绪事件以及对应的文件描述符
{
if(revents[i].events & EPOLLIN) //检查读事件是否就绪
{
if(revents[i].data.fd == listen_fd)
{
//处理链接事件
}
else
{
//处理正常读取事件
}
}
if(revents[i].events & EPOLLOUT) //检查写事件是否就绪
{
}
}
}
二、epoll模型实现
下面只给出了一个大致的框架,处理工作请自行填充
int main()
{
//创建套接字
int listen_fd = TcpSocket::CreateSocket();
//绑定端口号
TcpSocket::Bind(listen_fd, 8990);
//设为监听套接字
TcpSocket::Listen(listen_fd);
// 1. 创建epoll模型
int epfd = epoll_create(128);
// 2. 添加listen_fd,并添加读事件监听
struct epoll_event epev;
epev.events |= EPOLLIN;
epev.data.fd = listen_fd; //将读事件与 listen_fd绑定,用于后面的revents判断哪个文件描述符上有事件就绪
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &epev);
// 3. 事件循环检测是否就绪
volatile bool quit = false;
#define NUM 64
struct epoll_event revents[NUM];
while (!quit)
{
int timeout = 1000;
// 4. 等待事件就绪
int num = epoll_wait(epfd, revents, NUM, timeout);
if (num < 0)
{
std::cerr << "等待出错..." << std::endl;
}
else if (num == 0)
{
std::cout << "等待时间超时..." << std::endl;
}
else
{
std::cout << "有事件就绪了" << std::endl;
for (size_t i = 0; i < num; i++)
{
if (revents[i].events & EPOLLIN) //检查读事件是否就绪
{
if (revents[i].data.fd == listen_fd)
{
//处理链接事件
int accept_fd = TcpSocket::Accept(listen_fd);
if (accept_fd > 0)
{
//将新的文件描述符托管给epoll,此时需要调用epoll_ctl
struct epoll_event ev;
ev.events |= EPOLLIN;
ev.data.fd = accept_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, accept_fd, &ev);
}
}
else
{
//处理正常读取事件
char buffer[1024];
int s = recv(revents[i].data.fd,buffer,sizeof(buffer)-1,0);
if(s < 0)
{
std::cerr<<"读取出错"<<std::endl;
//关闭文件描述符(套接字)
close(revents[i].data.fd);
//取消对该文件描述符的关注
epoll(epfd,EPOLL_CTL_DEL,sock,nullptr);
}
else if(s == 0){
std::cout<<"对端关闭连接"<<std::endl;
//关闭文件描述符(套接字)
close(revents[i].data.fd);
//取消对该文件描述符的关注
epoll(epfd,EPOLL_CTL_DEL,sock,nullptr);
}
else
{
//开始处理数据
}
}
}
if (revents[i].events & EPOLLOUT) //检查写事件是否就绪
{
}
}
}
}
return 0;
}