muduo库学习所得

Reactor模型

多 Reactor 多进程 / 线程方案的示意图

一、MainReactor与SubReactor的区别

维度MainReactorSubReactor
核心职责仅处理新连接建立事件(OP_ACCEPT)处理已建立连接的I/O事件(如OP_READ/OP_WRITE)
线程数量通常为单线程(高并发场景可能少量线程)多线程(通常为CPU核数的1~2倍)
性能目标快速响应连接请求,避免成为瓶颈高效处理数据读写,避免I/O阻塞影响新连接接入
资源管理监听全局ServerSocketChannel管理分配到的SocketChannel连接队列
设计意义解决连接接入的突发流量压力解决数据传输的持续高并发压力

关键引用:

  • MainReactor仅暴露一个服务端口,通过accept()获取新连接并分配至SubReactor [][][]。
  • SubReactor维护已注册的Socket连接,执行非阻塞读写及事件分发 [][][]。

二、select操作的区别

1. MainReactor中的select

  • 监听事件类型:仅OP_ACCEPT(连接建立事件) [][][]。
  • 触发条件:客户端发起TCP握手(SYN包)时,内核通知就绪。
  • 后续动作:通过dispatch将新连接分配给SubReactor [][][]。

2. SubReactor中的select

  • 监听事件类型:OP_READ(读就绪)和OP_WRITE(写就绪) [][][]。
  • 触发条件:已建立连接的Socket有数据到达内核缓冲区或可写。
  • 后续动作:调用对应的Handler处理数据,可能将计算任务提交至Worker线程池 [][][]。
组件select目标事件线程模型核心任务
MainReactorOP_ACCEPT单/少量线程高效接收新连接并分配
SubReactorOP_READ/WRITE多线程处理已连接Socket的I/O事件

知识点扩充

LT和ET模式的区别:

  • LT 模式(水平触发):像“唠叨的闹钟”——事件没处理完就一直提醒你,直到你搞定为止。
  • ET 模式(边缘触发):像“高冷的通知员”——事件发生时只提醒一次,爱理不理随你便,漏了后果自负。

静态库和动态库后缀

库类型Linux 后缀Windows 后缀
静态库.a.lib
动态库.so.dll

Cmake中set(静态/动态)库分析

  1. 静态库必用 CMAKE_ARCHIVE_OUTPUT_DIRECTORY
  2. 共享库使用 CMAKE_LIBRARY_OUTPUT_DIRECTORY
  3. 项目路径规范:
    • /lib 存放静态库和导入库
    • /bin 存放共享库和可执行文件
# 静态库(.a/.lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)
# 共享库(.so/.dll)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)  
# 可执行文件 
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)

框架体系

reactor模型中具体部分:

  • Event :事件 (包含:读/写/错误事件)
  • Reactor :反应堆
  • Demultiplex :多路事件分发器
  • EventHandler :事件处理器
  • EventLoop : subReactor (包含一个Poller事件分发器和多个Channer(事件))

这里面的EventLoop就是subreactor,其中包含一个Poller(多路事件分发器)和多个Channel(event事件集合-----读/写一类的)

其中一个EventLoop在一个线程中(实现了one loop per thread)

由于使用的是多 Reactor 多进程 / 线程方案(用来解决并发压力),所以使用类似于Nginx相似的操作,一个main reactor通过accept组件负责处理新的客户端连接,并分给各个sub reactor,每个sub reactor负责一个连接的读写等工作。

Channel代码分析

EventLoop = 一个Poller(多路事件分发器) + 多个Channel(event事件集合-----读/写一类的)

Poller ----> 事件监听器(专门用来观察文件描述符所要发生的事件)
本职:服务员,负责盯着自己负责的桌子,看哪个桌子的顾客有需求(事件),然后报告


Channel理解为通道 通俗理解 —> “文件描述符的智能管家”
封装了sockfd和其感兴趣的 event 如EPOLLIN、EPOLLOUT事件 还绑定了poller返回的具体事件

Channel 具体做什么?(3 个核心功能)
  1. 替 fd 记住“关心的事件”
    比如某个 fd 只关心“收到数据”(读事件),Channel 会帮它记下来:“这个 fd 只听‘收数据’的消息”。
  2. 替 fd 对接“监控系统”
    Channel 会把 fd 和它关心的事件,统一注册到“监控系统”(比如 epoll),不用你手动调用 epoll_ctl 这种复杂命令。
  3. 替 fd 保存“事件处理方法”
    比如某个 fd 收到数据后,应该调用 onRead() 函数处理,Channel 会提前存好这个函数,等事件发生时直接“按按钮”执行。

Channel = 给 fd 配了个“秘书”,秘书负责记需求、对接监控、执行任务,你(程序员)只需要指挥秘书干活就行!

#pragma once

#include <functional>
#include <memory>

#include "noncopyable.h"
#include "Timestamp.h"

class EventLoop;

/**
 * 理清楚 EventLoop、Channel、Poller之间的关系  Reactor模型上对应多路事件分发器
 * Channel理解为通道 封装了sockfd和其感兴趣的event 如EPOLLIN、EPOLLOUT事件 还绑定了poller返回的具体事件
 **/
class Channel : noncopyable
{
public:
    using EventCallback = std::function<void()>; // muduo仍使用typedef
    using ReadEventCallback = std::function<void(Timestamp)>;

    Channel(EventLoop *loop, int fd);
    ~Channel();

    // fd得到Poller通知以后 处理事件 handleEvent在EventLoop::loop()中调用
    void handleEvent(Timestamp receiveTime);

