目录
epoll初识
epoll的相关系统调用
epoll工作原理
epoll服务器
epoll的优点
epoll工作方式
对比LT和ET
epoll也是系统提供的一个多路转接接口。
epoll有三个相关的系统调用,分别是epoll_create、epoll_ctl和epoll_wait。
epoll_create函数
epoll_create函数用于创建一个epoll模型,该函数的函数原型如下:
int epoll_create(int size);
参数说明:
返回值说明:
注意: 当不再使用时,必须调用close函数关闭epoll模型对应的文件描述符,当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源。
epoll_ctl函数
epoll_ctl函数用于向指定的epoll模型中注册事件,该函数的函数原型如下:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
第二个参数op的取值有以下三种:
EPOLL_CTL_ADD
:注册新的文件描述符到指定的epoll模型中。EPOLL_CTL_MOD
:修改已经注册的文件描述符的监听事件。EPOLL_CTL_DEL
:从epoll模型中删除指定的文件描述符。返回值说明:
第四个参数对应的struct epoll_event结构如下:
struct epoll_event结构中有两个成员,第一个成员events表示的是需要监视的事件,第二个成员data是一个联合体结构,一般选择使用该结构当中的fd,表示需要监听的文件描述符。
events的常用取值如下:
EPOLLIN
:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。EPOLLOUT
:表示对应的文件描述符可以写。EPOLLPRI
:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。EPOLLERR
:表示对应的文件描述符发送错误。EPOLLHUP
:表示对应的文件描述符被挂断,即对端将文件描述符关闭了。EPOLLET
:将epoll的工作方式设置为边缘触发(Edge Triggered)模式。EPOLLONESHOT
:只监听一次事件,当监听完这次事件之后,如果还需要继续监听该文件描述符的话,需要重新将该文件描述符添加到epoll模型中。这些取值实际也是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。
epoll_wait函数
epoll_wait函数用于收集监视的事件中已经就绪的事件,该函数的函数原型如下:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数说明:
参数timeout的取值:
返回值说明:
epoll_wait调用失败时,错误码可能被设置为:
EBADF
:传入的epoll模型对应的文件描述符无效。EFAULT
:events指向的数组空间无法通过写入权限访问。EINTR
:此调用被信号所中断。EINVAL
:epfd不是一个epoll模型对应的文件描述符,或传入的maxevents值小于等于0。红黑树和就绪队列
当某一进程调用epoll_create函数时,Linux内核会创建一个eventpoll结构体,也就是我们所说的epoll模型,eventpoll结构体当中的成员rbr和rdlist与epoll的使用方式密切相关。
struct eventpoll{
...
//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监视的事件
struct rb_root rbr;
//就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;
...
}
在epoll中,对于每一个事件都会有一个对应的epitem结构体,红黑树和就绪队列当中的节点分别是基于epitem结构中的rbn成员和rdllink成员的,epitem结构当中的成员ffd记录的是指定的文件描述符值,event成员记录的就是该文件描述符对应的事件。
struct epitem{
struct rb_node rbn; //红黑树节点
struct list_head rdllink; //双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
说明一下:
EPOLLONESHOT
选项,当监听完这次事件后,如果还需要继续监听该文件描述符则需要重新将其添加到epoll模型中,本质就是当设置了EPOLLONESHOT
选项的事件就绪时,操作系统会自动将其从红黑树当中删除。EPOLLONESHOT
,那么该节点插入红黑树后就会一直存在,除非用户调用epoll_ctl将该节点从红黑树当中删除。回调机制
所有添加到红黑树当中的事件,都会与设备(网卡)驱动程序建立回调方法,这个回调方法在内核中叫ep_poll_callback。
采用回调机制最大的好处,就是不再需要操作系统主动对就绪事件进行检测了,当事件就绪时会自动调用对应的回调函数进行处理。
说明一下:
epoll三部曲
总结一下,epoll的使用过程就是三部曲:
为了简单演示一下epoll的使用方式,这里我们也实现一个简单的epoll服务器,该服务器也只是读取客户端发来的数据并进行打印。
EpollServer类
EpollServer类当中除了需要包含监听套接字和端口号两个成员变量之外,最好将epoll模型对应的文件描述符也作为一个成员变量。
代码如下:
#include "socket.hpp"
#include
#define BACK_LOG 5
#define SIZE 256
class EpollServer{
private:
int _listen_sock; //监听套接字
int _port; //端口号
int _epfd; //epoll模型
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 >= 0){
close(_epfd);
}
}
};
运行服务器
服务器初始化完毕后就可以开始运行了,而epoll服务器要做的就是不断调用epoll_wait函数,从就绪队列当中获取就绪事件进行处理即可。
代码如下:
#include "socket.hpp"
#include
#define BACK_LOG 5
#define SIZE 256
#define MAX_NUM 64
class EpollServer{
private:
int _listen_sock; //监听套接字
int _port; //端口号
int _epfd; //epoll模型
public:
void Run()
{
AddEvent(_listen_sock, EPOLLIN); //将监听套接字添加到epoll模型中,并关心其读事件
for (;;){
struct epoll_event revs[MAX_NUM];
int num = epoll_wait(_epfd, revs, MAX_NUM, -1);
if (num < 0){
std::cerr << "epoll_wait error" << std::endl;
continue;
}
else if (num == 0){
std::cout << "timeout..." << std::endl;
continue;
}
else{
//正常的事件处理
//std::cout<<"有事件发生..."<
说明一下:
事件处理
如果底层就绪队列当中有就绪事件,那么调用epoll_wait函数时就会将底层就绪队列中的事件拷贝到用户提供的revs数组当中,接下来epoll服务器就应该对就绪事件进行处理了,事件处理过程如下:
代码如下:
#include "socket.hpp"
#include
#define BACK_LOG 5
#define SIZE 256
#define MAX_NUM 64
class EpollServer{
private:
int _listen_sock; //监听套接字
int _port; //端口号
int _epfd; //epoll模型
public:
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;
AddEvent(sock, EPOLLIN); //将获取到的套接字添加到epoll模型中,并关心其读事件
}
else if (revs[i].events&EPOLLIN){ //读事件就绪
char buffer[64];
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); //将文件描述符从epoll模型中删除
}
else{
std::cerr << "recv error" << std::endl;
close(fd);
DelEvent(fd); //将文件描述符从epoll模型中删除
}
}
}
}
private:
void AddEvent(int sock, uint32_t event)
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = sock;
epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
}
void DelEvent(int sock)
{
epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
}
};
epoll服务器测试
运行epoll服务器时需要先实例化出一个EpollServer对象,对epoll服务器进行初始化后就可以运行服务器了。
代码如下:
#include "epoll_server.hpp"
#include
static void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 2){
Usage(argv[0]);
exit(1);
}
int port = atoi(argv[1]);
EpollServer* svr = new EpollServer(port);
svr->InitEpollServer();
svr->Run();
return 0;
}
因为编写epoll服务器在调用epoll_wait函数时,我们将timeout的值设置成了-1,因此运行服务器后如果没有客户端发来连接请求,那么服务器就会在调用epoll_wait函数后进行阻塞等待。
当我们用telnet工具连接epoll服务器后,epoll服务器调用的epoll_wait函数在检测到监听套接字的读事件就绪后就会调用accept获取建立好的连接,并打印输出客户端的IP和端口号,此时客户端发来的数据也能够成功被epoll服务器收到并进行打印输出。
此外,我们这里编写的也是一个单进程的epoll服务器,但是它可以同时为多个客户端提供服务。
我们可以用ls /proc/PID/fd命令,查看当前epoll服务器的文件描述符的使用情况。其中文件描述符0、1、2是默认打开的,分别对应的是标准输入、标准输出和标准错误,3号文件描述符对应的是监听套接字,4号文件描述符对应的是服务器创建的epoll模型,5号和6号文件描述符对应的分别是正在访问服务器的两个客户端。
当服务器端检测到客户端退出后,也会关闭对应的连接,此时epoll服务器对应的5号和6号文件描述符就关闭了。
epoll服务器整体编程的顺序思路
注意:
与select和poll的不同之处
epoll有两种工作方式,分别是水平触发工作模式和边缘触发工作模式。
水平触发(LT,Level Triggered)
epoll默认状态下就是LT工作模式。
边缘触发(ET,Edge Triggered)
如果要将epoll改为ET工作模式,则需要在添加事件时设置EPOLLET
选项。
ET工作模式下应该如何进行读写
因为在ET工作模式下,只有底层就绪事件无到有或由有到多发生变化的时候才会通知用户,这就倒逼用户当读事件就绪时必须一次性将数据全部读取完毕,当写事件就绪时必须一次性将发送缓冲区写满,否则可能再也没有机会进行读写了。
因此读数据时必须循环调用recv函数进行读取,写数据时必须循环调用send函数进行写入。
强调: ET工作模式下,recv和send操作的文件描述符必须设置为非阻塞状态,这是必须的,不是可选的。