epoll系统调用和select以及poll是一样的,都是可以让我们的程序同时监视多个文件描述符上的事件是否就绪。
epoll在命名上比poll多了一个poll,这个e可以理解为extend,epoll就是为了同时处理大量文件描述符而改进的poll。
epoll在2.5.44内核中被引进,它几乎具备了select和poll的所有优点,被公认为Linux2.6下性能最好的多路IO就绪通知方法。
epoll有三个相关系统调用接口,分别是epoll_create,epoll_ctl 和 epoll_wait。
epoll_create
epoll_create函数的作用就是创建一个epoll的文件描述符。
返回值说明:
注意:当不再使用时,必须调用close函数关闭epoll模型对应的文件描述符,当所有引用epoll实例的文件描述符都已经关闭时,内核将销毁该实例并释放相关资源。
epoll_ctl
epoll_ctl 函数用于向指定的epoll模型中注册事件,它不同于seletct()的一点就是,select在监听事件时告诉内核要监听什么类型的事件,而它是先注册要监听的事件类型。
参数说明:
第二个参数op的取值有以下三种:
返回值说明:
第四个参数struct epoll_event 结构如下:
struct epoll_event结构当中有两个成员,第一个成员events表示的是需要监听的事件,第二个成员data是一个联合体结构,一般选择使用该结构当中的fd,表示需要监听的文件描述符。
events常用取值如下:
这些取值也是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。
epoll_wait
epoll_wait 函数用于收集监视的事件中已经就绪的事件
参数说明:
参数timeout的取值:
返回值说明:
epoll_wait 调用失败时,错误码可能被设置为:
红黑树和就绪队列
当某一进程调用epoll_create函数时,Linux内核会创建一个eventpoll结构体,也就是我们所说的epoll模型,eventpoll结构体当中的成员rbr和成员rdlist与epoll的使用方式密切相关。
注意:
说明一下:
回调机制
所有添加到红黑树当中的事件,都会与设备(网卡)驱动程序建立回调方法,这个回调方法在内核中叫做ep_poll_callback。
采用回调机制最大的好处,就是不再需要操作系统主动对就绪事件进行检测了,当时间就绪时会自动调用对应的回调函数进行处理。
说明一下:
epoll三部曲
为了简单演示一下epoll的使用方式,这里我们实现一个简单的epoll服务器,该服务器是获取客户端发来的数据并进行打印。
EpollServer类
EpollServer类中除了包含监听套接字和端口号两个成员变量之外,最好将epoll模型对应的文件描述符也作为一个成员变量。
#include "Socket.hpp"
#include
#define BACK_LOG 5
#define SIZE 256
class EpollServer
{
public:
EpollServer(int port)
: _port(port)
{}
void InitEpollServer()
{
_listen_sock = Socket::SocketCreate();
Socket::SocketBind(_listen_sock, _port);
Socket::SocketListen(_listen_sock, BACK_LOG);
// 创建epoll模型
_epfd = epoll_create(SIZE);
if (_epfd < 0)
{
std::cerr << "epoll_create error" << std::endl;
exit(5);
}
}
~EpollServer()
{
if (_listen_sock > 0) close(_listen_sock);
if (_epfd) close(_epfd);
}
private:
int _listen_sock; // 监听套接字
int _port; // 服务器端口号
int _epfd; // epollfd
};
运行服务器
服务器初始化完毕之后就可以开始运行了,而epoll服务器要做的就是不断调用epoll_wait函数,从就绪队列中获取就绪事件进行处理即可。
void HandlerEvent(struct epoll_event revs[], int num)
{
for (int i = 0; i < num; ++i)
{
int fd = revs[i].data.fd; // 就绪的文件描述符
if (fd == _listen_sock && revs[i].events & EPOLLIN)
{
// 连接事件就绪
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
std::string peer_ip = inet_ntoa(peer.sin_addr);
int peer_port = ntohs(peer.sin_port);
std::cout << "get a new link[" << peer_ip << ":" << peer_port << "]" << std::endl;
// 将获取到的文件描述符添加到sock中,并关心其读事件
AddEvent(sock, EPOLLIN);
}
else if (revs[i].events & EPOLLIN)
{
char buffer[1024];
ssize_t size = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (size > 0)
{
buffer[size] = 0;
std::cout << "echo# " << buffer << std::endl;
}
else if (size == 0)
{
std::cout << "client quit" << std::endl;
close(fd);
DelEvent(fd); // 将fd从epoll中删除
}
else
{
std::cerr << "recv error" << std::endl;
close(fd);
DelEvent(fd);
}
}
}
}
private:
void DelEvent(int sock)
{
epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
}
epoll服务器测试
#include "EpollServer.hpp"
#include
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cout << "Usage: " << "./EpollServer port" << std::endl;
exit(1);
}
int port = atoi(argv[1]);
EpollServer* svr = new EpollServer(port);
svr->InitEpollServer();
svr->Run();
return 0;
}
因为我们在调用epoll_wait函数时,将timeout的值设为了-1,因此服务器运行之后如果没有读事件就绪,那么就会阻塞等待。
注意:
epoll有两种工作方式,分别是水平触发模式和边缘触发工作模式。
水平触发(LT, Level Triggered)
epoll默认状态下就是LT工作模式:
边缘触发(ET, Edge Triggered)
如果要将epoll改为ET工作模式,则需要在添加时间时设置EPOLLET选项。
ET工作模式下应该如何进行读写?
因为在ET工作模式下,只有底层就绪事件由无到有或者由有到多时才会通知用户,这就倒逼用户当读事件就绪时必须一次性将数据全部读取完毕,当写事件就绪时就必须一次性将发送缓冲区写满,否则可能再也没有机会进行读写了。
因此读数据时必须循环调用recv函数进行读取,写数据时必须循环调用send函数进行写入。
注意:ET工作模式下,recv和send操作的文件描述符必须设置为非阻塞状态,这是必须的!
LT模式与ET模式对比
epoll的高性能,是有特定的场景的,如果场景选择不合适,epoll的性能可能适得其反。
对于多连接,且多连接中只有一部分连接活跃时,比较适合使用epoll。
如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下使用epoll就并不合适,具体要根据需求和场景特定来决定使用哪种IO模型。