    // 设置回调函数对象
    void setReadCallback(ReadEventCallback cb) { readCallback_ = std::move(cb); }
    void setWriteCallback(EventCallback cb) { writeCallback_ = std::move(cb); }
    void setCloseCallback(EventCallback cb) { closeCallback_ = std::move(cb); }
    void setErrorCallback(EventCallback cb) { errorCallback_ = std::move(cb); }

    // 防止当channel被手动remove掉 channel还在执行回调操作
    void tie(const std::shared_ptr<void> &);

    int fd() const { return fd_; }
    int events() const { return events_; }
    void set_revents(int revt) { revents_ = revt; }

    // 设置fd相应的事件状态 相当于epoll_ctl add delete
    void enableReading() { events_ |= kReadEvent; update(); }
    void disableReading() { events_ &= ~kReadEvent; update(); }
    void enableWriting() { events_ |= kWriteEvent; update(); }
    void disableWriting() { events_ &= ~kWriteEvent; update(); }
    void disableAll() { events_ = kNoneEvent; update(); }

    // 返回fd当前的事件状态
    bool isNoneEvent() const { return events_ == kNoneEvent; }
    bool isWriting() const { return events_ & kWriteEvent; }
    bool isReading() const { return events_ & kReadEvent; }

    int index() { return index_; }
    void set_index(int idx) { index_ = idx; }

    // one loop per thread
    EventLoop *ownerLoop() { return loop_; }
    void remove();
private:

    void update();
    void handleEventWithGuard(Timestamp receiveTime);

    static const int kNoneEvent;
    static const int kReadEvent;
    static const int kWriteEvent;

    EventLoop *loop_; // 事件循环
    const int fd_;    // fd,Poller监听的对象
    int events_;      // 注册fd感兴趣的事件
    int revents_;     // Poller返回的具体发生的事件
    int index_;

    std::weak_ptr<void> tie_;
    bool tied_;

    // 因为channel通道里可获知fd最终发生的具体的事件events,所以它负责调用具体事件的回调操作
    ReadEventCallback readCallback_;
    EventCallback writeCallback_;
    EventCallback closeCallback_;
    EventCallback errorCallback_;
};

核心功能定位

  • 事件封装层
    封装文件描述符(fd_)及其关联的事件(读/写/错误/关闭),是 Reactor 模式中事件分发的核心单元。
  • 回调机制
    提供四类事件回调函数(读/写/错误/关闭),实现事件触发与业务逻辑解耦。
  • 生命周期管理
    通过 tie() 机制绑定 shared_ptr,确保事件处理期间对象不被意外销毁。

weak_ptr和shared_ptr区别

1. 所有权与生命周期控制

类型所有权影响引用计数资源释放条件
shared_ptr✅ 强所有权增加引用计数所有 shared_ptr 销毁时释放
weak_ptr❌ 无所有权不增加引用计数不参与生命周期管理

2. 资源访问方式

操作shared_ptrweak_ptr
直接访问资源✅ 通过 operator*/->❌ 不允许直接访问
安全访问机制-✅ 必须调用 lock() 升级为 shared_ptr
访问失败处理-检查 lock() 返回的指针是否为空
if (auto tmp = wp.lock()) { // 升级成功则对象存活  
  std::cout << *tmp;        // 安全访问  
} else {  
  std::cout << "对象已销毁";  
}  

智能指针的使用情况判断:

  1. 默认选择 shared_ptr:需要管理资源生命周期时使用
  2. 打破循环用 weak_ptr:存在双向引用风险时替换单向指针
  3. 回调安全必绑 weak_ptr:避免异步回调时对象已销毁(如网络库 Channel::tie 机制)

上述三个原则的理解

weak_ptr用处:

用weak_ptr绑定回调,相当于给异步操作加了个 “对象存活检测器”:
“您依赖的对象已下班,本次回调服务已自动取消”
避免强行访问已销毁对象导致的崩溃,是C++高性能程序的安全气囊

using与typedef使用区别

using EventCallback = std::function<void()>; // muduo仍使用typedef
using ReadEventCallback = std::function<void(Timestamp)>;

//等效写法
typedef std::function<void()> EventCallback;          // 等效于 using 版本1 
typedef std::function<void(Timestamp)> ReadEventCallback; // 等效于 using 版本2 
  • 代码注释 // muduo仍使用typedef 旧版 muduo 使用 typedef,但现代 C++(C++11 起)推荐使用 using。
  • using 的优势:
    • 语法更清晰,尤其是对函数指针或模板别名。
    • 支持模板别名(template using MyVec = std::vector;),而 typedef 不支持。

std::function <void()>

  • 实际类型:std::function
  • 含义:
    • 这是一个通用事件回调函数,不接收任何参数,也无返回值(void)。
    • 当某个事件(如连接关闭、定时器到期等)发生时,调用此回调函数。
  • 典型应用场景:
    • 定时器超时回调。
    • 连接关闭后的清理操作。
    • 其他无需额外数据的事件通知。
using EventCallback = std::function<void()>; 

EventCallback callback = []{ 
    std::cout << "Event occurred!" << std::endl; 
};
callback(); // 输出 "Event occurred!"

Poller代码分析

Poller里面是muduo库中多路事件分发器的核心IO复用模块

Poller ----> 事件监听器(专门用来观察文件描述符所要发生的事件)
本职:服务员,负责盯着自己负责的桌子,看哪个桌子的顾客有需求(事件),然后报告
核心在于报告(即上传自己监听到的事件)

Poller中核心函数

 // 给所有IO复用保留统一的接口
 virtual Timestamp poll(int timeoutMs, ChannelList *activeChannels) = 0;
 virtual void updateChannel(Channel *channel) = 0;
 virtual void removeChannel(Channel *channel) = 0;

