通俗易懂的epoll

本文详细解析epoll的工作原理,包括其红黑树和回调机制,并展示了如何利用epoll创建高效服务器,通过回调函数处理读写事件。介绍了epoll的优点如便捷接口、轻量数据拷贝和事件回调,以及如何使用epoll_wait进行事件检测和处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

理解epoll工作原理

  • 每一个epoll对象都有eventepoll结构体
  • epoll底层是一颗红黑数来管理文件描述符中的事件。
  • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法
  • 当事件发生时,回调方法会拷贝一份该节点到一个队列中,该队列的用双链表实现的。
  • 在epoll中没一个事件都会建立节点(epitem结构体)

在这里插入图片描述
例子:我们在编写套接字编程的代码时,要把监听套接字放在epoll中来,让epoll帮我们来进行等待连接到来,连接到来事件叫做读事件。
这时候我们通过调用函数把监听套接字放入到红黑树中,Linux内核会对每一个事件对象创建一个epintm结构体。
判断也没有事件只需要通过rdllist是否为空。

struct epitem{ 
 struct rb_node rbn;//红黑树节点 
 struct list_head rdllink;//双向链表节点 
 struct epoll_filefd ffd; //事件句柄信息 
 struct eventpoll *ep; //指向其所属的eventpoll对象 
 struct epoll_event event; //期待发生的事件类型 
}

epoll的两种工作模式

LT:水平触发,不断的提醒你有事件到来,直到你把事件全部执行完

ET:边缘触发:只提醒你一次有事件到来,如果不执行,就要等下一次事件到来。

ET模式下:要采用非阻塞的方式进行读操作。且要不断的读,直到读完。
如果不采用非阻塞方式读取,则可能会阻塞住。

如何使用epoll

创建句柄
int epoll_create(int size);
返回一个整数,是一个文件描述符。
使用:int epfd=epoll_create(256);


添加事件到红黑树中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op:是一个宏
EPOLL_CTL_ADD:添加
EPOLL_CTL_MOD:修改
EPOLL_CTL_DEL:删除

struct epoll_event{
	uint32_t     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(写事件)。
返回值:成功返回0,失败返回-1

//拿出事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
timeout:轮询检查的事件
返回值:成功返回事件到达的数量。
使用:
	int epfd=epoll_create(256);
	
	struct epoll_event item;
	item.data.fd=sock;     //添加套接字
	item.events=EPOLLIN   //只关心读时间
	epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&item);//添加事件到红黑树
	
	struct events eve[64];
	int num=epoll_wait(epfd,eve,64,1000);
	//有事件到来,会把事件的节点拷贝到eve数组中,
	//我们只需要遍历数组就可以进行读或者写。
	for(int i=0;i<num;i++){
		if(eve[i].events & EPOLLIN){
			int sock=eve[i].data.fd;
			开始进行读操作(进行读操作时要区分是连接到来,还只是进行读取)
		}
		else if(eve[i].event& EPOLLOUT){
			int sock=eve[i].data.fd;
			开始进行写操作
		}
	}

epoll的优点

  • 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  • 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,
  • epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
  • 没有数量限制: 文件描述符数目无上限

使用epoll实现一个服务器

									sock.hpp//创建套接字
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>
#include<cstring>
#include"log.hpp"

#define LONG 5

using namespace std;
class Socket{
    public:
        static void Sock(int& listen_sock)
        {
            listen_sock=socket(AF_INET,SOCK_STREAM,0);
            if(listen_sock<0){
                LOG(ERROR,"socket error...");
                exit(1);
            }
            LOG(INFO,"socket success...");
        }
        static void Bind(int listen_sock,int port)
        {
            struct sockaddr_in own;
            memset(&own,0,sizeof(own));
            own.sin_family=AF_INET;
            own.sin_port=htons(port);
            own.sin_addr.s_addr=INADDR_ANY;
            if(bind(listen_sock,(struct sockaddr*)&own,sizeof(own))<0){
                LOG(error,"bind error...");
                exit(2);
            }
            LOG(INFO,"bind sucess...");
        }
        static void Listen(int listen_sock)
        {
            if(listen(listen_sock,LONG)<0){
                LOG(error,"listen error...");
                exit(3);
            }
            LOG(INFO,"listen succsee...");
        }
};
									reactor.hpp
