Linux & IO多路转接——epoll详解
Posted 努力学习的少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux & IO多路转接——epoll详解相关的知识,希望对你有一定的参考价值。
目录
- 💂 个人主页:努力学习的少年
- 🤟 版权: 本文由【努力学习的少年】原创、在CSDN首发、需要转载请联系博主
- 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦
一. epoll简介
epoll的功能一样跟select和poll一样,都是用来检测文件描述符中的事件是否就绪,当有事件就绪,可以通知给应用层,上层调用 read,recv,write,send 等类似接口就不会被阻塞。
我们之前学过select,poll应该知道,select 和 poll 有如下缺陷:
- 它们需要 额外创建数组保存文件描述符,每一次检测时候,都需要将数组中的文件描述重新设置设置进 文件描述符集 中。
- 除此之外,调用select,epoll检测文件描述符集是否有文件描述符事件就的事件复杂度是
- O(N),因为内核需要依次检测文件描述符集中每个文件符的事件是否就绪。
- select中的文件描述集能够设置的文件描述符是有限的.
epoll通过两方面就很好的解决了select和epoll的缺陷
- 第一, epoll在内核中使用 红黑树 来 跟踪进程所有待检测的文件描述符,把需要监 控socket通过epoll_ctl函数加入到内核的红黑树里,红黑树是个高效的数据结构,它的增删查改的时间复杂度是O(logN),当需要进行加入某个文件描述符进行跟踪检测,需要epoll_ctl接口将文件描述符到红黑树中,添加到红黑树的文件描述符则会不断的进行检测,如果想取消 epoll跟踪的检测某个文件描述符,则也可以使用epoll_ctl接口将红黑树中相对应的节点给删除掉.
- 第二,epoll使用事件的驱动机制,内核中会维护着一个就绪队列,当某个文件描述符有事件发生时,则通过回调函数内核将其事件加入到这个就绪队列中,当用户调用epoll_wait接口时,通过就绪队列是否为空来判断是否有某个文件描述符的事件就绪,如果不为空,则说明有文件描述符就绪,则返回就绪队列中文件描述符的个数,因此epoll检测是否有文件描述符就绪的时间复杂度时O(1)。
二. epoll相关系统的调用
1. epoll_create
int epoll_create(int size);
调用epoll_create后,内核会创建一个epoll_create对象,对象中包括跟踪检测事件的红黑树,就绪队列,回调机制。
- 参数: 自从linux2.6.8之后,size参数是被忽略的。
- 返回值:创建epoll_create后会返回一个epoll对象的文件描述符,调用者可以通过文件描述符访问到epoll对象
2. epoll_ctl
epoll_ctl 接口是用来 维护 epoll 对象中红黑树的节点,epoll_ctl可以在红黑树中添加,删除,修改节点。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
- epfd:eppoll对象的文件描述符
- op:选择修改epoll中红黑树的方式,如下:
- EPOLL_CTL_ADD:往红黑树中插入节点
- EPOLL_CTL_MOD:修改红黑树中的节点的信息。
- EPOLL_CTL_DELL:删除红黑树中节点。
- fd:文件描述符。
- epoll_event保存的是事件信息,他的结构体如下:
struct epoll_event
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
;
typedef union epoll_data
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
epoll_data_t;
events本质是一个位图,它是用来表示事件的等待方式和事件的工作方式, 相对应的宏定义如下:
- EPOLLIN:表示读事件
- EPOLLOUT:表示写事件。
- EPOLLPRI:表示有紧急数据可以读。
- EPOLLET:表示使用ET的工作方式。
如果想要设置events多个条件,可以将用" | “表示,比如,既想要读事件又想要是ET的触发事件方式,则可以用 EPOLLIN | EPOLLET 表示。
epoll_data是一个联合体,他只能记录一个信息,他可以是指针,或者是一个文件描述符等等.如果是epoll服务器,epoll_data中一般记录的是socket文件描述符.
返回值
调用成功,返回0,调用失败返回-1,并设置errno错误码.
epoll_ctl本质是 以 fd-event 作为 key-value 映射关系插入到红黑树中,底层会根据红黑树节点的event中 events 判断是 需要检测读事件 还是检测写事件。
3. epoll_wait
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
描述:调用epoll_wait能够获取就绪队列中已经就绪的事件。
- epfd:epoll对象的文件描述符
- events:将从就绪队列中获取到的事件信息保存进 events 数组中中,上层就可以通过eventsl中获取到事件信息判断接下来的操作,如果想要从就绪队列中取出多个文件描述符信息,则需要传进去一个event_poll类型的数组。
- maxevents:期望获取就绪队列中的事件的个数
- timeout:在内核阻塞为timeout秒,直到就绪队列不为空。
- 返回值:成功返回获取到的事件的数量,返回0,表明在timeout时间内就绪队列一直为空,返回-1表示epoll_wait发生错误,并且设置errno错误码.
epoll_wait本质是从就绪队列中已经就绪的节点event信息复制上来,上层可以通过event的信息判断是哪一个文件描述符事件就绪,events中判断是读事件就绪还是写事件就绪
三. epoll工作方式
epoll有两种触发模式,一种是水平触发(level-triggered,LT),
另一种是边缘触发(edgetriggered,ET),epoll默认的的工作方式是LT,如果想要设置ET工作方式,需要使用epoll_ctl进行设置。
1. 水平触发模式(level-triggered,LT)
使用水平触发模式,当socket缓冲区如果一直有数据,则就会一直触发回调函数将其socket的事件加入到就绪队列中,只有当socket缓冲区中没有数据,才不会触发回调函数.水平触发模式的socket,水平触发模式的socket可以不用一次性读取socket缓冲区中的数据,因为只要socket缓冲区有数据,则会一直触发回调函数,将socket的事件加入到就绪队列中,上层调用epoll_wait则就可以一直获得到该socket文件描述符.
2. 边缘触发模式(edgetriggered,ET)
- 使用边缘触发,当底层的socket文件描述符中的缓冲区出现变化的时候(缓冲区数据从无到有,从有到多),才会触发回调函数将socket的事件加入到就绪队列中。
- 如果socket缓冲区中没有发生变化,则socket一直不会被触发.即使相对应的socket缓冲区中有数据,。
- 如果是ET模式触发的socket,则每次都需要通过循环调用recv将事件中的socket缓冲区中的数据读取干净,如果没有将数据读取干净,那么下次socket的缓冲区没有数据就绪,那么就一直不会触发socket事件,socket事件就不会加入到就绪队列中,那么socket缓冲区剩下的数据就一直不会读取上来.
- 这里有个小细节,如果是ET模式触发的socket,那么往 socket中的缓冲区读取数据时,使用recv 或者 read 等接口时 去读取缓冲区的数据 一定要设置为非阻塞,因为每次读取都需要循环调用recv接口 去读取socket缓冲区的数据,最后一次读取socket缓冲区一定是为空,则最后一次recv不会读取的时候不会被阻塞在内核中。如果调用read,recv是阻塞读取,那么读取到socket缓冲区为空时,则read,recv则会阻塞在内核中,等待socket数据就绪,此时就相当于破坏了epoll的作用,因为epoll的作用本质是消除recv和read等接口等待数据就绪的过程。
总结:
使用边缘触发模式的效率相比使用水平触发模式的效率更高,因为 边缘触发模式 会逼迫上层一次性读完缓冲区,如果没有读取干净,则剩下的数据可能就不会读取到。每次检测socket触发回调机制,回调机制是会消耗cpu资源。
四.简易的epoll服务器代码编写
server.hpp文件
#pragma once
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
#define LOG_NUM 5
using std::cout;
using std::endl;
namespace sjp
class server
public:
server()
~server()
//创建套接字
static int Socket()
int sockfd=socket(AF_INET,SOCK_STREAM,0);
return sockfd;
//绑定套接字接口
static bool Bind(int sockfd,unsigned short int port)
struct sockaddr_in s;
memset(&s,'\\0',sizeof(s));
s.sin_family=AF_INET;
s.sin_port=htons(port);
s.sin_addr.s_addr=0;
if(bind(sockfd,(struct sockaddr*)&s,sizeof(s))<0)
cout<<"bind error"<<endl;
_exit(-1);
return true;
//监听套接字
static bool Listen(int sockfd)
int i=listen(sockfd,LOG_NUM);
if(i==-1)
cout<<"listen fail"<<endl;
_exit(-2);
return true;
;
epoll_server.hpp文件
#pragma once
#include"server.hpp"
#define NUM 1024
#define WAIT_NUM 32;
#include<sys/epoll.h>
namespace ep_server
class EpollServer
private:
int port;//端口号
int listen_sock;//监听套接字
int epfd;
public:
EpollServer(int _port):port(_port)
void InitServer()
listen_sock=sjp::server::Socket();
sjp::server::Bind(listen_sock,port);
sjp::server::Listen(listen_sock);
epfd=epoll_create(NUM);
void Run()
Addevent(listen_sock,EPOLLIN);
while(1)
struct epoll_event ep[32];//保存就绪事件
int sz=epoll_wait(epfd,ep,32,1000);//sz是获取就绪事件的个数
if(sz>0)
for(int i=0;i<sz;i++)
if(ep[i].events==EPOLLIN)
//可读事件
if(ep[i].data.fd==listen_sock)
//监听套接字就绪,读取socket
//LT触发可以不用一次性将所有链接读上来
//读取到事件的信息
struct sockaddr peer;
socklen_t len;
int fd=accept(listen_sock,&peer,&len);
if(fd>0)
Addevent(fd,EPOLLIN);//将新的socket添加到红黑树中
else
char str[1024];
size_t sz=recv(ep[i].data.fd,(void*)str,1024,MSG_DONTWAIT);
if(sz>0)
str[sz]='\\0';
cout<<str<<endl;
else if(sz==0)
else
//对端关闭,需要在红黑树中删除等待事件
Deletevent(ep[i].data.fd);
close(ep[i].data.fd);//关闭socket
else if(ep[i].events==EPOLLOUT)
//可写事件
else
//其他事件
else if(sz==0)
cout<<"without file fd"<<endl;
else
cout<<"epoll_wait failing"<<endl;
void Deletevent(int fd)
if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)<0)
cout<<"delete event failing,fd :"<<fd<<endl;
//添加等待事件函数
void Addevent(int fd,uint64_t event)
struct epoll_event _event;
_event.events=event;
_event.data.fd=fd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&_event)<0)
cout<<"Add epoll fail,fd :"<<fd<<endl;
;
epoll_server.cc
#include"epoll_server.hpp"
#include<stdlib.h>
void Usage()
cout<<"Usage Way: epollserver port"<<endl;
int main(int argc,char* argv[])
if(argc!=2)
Usage();
int port= atoi(argv[1]);
//创建epoll服务器对象
ep_server::EpollServer* es=new ep_server::EpollServer(port);
es->InitServer();
es->Run();
以上是关于Linux & IO多路转接——epoll详解的主要内容,如果未能解决你的问题,请参考以下文章
典型I/O模型——阻塞IO,非阻塞IO,信号驱动IO,异步IO,IO多路转接(select&poll&epoll)
epoll(eventpoll)是干嘛的?IO多路转接技术(相较selectpoll的优点)