核心功能:

  • updateChannel:添加/修改监听的通道事件
  • removeChannel:移除已监听的通道
  • 对应 epoll_ctl 的 ADD/MOD/DEL 操作

virtual void updateChannel(Channel *channel) = 0; 中的 = 0 语法表示这是一个纯虚函数

  1. 抽象接口声明
    = 0 表明该函数是抽象方法,只有声明没有实现(即无函数体)。它强制要求所有继承该类的子类必须重写此函数并提供具体实现。

  2. 抽象类标识
    包含纯虚函数的类自动成为抽象基类(Abstract Base Class),无法直接实例化对象。例如 Poller 类作为抽象基类,需通过子类(如 EPollPoller)实现功能:

    class Poller {  // 抽象基类
      virtual void updateChannel(Channel* channel) = 0;  // 纯虚函数
    };
    class EPollPoller : public Poller { 
      void updateChannel(Channel* channel) override;  // 子类必须实现 
    };
    
  3. 多态行为基础
    通过基类指针调用 updateChannel() 时,实际执行的是子类的实现,实现运行时多态。例如:

    Poller* poller = new EPollPoller();  // 基类指针指向子类对象 
    poller->updateChannel(channel);      // 调用 EPollPoller 的实现 
    

使用纯虚函数好处:新增多路复用机制(如 kqueue)只需继承 Poller 并重写纯虚函数,无需修改现有代码

函数传参()使用*指针

using ChannelList = std::vector<Channel *>;
virtual Timestamp poll(int timeoutMs, ChannelList *activeChannels) = 0;
ChannelList *activeChannels

数组传参,要想修改传递的值可以通过指针或引用来修改
所以上述代码传递的vector<Channel *>数组必须得要指针才能更改对应里面的值。

vector<Channel *> *activeChannels与vector<Channel *> &activeChannels无明显区别

EPoller代码分析

class Channel;

class EPollPoller : public Poller
{
public:
    //初始化epollfd_(epoll_create),epoll_events事件个数
    EPollPoller(EventLoop *loop);
    //关闭epollfd_操控epoll实例
    ~EPollPoller() override;

    //用来等待最终监控事件(读写or)的返回个数
    Timestamp poll(int timeoutMs, ChannelList *activeChannels) override;
    // 重写基类Poller的抽象方法
    void updateChannel(Channel *channel) override;	//用来更新CTL_ADD还是CTL_MOD
    void removeChannel(Channel *channel) override;	//用来移除Channel控制的文件描述符

private:
    static const int kInitEventListSize = 16;

    // 填写活跃的连接,也就是epoll_wait中响应的文件个数
    void fillActiveChannels(int numEvents, ChannelList *activeChannels) const;
    // 更新channel通道 其实就是调用epoll_ctl
    void update(int operation, Channel *channel);

    using EventList = std::vector<epoll_event>; // C++中可以省略struct 直接写epoll_event即可

    int epollfd_;      // epoll_create创建返回的fd保存在epollfd_中
    EventList events_; // 用于存放epoll_wait返回的所有发生的事件的文件描述符事件集
};

父类与子类的交互机制

一、父类与子类的核心关系

一、父类与子类的核心关系

关系类型说明代码示例
继承关系子类继承父类属性和方法class Child : public Parent {...}
方法重写子类覆盖父类虚函数实现多态
(必须重写override)
virtual void func() override {...}
类型层级父类指针可指向子类对象(向上转型)Parent* ptr = new Child();
构造/析构顺序父类构造→子类构造;子类析构→父类析构自动执行,不可逆
二、继承之后父类调用子类对象操作指南

向上转型举例使用 (即:父类指针调用子类成员 ):(前提是父类所写对象是: public/protected)

向上转型(Upcasting)深度解析:Parent* ptr = new Child();

核心概念说明

Parent* ptr = new Child();  // 父类指针指向子类对象 
  • 向上转型(Upcasting):将子类对象视为父类类型(安全且自动)

  • 多态基石:实现运行时动态绑定(虚函数机制)

  • 访问权限规则

    • ✅ 可访问:父类中的 public/protected 成员 (private类的成员无法访问)

    • ❌ 不可访问:子类扩展的新成员(如 fetch())

    • ⚠️ 危险操作:强转回子类指针需类型检查(上述问题的解决方法)

    • if (Dog* dogPtr = dynamic_cast<Dog*>(animalPtr)) {
          dogPtr->fetch();  // 安全调用子类方法 
      }
      
#include <iostream>
using namespace std;
 
// 父类:动物基类
class Animal {
public:
    virtual void speak() { 
        cout << "Animal sound!" << endl; 
    }
    virtual ~Animal() {}  // 虚析构确保正确销毁子类 
};
 
// 子类:狗(继承自动物)
class Dog : public Animal {
public:
    void speak() override {  // 重写父类虚函数
        cout << "Woof! Woof!" << endl; 
    }
    void fetch() {  // 子类特有方法
        cout << "Fetching ball..." << endl; 
    }
};
 
int main() {
    // 向上转型:父类指针管理子类对象
    Animal* animalPtr = new Dog();  
 
    // 多态调用:运行时绑定子类实现
    animalPtr->speak();  // 输出:Woof! Woof!(非 Animal sound!)
 
    // 错误示范:父类指针无法调用子类特有成员 除非使用dynamic_cast转换到子类对象来调用
    // animalPtr->fetch();  // 编译错误!
 
    // 安全释放内存(依赖虚析构链)
    delete animalPtr;  
    return 0;
}

父类所写对象为private之后的解决方法 ——>间接访问模式

方案1:添加公共访问接口(推荐)