#pragma once
#include<iostream>
#include<string>
#include<unordered_map>
#include<unistd.h>
#include<sys/epoll.h>
#include"log.hpp"
using namespace std;
#define MAX_NUM 64

class Reactor;
class EventItem;

typedef int(*callback_t)(EventItem *);

每一个事件都要对应一个这个结构体
class EventItem
{
    public:
        //定义回调函数
        callback_t recv_handler;
        callback_t send_handler;
        //sock
        int sock;
        //缓存
        string inbuffer;
        string outbuffer;
        //回指向Reactor
        Reactor* R;
    public:
        EventItem():sock(0),R(nullptr),recv_handler(nullptr),send_handler(nullptr)
        {}
		
		//注册回调函数
        void MakeCallBack(callback_t _recv,callback_t _send)
        {
            recv_handler=_recv;
            send_handler=_send;
        }

        ~EventItem(){}
};

class Reactor
{
    private:
    //epoll句柄
    int epfd;
    //使用哈希容器来一一对应
    unordered_map<int,EventItem> mp;

    public:
    
    Reactor(){}
    void InitReactor()
    {
        epfd=epoll_create(256);
        if(epfd<0){
            LOG(ERROR,"epoll_create error...");
            exit(5);
        }
        LOG(INFO,"epoll_create success..."+to_string(epfd));
    }
	
	添加事件到红黑树中
    void AddToEpoll(int sock,uint32_t ev,const EventItem& item)
    {
        struct epoll_event event;
        event.events=0;
        event.events|=ev;
        event.data.fd=sock;

        string s=to_string(sock);
        if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&event)<0){
            LOG(ERROR,"epoll_ctl add error...sock:"+s);
        }
        else{
            mp.insert({sock,item});
            LOG(INFO,"epoll_ctl add success...sock:"+s);
        }

    }

	删除红黑树中的事件
    void RevokeEpoll(int sock)
    {
        if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr)<0){
            string s=to_string(sock);
            LOG(ERROR,"epoll_ctl del error...sock:"+s);
        }
        删除哈希中的映射
        mp.erase(sock);
    }
    void Assignment(int timeout)
    {
        struct epoll_event revs[MAX_NUM];
        int num=epoll_wait(epfd,revs,MAX_NUM,timeout);
        
        事件到来轮询recv中的节点
        for(int i=0;i<num;i++){
            int sock=revs[i].data.fd;
            uint32_t mask=revs[i].events;
            if(mask & EPOLLIN){
                if(mp[sock].recv_handler)
                    mp[sock].recv_handler(&mp[sock]);调用回调函数进行读
            }
            else if(mask & EPOLLOUT){
                if(mp[sock].send_handler)
                    mp[sock].send_handler(&mp[sock]);调用回调函数进行写
            }
        }
    }
    ~Reactor()
    {
        if(epfd>=0)
            close(epfd);
    }
};
									insertface.hpp
#pragma once
#include<iostream>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include"reactor.hpp"
#include"util.hpp"


int recver(EventItem* eve);

连接到来
int accepter(EventItem* eve)
{
    struct sockaddr_in oth;
    socklen_t len=sizeof(oth);
    int sock=accept(eve->sock,(struct sockaddr*)&oth,&len);
    if(sock<0){

    }
    else{
        SelNonBlock(sock);

        EventItem item;
        item.sock=sock;
        item.R=eve->R;

        item.MakeCallBack(recver,nullptr);
        Reactor* ptr=eve->R;
        ptr->AddToEpoll(sock,EPOLLIN|EPOLLET,item);
    }
}

只写了连接到来的接口,读没有写
int recver(EventItem* eve)
{

}
									log.hpp//日志,方便知道走到哪里了
#pragma once
#include<iostream>
#include<string>
#include<time.h>

using namespace std;

#define INFO 1
#define ERROR 2

采用宏来调用,具有可读性
#define LOG(str1,str2) log(#str1,str2,__FILE__,__LINE__)

