muduo库学习所得(上)
muduo库学习所得
Reactor模型
多 Reactor 多进程 / 线程方案的示意图

一、MainReactor与SubReactor的区别
| 维度 | MainReactor | SubReactor |
|---|---|---|
| 核心职责 | 仅处理新连接建立事件(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目标事件 | 线程模型 | 核心任务 |
|---|---|---|---|
| MainReactor | OP_ACCEPT | 单/少量线程 | 高效接收新连接并分配 |
| SubReactor | OP_READ/WRITE | 多线程 | 处理已连接Socket的I/O事件 |
知识点扩充
LT和ET模式的区别:
- LT 模式(水平触发):像“唠叨的闹钟”——事件没处理完就一直提醒你,直到你搞定为止。
- ET 模式(边缘触发):像“高冷的通知员”——事件发生时只提醒一次,爱理不理随你便,漏了后果自负。
静态库和动态库后缀
| 库类型 | Linux 后缀 | Windows 后缀 |
|---|---|---|
| 静态库 | .a | .lib |
| 动态库 | .so | .dll |
Cmake中set(静态/动态)库分析
- 静态库必用
CMAKE_ARCHIVE_OUTPUT_DIRECTORY - 共享库使用
CMAKE_LIBRARY_OUTPUT_DIRECTORY - 项目路径规范:
/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 个核心功能)
- 替
fd记住“关心的事件”
比如某个fd只关心“收到数据”(读事件),Channel 会帮它记下来:“这个fd只听‘收数据’的消息”。- 替
fd对接“监控系统”
Channel 会把fd和它关心的事件,统一注册到“监控系统”(比如epoll),不用你手动调用epoll_ctl这种复杂命令。- 替
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_ptr | weak_ptr |
|---|---|---|
| 直接访问资源 | ✅ 通过 operator*/-> | ❌ 不允许直接访问 |
| 安全访问机制 | - | ✅ 必须调用 lock() 升级为 shared_ptr |
| 访问失败处理 | - | 检查 lock() 返回的指针是否为空 |
if (auto tmp = wp.lock()) { // 升级成功则对象存活
std::cout << *tmp; // 安全访问
} else {
std::cout << "对象已销毁";
}
智能指针的使用情况判断:
- 默认选择
shared_ptr:需要管理资源生命周期时使用 - 打破循环用
weak_ptr:存在双向引用风险时替换单向指针 - 回调安全必绑
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 语法表示这是一个纯虚函数
-
抽象接口声明
= 0表明该函数是抽象方法,只有声明没有实现(即无函数体)。它强制要求所有继承该类的子类必须重写此函数并提供具体实现。 -
抽象类标识
包含纯虚函数的类自动成为抽象基类(Abstract Base Class),无法直接实例化对象。例如Poller类作为抽象基类,需通过子类(如EPollPoller)实现功能:class Poller { // 抽象基类 virtual void updateChannel(Channel* channel) = 0; // 纯虚函数 }; class EPollPoller : public Poller { void updateChannel(Channel* channel) override; // 子类必须实现 }; -
多态行为基础
通过基类指针调用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整个模拟流程
-
想象你住在一个大型小区(服务器程序),小区里有个快递驿站(epoll 实例,由
epoll_create创建,对应epfd)。
每家每户(每个 Socket 连接)的快递(网络数据)都会送到这个驿站。 -
epoll_ctl(ADD)就像 住户到驿站登记需求:把自家门牌号(sockfd)和关注的事件(如EPOLLIN)绑定到驿站的监控系统(epfd),之后驿站才会帮你盯快递! -
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)
核心参数解析
| 参数 | 类型 | 作用说明 | 典型值示例 |
|---|---|---|---|
__epfd | int | epoll 实例的文件描述符(由 epoll_create 创建) | 3(表示已打开的 epoll 实例) |
__op | int | 操作类型:添加、修改或删除监听事件 | EPOLL_CTL_ADD(添加新事件) |
__fd | int | 需要监听的目标文件描述符(如 socket对应的文件描述符、管道等) | 4(某个 socket 描述符) |
__event | struct 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;
}
- 功能扩展
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);
| 参数 | 作用 | 技术细节 |
|---|---|---|
__epfd | epoll 实例的文件描述符(由 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 可读/可写)和定时任务。 - 工作流程:
- 通过
loop()启动循环,调用Poller监听注册的文件描述符(如 Socket)。 - 当事件发生时,
Poller返回有事件的Channel列表(activeChannels_)。 - 遍历
activeChannels_,执行每个Channel绑定的回调函数(如读/写处理)handleEvent()。 - 执行其他线程投递的任务(
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 实现通信的底层机制
工作流程
-
写操作(通知)
write(eventfd_fd, &value, sizeof(uint64_t)); // 计数器加 value- 写操作增加计数器的值,内部计数器变为非零 → 触发可读信号
- 可读信号:当计数器 >0 时,fd 处于可读状态(触发
epoll/select等 I/O 多路复用监听)。
-
读操作(响应)
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_的状态
工作流程:
- 通过
loop()启动循环,调用Poller监听注册的文件描述符(如 Socket),同时记录监听时间戳 - 当事件发生时,遍历
activeChannels_,执行每个Channel绑定的回调函数(如读/写处理)handleEvent()。 - 执行其他线程投递的任务(
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);
}
}
跨线程任务调度处理器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();
}
问题分析:
- 死锁风险:
任务函数中调用queueInLoop()→ 需获取已被持有的mutex_→ 线程永久阻塞。 - 性能瓶颈:
长任务导致锁长期占用,其他线程无法添加新任务。
线程一致性检查的设计必要性
代码中执着于 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)。
上完结——
更多推荐
所有评论(0)