class Animal {
private:
    virtual void speak() { /*...*/ }
public:
    void makeSound() { speak(); } // 公共接口 
    virtual ~Animal() {}
};
 
// 使用
animalPtr->makeSound(); // 输出"Woof! Woof!"(多态生效)

方案2:友元关系(谨慎使用)

class Animal {
private:
    virtual void speak() { /*...*/ }
    friend class SoundMaker; // 授权特定类访问 
};
 
class SoundMaker {
public:
    static void callSpeak(Animal* a) { a->speak(); }
};
 
// 使用
SoundMaker::callSpeak(animalPtr); // 输出"Woof! Woof!"

方案3:子类公开重写(需继承设计)

语法合法性
✅ 子类重写虚函数时,访问权限可以自由改变(private → public/protected)。
⚠️ 但父类的 private 虚函数无法在子类中直接调用(需通过父类接口间接调用)。

class Dog : public Animal {
public: // 重写为public
    void speak() override { /*...*/ } 
};
 
// 但父类指针调用仍需方案1的公共接口

epoll整个模拟流程

  1. 想象你住在一个大型小区(服务器程序),小区里有个快递驿站(epoll 实例,由 epoll_create 创建,对应 epfd)。
    每家每户(每个 Socket 连接)的快递(网络数据)都会送到这个驿站。

  2. epoll_ctl(ADD) 就像 住户到驿站登记需求:把自家门牌号(sockfd)和关注的事件(如 EPOLLIN)绑定到驿站的监控系统(epfd),之后驿站才会帮你盯快递!

  3. epoll_wait 就像驿站监控值班

驿站保安盯着监控屏(调用epoll_wait),一旦发现:

  • 202住户的大件快递到了 → 系统标记“202有事件!”
  • 202住户的快递被退回 → 系统标记“202有异常!”

通知住户取件
保安生成一张取件条(返回 events 数组),上面写着:
[ 门牌号:202, 事件:大件到了 ]
你凭条就能知道:“哦!我家快递到了,该去拿了!”

events 数组本质区别

函数events参数角色数据流向内容含义
epoll_ctl输入参数(仅用于配置)用户→内核期望监听的事件(如 EPOLLIN)
epoll_wait输出参数(存储结果)内核→用户实际发生的事件(如 EPOLLIN)
// ====== epoll_ctl 使用 ====== 
struct epoll_event ctl_event;             // 用户构造配置 
ctl_event.events = EPOLLIN | EPOLLET;     // 设置监听事件(输入)
ctl_event.data.fd = sockfd;               // 关联socket 
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ctl_event); // 提交配置 
 
// ====== epoll_wait 使用 ====== 
struct epoll_event wait_events[10];       // 预分配结果数组
int n = epoll_wait(epfd, wait_events, 10, 1000); // 接收事件(输出)
for (int i=0; i<n; i++) {
    // wait_events[i].events 含实际事件(如 EPOLLIN)
    // wait_events[i].data.fd 是就绪的socket fd 
}

核心区别:

epoll_ctl 主要就是将发生的sokct fd和事件类型放入到events数组中。(作为后期的监控标志)

而epoll_wait 则是只是用来判断是否后期的行为是否为events数组里面存储事件类型一模一样 (作为判断标志)

epoll_ctl分析

以下是 Linux 系统调用 epoll_ctl 的代码声明及其核心要素分析:

extern int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event) __THROW;

返回值含义

  • 0:操作成功(永不返回事件数/超时❗)
  • -1:操作失败(检查 errno 如 EBADF/EEXIST)

核心参数解析

参数类型作用说明典型值示例
__epfdintepoll 实例的文件描述符(由 epoll_create 创建)3(表示已打开的 epoll 实例)
__opint操作类型:添加、修改或删除监听事件EPOLL_CTL_ADD(添加新事件)
__fdint需要监听的目标文件描述符(如 socket对应的文件描述符、管道等)4(某个 socket 描述符)
__eventstruct epoll_event*事件配置结构体指针,定义监听的事件类型和回调数据指向自定义 epoll_event 的指针

操作类型 __op 详解 ——> 主要用于监控具体哪一类事件的发生(添加,修改,还是删除)

通过宏定义实现三种操作:

#define EPOLL_CTL_ADD 1  // 添加新监听事件 
#define EPOLL_CTL_MOD 2  // 修改已有事件 更新 已注册fd 的监听事件(如从 EPOLLIN 改为 EPOLLOUT)
#define EPOLL_CTL_DEL 3  // 删除事件监听 
EPOLL_CTL_ADD:       	// 注册新的fd到epfd中;
EPOLL_CTL_MOD:       	// 修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:       	// 从epfd中删除一个fd;
  • 示例场景:
    • 新连接接入 → EPOLL_CTL_ADD
    • 修改监听事件(如从读切换到写)→ EPOLL_CTL_MOD (modify)
    • 连接关闭 → EPOLL_CTL_DEL

事件结构体 epoll_event

定义于 sys/<epoll.h>:

typedef union epoll_data {
    void    *ptr;      // 用户自定义数据指针(常用)
    int      fd;       // 关联的文件描述符 
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
 
struct epoll_event {
    uint32_t     events;  // 事件掩码(EPOLLIN/EPOLLOUT 等)
    epoll_data_t data;    // 事件触发时的回调数据 
};
  • 关键事件掩码:(events) 指的是epoll_event里面的成员
    • EPOLLIN:数据可读
    • EPOLLOUT:数据可写
    • EPOLLERR:错误发生
    • EPOLLET:边缘触发模式(默认水平触发)

实际调用示例

添加 socket 可读事件监听:

int epfd = epoll_create1(0);  // 创建 epoll 实例
struct epoll_event ev;
ev.events = EPOLLIN;         // 监听读事件
ev.data.fd = sockfd;         // 关联 socket
 
// 将 socket 加入 epoll 监听    主要处理新增监听事件
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
    perror("epoll_ctl failed");
    exit(EXIT_FAILURE);
}