void log(string str1,string str2,string str3,int line)
{
    cout<<"["<<str1<<"]"<<"["<<time(nullptr)<<"]"<<"["<<str2<<"]"<<"["<<str3<<"]"<<"["<<line<<"]"<<endl;
}
									util.hpp
#pragma once
#include<iostream>
#include<unistd.h>
#include<fcntl.h>

设置非阻塞
void SelNonBlock(int sock)
{
     int fl = fcntl(sock, F_GETFL);
        fcntl(sock, F_SETFL, fl | O_NONBLOCK);
}
									server.cc
#include<iostream>
#include<cstdlib>
#include"sock.hpp"
#include"reactor.hpp"
#include"insertface.hpp"
#include"util.hpp"
int main(int argc,char* argv[])
{
    if(argc!=2){
        exit(4);
    }
    int port=atoi(argv[1]);
    int listen_sock=-1;
    
    创建套接字
    Socket::Sock(listen_sock);
    Socket::Bind(listen_sock,port);
    Socket::Listen(listen_sock);
	
	创建Reactor,并初始化
    Reactor Re;
    Re.InitReactor();
	
	创建监听套接字对应的结构
    EventItem item;
    item.sock=listen_sock;
    item.R=&Re;
	
	把监听套接字设置成非阻塞
    SelNonBlock(listen_sock);
    
	注册回调函数,我这里把读注册了accept,没有注册写
    item.MakeCallBack(accepter,nullptr);
	
	添加到红黑树中,采用ET模式
    Re.AddToEpoll(listen_sock,EPOLLIN|EPOLLET,item);

    int timeout=1000;

	一直调用检查事件是否到来
    while(true){
        Re.Assignment(timeout);
    }
}


整个的精髓是,把 epoll 和	读写分开了,采用了回调函数的方法,只不要管reactor.hpp,只需要在insertface.hpp中添加函数就行。
<think>嗯,用户问“事件驱动到底是什么,就是epoll吗”。首先,我需要确认用户对事件驱动模型和epoll的关系的理解可能存在混淆。用户可能知道epollLinux下的一个I/O多路复用机制,但可能不清楚事件驱动架构的总体概念。 接下来,我需要解释事件驱动模型的基本原理,强调它如何处理并发连接而不需要多线程或多进程的开销。然后,区分事件驱动和epoll的关系,说明epoll是实现事件驱动的一种具体技术,尤其是在Linux环境下。 用户可能混淆了概念,认为事件驱动就是epoll,但实际上事件驱动是一种编程范式,而epoll是实现这种范式的工具之一。需要举例说明其他系统上的类似机制,比如kqueue在FreeBSD,或者select/poll这些较老的实现。 还需要指出事件驱动的优势,比如高并发处理能力、低资源消耗,以及适用场景,如Web服务器、实时通信等。同时,提到可能的缺点,比如回调地狱或者对CPU密集型任务的不友好,但用户的问题可能更关注于基本概念,所以这部分可以简要带过。 另外,用户可能想知道为什么Nginx这样的服务器使用事件驱动模型,以及epoll在其中扮演的角色。需要将之前的回答与Nginx的实际应用结合起来,说明epoll如何帮助Nginx高效处理大量连接。 最后,总结事件驱动与epoll的关系,强调epoll是事件驱动在Linux中的一种高效实现方式,但事件驱动本身是一个更广泛的概念。确保语言通俗易懂,避免过多技术术语,让用户能够清晰理解两者的区别和联系。</think>### 事件驱动模型详解:不只是 epoll --- #### 一、事件驱动的本质 **事件驱动(Event-Driven)** 是一种编程范式,其核心逻辑为: - **不主动轮询**:不通过循环检查资源状态(如 `while(1)` 轮询文件描述符) - **被动响应**:当特定事件(如网络数据到达、文件可读、定时器触发)发生时,由系统通知程序处理 - **非阻塞**:主线程始终处于空闲状态等待事件,避免无意义的 CPU 占用 类比生活中的例子: 传统轮询 ➜ 你每隔5分钟检查一次快递柜 事件驱动 ➜ 快递员到达时直接打电话通知你 --- #### 二、事件驱动 ≠ epoll | 概念层级 | 说明 | |-------------------|----------------------------------------------------------------------| | **事件驱动模型** | 编程架构思想(如浏览器事件、GUI点击事件) | | **I/O 多路复用** | 实现事件驱动的具体技术(如 `epoll`、`kqueue`、`select`、`poll`) | | **epoll** | Linux 平台的高性能 I/O 多路复用实现 | **关键区别**: - **epoll 是工具**:解决“如何高效监听多个文件描述符状态变化”的问题 - **事件驱动是设计模式**:定义“如何基于事件流组织程序逻辑” --- #### 三、事件驱动的核心组件 1. **事件收集器(Event Demultiplexer)** - 功能:监听并收集所有待处理事件(如 `epoll_wait`) - 实现:依赖操作系统接口(Linux 用 `epoll`,BSD 用 `kqueue`,Windows 用 `IOCP`) 2. **事件分发器(Event Dispatcher)** - 流程: ```plaintext 等待事件 → 识别事件类型 → 调用对应的回调函数 ``` - 示例:Nginx 的 `ngx_process_events` 函数 3. **事件处理器(Event Handler)** - 形式:预定义的回调函数(如 `on_read`、`on_write`) - 特性:非阻塞、短耗时(避免阻塞事件循环) --- #### 四、epoll 的工作机制(以 Linux 为例) 1. **三阶段操作** ```c epoll_create() // 创建 epoll 实例 epoll_ctl() // 注册/修改/删除监控的文件描述符 epoll_wait() // 等待事件触发 ``` 2. **高效原理** - **红黑树存储**:快速管理百万级文件描述符(`O(log n)` 时间复杂度) - **事件就绪列表**:仅返回已触发事件,无需遍历全部描述符 - **边缘触发(ET)**:减少重复通知(对比 `select`/`poll` 的水平触发) 3. **性能对比** | 方法 | 时间复杂度 | 最大连接数 | 内存拷贝 | |------------|------------|------------|--------------------| | `select` | O(n) | 1024 | 每次传递全量描述符 | | `poll` | O(n) | 无限制 | 同 select | | `epoll` | O(1) | 数十万 | 仅返回就绪事件 | --- #### 五、事件驱动的应用场景 1. **高并发服务器** - Nginx:单线程处理数万并发连接(依赖 `epoll` + 非阻塞 I/O) - Redis:单线程实现高吞吐量(事件循环处理命令请求) 2. **实时通信** - WebSocket 服务:同时维持大量长连接 - 游戏服务器:快速响应玩家动作事件 3. **GUI 框架** - 浏览器:点击/滚动事件处理 - 桌面应用:异步响应用户操作 --- #### 六、与其他模型的对比 | 模型 | 事件驱动 | 多线程/多进程 | |------------------|------------------------------|-----------------------------| | **资源消耗** | 内存占用低(单线程) | 线程栈内存叠加消耗大 | | **上下文切换** | 无线程切换开销 | 频繁切换导致 CPU 缓存失效 | | **编程复杂度** | 回调嵌套可能引发“回调地狱” | 需处理线程同步、锁竞争问题 | | **适用场景** | I/O 密集型任务 | CPU 密集型任务 | --- #### 七、事件驱动的局限性 1. **回调地狱** - 深层嵌套回调导致代码可读性差(可通过 `Promise`/`async-await` 缓解) 2. **CPU 密集型短板** - 单线程中长时间运算会阻塞事件循环(需配合 Worker 线程) 3. **调试难度** - 异步执行流程难以跟踪(需依赖日志或调试工具) --- ### 总结 - **事件驱动是“怎么做”**:一种通过事件响应机制组织程序逻辑的设计思想 - **epoll 是“用什么做”**:Linux 下实现高效 I/O 事件监听的具体工具 - **完整技术栈示例**: ```plaintext 事件驱动架构 → I/O 多路复用 → epollLinux)/kqueue(BSD) → Nginx/Redis 应用 ``` 实际开发中,事件驱动常与非阻塞 I/O、线程池等技术结合,在保持高并发的同时兼顾灵活性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世_生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值