*ptr的优点分析

epoll_data联合体中的*ptr`设计是Linux高性能I/O模型的核心机制之一,其价值主要体现在性能优化和设计灵活性两大维度。

核心优势:打破fd映射的性能瓶颈

传统方案的性能缺陷

当epoll事件触发时,若仅通过data.fd传递文件描述符,开发者需额外维护全局映射表(如哈希表、红黑树)关联fd与对应上下文对象:

// 传统做法:每次事件触发需查表
int client_fd = event.data.fd;
Connection* conn = lookup_connection(fd_map, client_fd); // 哈希查找 O(1)但仍有开销 
process_data(conn);
  • 问题:高并发场景(如10万连接)下,查表成为性能瓶颈(CPU缓存失效、哈希冲突)

*ptr的颠覆性方案

// 注册事件时直接绑定上下文
Connection* conn = new Connection(fd); 
epoll_event ev;
ev.events = EPOLLIN;
ev.data.ptr = conn; // 指针直接关联对象
 
// 事件触发时零成本获取上下文 
Connection* conn = static_cast<Connection*>(event.data.ptr); // 直接访问 O(1)
process_data(conn);
  • 性能提升
    • 消除映射表查询开销(节省10-50ns/事件)
    • 避免哈希表内存占用(1万连接可节省约1.6MB内存)
    • 提升CPU缓存命中率(上下文对象连续访问)

*ptr的扩展性突破:支持非文件描述符事件 eg:定时器的实现

epoll_create分析

其中EPoller.h里面的epollfd_就是用epoll_create方法返回的epoll句柄。

变量 epollfd_ 存储着访问和管理你通过 epoll_create 创建的那个特定 epoll 事件监听中心 (epoll维护的内核) 的钥匙(文件描述符)。后续所有对 epoll 的操作 (epoll_ctl, epoll_wait) 都必须通过这把钥匙来指定你要操作的是哪个中心

  • epoll_create(int __size)
    • 参数 __size 是一个历史遗留的提示值(内核 ≥ 2.6.8 后已忽略此参数),仅用于兼容性。
    • 必须传入 大于 0 的值(如 1),否则会返回 EINVAL 错误。
  • epoll_create1(int __flags)
    • 移除了无用的 __size 参数。
    • 新增__flags参数,支持设置文件描述符标志:
      • EPOLL_CLOEXEC:在 exec 时(下方有解释)自动关闭文件描述符(避免泄漏到子进程)
      • FD_CLOEXEC 标志
        • 设置方式:
          • 通过 fcntl(fd, F_SETFD, FD_CLOEXEC) 手动设置。
          • 或使用 创建时标记(如 epoll_create1(EPOLL_CLOEXEC))。
        • 作用机制:
          当进程调用 exec() 时,内核自动关闭所有标记 FD_CLOEXEC 的文件描述符,确保新程序无法访问它们。
      • 若无需标志,可传 0。
int main() {
    int epfd = epoll_create1(EPOLL_CLOEXEC);  // 父进程创建epoll 
    pid_t pid = fork();                       // 创建子进程 
    
    if (pid == 0) {                           // 子进程代码块 
        execl("/bin/ls", "ls", NULL);         //  关键点:执行程序替换 
        // 此处代码不会执行(除非 exec 失败)  	 
    }
    // 父进程继续运行...
    return 0;
}
  1. 功能扩展
  • epoll_create1 额外支持 EPOLL_CLOEXEC 标志:
    在多线程/多进程编程中,此标志能防止文件描述符意外泄漏到 exec 后的子进程,提升安全性。
    示例:调用 epoll_create1(EPOLL_CLOEXEC) 后,fork+exec 时描述符自动关闭。
  • epoll_create 无标志控制:
    创建的文件描述符默认无 CLOEXEC,需手动调用 fcntl(fd, F_SETFD, FD_CLOEXEC) 设置。

exec 函数族详解:参数传递与环境变量处理

核心作用:所有 exec 函数均用于替换当前进程的代码和数据为新程序,但参数传递方式和环境变量处理存在显著差异。

(简单理解:execl 类函数的作用是:让当前程序“灵魂出窍”,彻底变成另一个程序,但保留它的“身体”(进程ID和资源)。)

epoll_wait分析

extern int epoll_wait (int __epfd, struct epoll_event *__events, 
                       int __maxevents, int __timeout);
参数作用技术细节
__epfdepoll 实例的文件描述符(由 epoll_create 创建)内核通过此标识符定位事件监控池
__events输出参数,存储就绪事件的数组需用户预分配内存,内核填充触发事件的详细信息
__maxevents期望返回的最大事件数量必须 ≤ __events 数组长度,避免缓冲区溢出
__timeout等待超时时间(毫秒)-1:阻塞等待;0:立即返回;>0:最长等待时间
返回值>0:就绪事件数;0:超时无事件;-1:错误(需检查 errno)常见错误:EBADF(epfd 非法)、EINTR(被信号中断)、EFAULT(缓冲区不可访问)

返回值

返回值含义后续操作
> 0就绪事件数量 • 表示有 N 个 Socket 发生监听的事件 • N 最大不超过传入的 __maxevents 参数需遍历 events[0] 到 events[N-1] 处理事件
0超时无事件 • 在 __timeout 毫秒内无任何事件触发可继续调用 epoll_wait 等待或执行其他逻辑
-1发生错误 • 需通过 errno 获取错误码必须检查 errno 定位问题

EventLoop代码分析

核心功能:事件循环引擎

  • 作用:EventLoop 是网络编程中的事件驱动模型核心,用于监听、分发和处理 I/O 事件(如 Socket 可读/可写)和定时任务。
  • 工作流程:
    1. 通过 loop() 启动循环,调用 Poller 监听注册的文件描述符(如 Socket)。
    2. 当事件发生时,Poller 返回有事件的 Channel 列表(activeChannels_)。
    3. 遍历 activeChannels_,执行每个 Channel 绑定的回调函数(如读/写处理)handleEvent() 。
    4. 执行其他线程投递的任务(pendingFunctors_)如:数据库读写、日志记录。
      • 通过 eventfd 实现的唤醒文件描述符,wakeup()用于跨线程唤醒事件循环。
      • 强制 Poller 提前返回—>epoll_wait(),优先处理新投递的任务队列,节省CPU资源(对比忙等待)
  • 子线程主要作用:
    • 应对自身线程epoll_wait()发生的对应I/O事件外
    • 还要处理其他线程的事件(数据库/日志记录)

其他线程向 wakeupFd_ 写入数据后,Poller 返回 wakeupFd_ 的可读事件,从而唤醒事件循环。

唤醒事件唤醒过程唤醒后操作
唤醒事件其他线程向 wakeupFd_ 写入数据唤醒事件循环
唤醒过程Poller 返回 wakeupFd_ 的可读事件唤醒事件循环
唤醒后操作处理任务队列恢复阻塞等待
#pragma once
#include <functional>
#include <vector>
#include <atomic>
#include <memory>
#include <mutex>
#include "noncopyable.h"
#include "Timestamp.h"
#include "CurrentThread.h"

class Channel;
class Poller;
// 事件循环类 主要包含了两个大模块 Channel Poller(epoll的抽象)
class EventLoop : noncopyable
{
public:
    using Functor = std::function<void()>;

    EventLoop();
    ~EventLoop();

    // 开启事件循环
    void loop();
    // 退出事件循环
    void quit();

    Timestamp pollReturnTime() const { return pollRetureTime_; }

    // 在当前loop中执行,立即执行回调
    void runInLoop(Functor cb);
    // 把上层注册的回调函数cb放入队列中 唤醒loop所在的线程执行cb
    void queueInLoop(Functor cb);

    // 通过eventfd唤醒loop所在的线程
    void wakeup();

    // EventLoop的方法 => Poller的方法
    void updateChannel(Channel *channel);
    void removeChannel(Channel *channel);
    bool hasChannel(Channel *channel);

    // 判断EventLoop对象是否在自己的线程里
    bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); } // threadId_为EventLoop创建时的线程id CurrentThread::tid()为当前线程id

private:
    void handleRead();        // 给eventfd返回的文件描述符wakeupFd_绑定的事件回调 当wakeup()时 即有事件发生时 调用handleRead()读wakeupFd_的8字节 同时唤醒阻塞的epoll_wait
    void doPendingFunctors(); // 执行上层回调

    using ChannelList = std::vector<Channel *>;

    std::atomic_bool looping_; // 原子操作 底层通过CAS实现
    std::atomic_bool quit_;    // 标识退出loop循环

    const pid_t threadId_; // 记录当前EventLoop是被哪个线程id创建的 即标识了当前EventLoop的所属线程id

    Timestamp pollRetureTime_; // Poller返回发生事件的Channels的时间点
    std::unique_ptr<Poller> poller_;

    int wakeupFd_; // 作用:当mainLoop获取一个新用户的Channel 需通过轮询算法选择一个subLoop 通过该成员唤醒subLoop处理Channel
    std::unique_ptr<Channel> wakeupChannel_;

    ChannelList activeChannels_; // 返回Poller检测到当前有事件发生的所有Channel列表

    std::atomic_bool callingPendingFunctors_; // 标识当前loop是否有需要执行的回调操作
    std::vector<Functor> pendingFunctors_;    // 存储loop需要执行的所有回调操作
    std::mutex mutex_;                        // 互斥锁 用来保护上面vector容器的线程安全操作
};

构造函数启动与析构函数

// 防止一个线程创建多个EventLoop
__thread EventLoop *t_loopInThisThread = nullptr;

int createEventfd()
{
    int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    if (evtfd < 0)
    {
        LOG_FATAL("eventfd error:%d\n", errno);
    }
    return evtfd;
}

EventLoop::EventLoop()	//初始化对应的成员变量
    : looping_(false)	//作为事件循环的标志
    , quit_(false)		//退出循环的标识
    , callingPendingFunctors_(false)	//执行其他线程投递的任务(数据库读写/日志记录)
    , threadId_(CurrentThread::tid())	//记录线程id,为后期作为一个线程对应一个loop作为一个判断
    , poller_(Poller::newDefaultPoller(this))	//封装一个智能指针unique_ptr指向Poller
    , wakeupFd_(createEventfd())		//轻量级进程间通信(IPC)机制,调用上面的函数,并记录标识
    , wakeupChannel_(new Channel(this, wakeupFd_))
    //把eventfd创建的标识封装到Channel中
{
    //线程唯一性检查
    LOG_DEBUG("EventLoop created %p in thread %d\n", this, threadId_);
    if (t_loopInThisThread)// t_loopInThisThread是线程局部变量
    {
        LOG_FATAL("Another EventLoop %p exists in this thread %d\n", t_loopInThisThread, threadId_);
    }
    else
    {
        t_loopInThisThread = this;
    }
    
    wakeupChannel_->setReadCallback(
        std::bind(&EventLoop::handleRead, this)); // 绑定回调(this指定调用对象)
        // 设置wakeupfd的事件类型以及发生事件后的回调操作
    
    wakeupChannel_->enableReading(); // 注册EPOLLIN事件 
    // 每一个EventLoop都将监听wakeupChannel_的EPOLL读事件了
}

//
EventLoop::~EventLoop()
{
    wakeupChannel_->disableAll(); // 给Channel移除所有感兴趣的事件
    wakeupChannel_->remove();     // 把Channel从EventLoop上删除掉
    ::close(wakeupFd_);
    t_loopInThisThread = nullptr;
    //t_loopInThisThread是线程局部变量
}

特性全局变量线程局部变量(如 t_loopInThisThread)
作用域进程全局线程局部
内存共享性所有线程共享同一内存每个线程独占副本
线程安全性需加锁保护天然线程安全
典型应用场景跨线程共享配置线程上下文状态管理

核心:全局变量会被各种人共享,但是局部之间不会被影响,更加安全。

this指针

其中 this 指针始终指向当前正在构造的 EventLoop 对象自身。(永远指代的是EventLoop这个类自身)

  • 构造函数中this 的作用:

    • 在 poller_ 初始化时传入,使 Poller 知道其所属的 EventLoop
    • 在 wakeupChannel_ 初始化时传入,建立 Channel 与 EventLoop 的归属关系
  • 核心约束:一个线程最多只能有一个 EventLoop 对象

  • this 的作用:将当前对象地址存入线程局部存储 t_loopInThisThread

this 指针的核心作用总结

使用位置目的
Poller::newDefaultPoller(this)让 Poller 持有所属 EventLoop 的引用,用于后续事件回调
new Channel(this, ...)建立 Channel 与 EventLoop 的所属关系,事件触发时能正确路由
std::bind(..., this)确保非静态成员函数 handleRead() 能在正确的对象上下文中执行
t_loopInThisThread=this实现线程与 EventLoop 的 1:1 绑定,避免线程冲突
eventfd

Linux 提供的一种轻量级进程间通信(IPC)机制,主要用于高效的事件通知。其核心是通过计数器绑定文件描述符(fd),实现跨进程/线程的事件通知。

一、eventfd 的本质与结构

  • 事件通知专用:替代传统信号机制(如管道),开销更低。
  • fd 即计数器:通过 fd 传递整数值(8 字节),避免复杂数据结构。

二、eventfd 实现通信的底层机制

工作流程

  1. 写操作(通知)

    write(eventfd_fd, &value, sizeof(uint64_t)); // 计数器加 value
    
    • 写操作增加计数器的值,内部计数器变为非零 → 触发可读信号
    • 可读信号:当计数器 >0 时,fd 处于可读状态(触发 epoll/select 等 I/O 多路复用监听)。
  2. 读操作(响应)

    read(eventfd_fd, &buf, sizeof(uint64_t));    // 读取计数器值(并清零)
    
    • 读操作清空计数器 → 重置 fd 状态
    • 若计数器为 0,读操作默认阻塞(除非设为 EFD_NONBLOCK)。
#include <sys/eventfd.h>
/* 创建 eventfd 对象 */
int eventfd(unsigned int initval, int flags); 

参数:

  • initval:计数器初始值
  • flags:EFD_CLOEXEC | EFD_NONBLOCK 等标志
    • EFD_CLOEXEC:进程执行 exec 时关闭 fd。
    • EFD_NONBLOCK:非阻塞模式(读写立即返回)。
    • EFD_SEMAPHORE(可选):信号量模式(读操作减 1 而非清零)。
void EventLoop::handleRead()
{
    uint64_t one = 1;
    ssize_t n = read(wakeupFd_, &one, sizeof(one));
    if (n != sizeof(one))
    {
        LOG_ERROR("EventLoop::handleRead() reads %lu bytes instead of 8\n", n);
    }
}

// 用来唤醒loop所在线程 向wakeupFd_写一个数据 wakeupChannel就发生读事件 当前loop线程就会被唤醒
void EventLoop::wakeup()
{
    uint64_t one = 1;
    ssize_t n = write(wakeupFd_, &one, sizeof(one));
    if (n != sizeof(one))
    {
        LOG_ERROR("EventLoop::wakeup() writes %lu bytes instead of 8\n", n);
    }
}

loop循环调用

// 开启事件循环
void EventLoop::loop()
{
    looping_ = true;
    quit_ = false;

    LOG_INFO("EventLoop %p start looping\n", this);

    while (!quit_)
    {
        activeChannels_.clear();
        pollRetureTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
        for (Channel *channel : activeChannels_)
        {
            // Poller监听哪些channel发生了事件 然后上报给EventLoop 通知channel处理相应的事件
            channel->handleEvent(pollRetureTime_);
        }
        /**
         * 执行当前EventLoop事件循环需要处理的回调操作 对于线程数 >=2 的情况 IO线程 mainloop(mainReactor) 主要工作:
         * accept接收连接 => 将accept返回的connfd打包为Channel => TcpServer::newConnection通过轮询将TcpConnection对象分配给subloop处理
         *
         * mainloop调用queueInLoop将回调加入subloop(该回调需要subloop执行 但subloop还在poller_->poll处阻塞) queueInLoop通过wakeup将subloop唤醒
         **/
        doPendingFunctors();
    }
    LOG_INFO("EventLoop %p stop looping.\n", this);
    looping_ = false;
}

注意一定要记得标记清楚对应的looping_和quit_的状态

工作流程:

  1. 通过 loop() 启动循环,调用 Poller 监听注册的文件描述符(如 Socket),同时记录监听时间戳
  2. 当事件发生时,遍历 activeChannels_,执行每个 Channel 绑定的回调函数(如读/写处理)handleEvent() 。
  3. 执行其他线程投递的任务(pendingFunctors_)如:数据库读写、日志记录。
    • 通过 eventfd 实现的唤醒文件描述符,wakeup()用于跨线程唤醒事件循环。
    • 强制 Poller 提前返回—>epoll_wait(),优先处理新投递的任务队列,节省CPU资源(对比忙等待)

quit退出循环

/**
 * 退出事件循环
 * 1. 如果loop在自己的线程中调用quit成功了 说明当前线程已经执行完毕了loop()函数的poller_->poll并退出
 * 2. 如果不是当前EventLoop所属线程中调用quit退出EventLoop 需要唤醒EventLoop所属线程的epoll_wait
 *
 * 比如在一个subloop(worker)中调用mainloop(IO)的quit时 需要唤醒mainloop(IO)的poller_->poll 让其执行完loop()函数
 *
 * !!! 注意: 正常情况下 mainloop负责请求连接 将回调写入subloop中 通过生产者消费者模型即可实现线程安全的队列
 * !!!       但是muduo通过wakeup()机制 使用eventfd创建的wakeupFd_ notify 使得mainloop和subloop之间能够进行通信
 **/
void EventLoop::quit()
{
    quit_ = true;

    if (!isInLoopThread())
    {
        wakeup();
    }
}

同线程任务处理器RunInloop

void EventLoop::runInLoop(Functor cb)
{
    if (isInLoopThread()) // 当前EventLoop中执行回调
    {
        cb();
    }
    else // 在非当前EventLoop线程中执行cb,就需要唤醒EventLoop所在线程执行cb
    {
        queueInLoop(cb);
    }
}

是

否

调用 runInLoop

是否在EventLoop线程?

立即执行回调

通过queueInLoop加入任务队列

跨线程任务调度处理器queueInLoop和doPendingFunctors

// 把cb放入队列中 唤醒loop所在的线程执行cb
void EventLoop::queueInLoop(Functor cb)
{
    {
        std::unique_lock<std::mutex> lock(mutex_);
        pendingFunctors_.emplace_back(cb);
    }

    /**
     * || callingPendingFunctors的意思是 当前loop正在执行回调中 但是loop的pendingFunctors_中又加入了新的回调 需要通过wakeup写事件
     * 唤醒相应的需要执行上面回调操作的loop的线程 让loop()下一次poller_->poll()不再阻塞(阻塞的话会延迟前一次新加入的回调的执行),然后
     * 继续执行pendingFunctors_中的回调函数
     **/
    if (!isInLoopThread() || callingPendingFunctors_)
    {
        wakeup(); // 唤醒loop所在线程
    }
}


void EventLoop::doPendingFunctors()
{
    std::vector<Functor> functors;
    callingPendingFunctors_ = true;

    {
        std::unique_lock<std::mutex> lock(mutex_);
        functors.swap(pendingFunctors_); // 交换的方式减少了锁的临界区范围 提升效率 同时避免了死锁 如果执行functor()在临界区内 且functor()中调用queueInLoop()就会产生死锁
    }

    for (const Functor &functor : functors)
    {
        functor(); // 执行当前loop需要执行的回调操作
    }

    callingPendingFunctors_ = false;
}

核心点:一定要记得及时加锁,保证在当前工作中不能让资源收到其他线程的影响

doPendingFunctors设计技巧:

技巧原理解决的问题
交换队列通过 swap() 将 pendingFunctors_ 内容转移到局部变量 functors临界区从 O(n) 缩短到 O(1)
锁范围最小化锁只保护交换操作(几纳秒),而非整个任务执行过程避免任务执行时阻塞其他线程
死锁预防执行任务时 不持有锁,任务中调用 queueInLoop() 不会触发重复加锁防止嵌套调用导致的死锁

为什么必须用 swap?

若直接遍历 pendingFunctors_ 执行:

//  错误示范(导致死锁或阻塞)
{
    std::lock_guard lock(mutex_);
    for (auto& fn : pendingFunctors_) {
        fn(); // 若fn内部调用queueInLoop(),会再次请求锁!
    }
    pendingFunctors_.clear();
}

问题分析:

  1. 死锁风险:
    任务函数中调用 queueInLoop() → 需获取已被持有的 mutex_ → 线程永久阻塞。
  2. 性能瓶颈:
    长任务导致锁长期占用,其他线程无法添加新任务。
functors 事件循环线程 工作线程 functors 事件循环线程 工作线程 Poller被唤醒 期间新任务直接入队 pendingFunctors_ queueInLoop(任务1) queueInLoop(任务2) queueInLoop(任务3) doPendingFunctors() swap(pendingFunctors_) 执行任务1/2/3

线程一致性检查的设计必要性

代码中执着于 isInLoopThread() 检查,原因在于:

(1) 🌟 线程安全约束(核心原因)

EventLoop 是 线程绑定对象(one-loop-per-thread 模型),关键操作需在创建线程执行:

  • updateChannel/removeChannel:修改监听的 fd,若跨线程操作会导致:
    • 竞态条件(如正在处理事件时删除 Channel)。
    • epoll 相关函数非线程安全(需保证调用线程与 epoll_wait 线程一致)。
  • runInLoop:若在非创建线程调用,需通过 queueInLoop 转移任务。

(2) 避免锁的开销

  • 通过强制线程绑定,省去 Poller 和 Channel 操作的锁机制,提升性能。
  • 仅 pendingFunctors_ 需互斥锁(因可能被多线程写入)。

(3) 事件循环状态一致性

  • looping_ 和 quit_ 等标志的修改需原子性,但组合操作仍需线程约束(如 quit() 需同步停止 Poller)。

上完结——

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