Muduo的本质:
Muduo 是一个基于 Reactor 模式、One loop per thread 线程模型的高性能 C++ 网络库。
| 关键词 | 对应章节 | 大白话 |
|---|---|---|
| Reactor 模式 | 第一章 | 事件驱动,来了事件才干活,不傻等 |
| One loop per thread | 第四章 | 一个线程配一个事件循环 |
| 高性能 | 各章 | 非阻塞 + epoll + 缓冲区复用 |
整个库的零件清单如下,后面逐章拆解:
| 零件 | 职责 | 一句话 |
|---|---|---|
EventLoop | 事件循环 | 大总管,负责分发所有事件 |
Channel | 事件载体 | 每个 fd 配一个,知道"我对什么事件感兴趣" |
Buffer | 数据缓冲区 | 每个连接配一个,暂存收发数据 |
Poller | 多路复用器 | 封装 epoll,负责"等事件" |
TimerQueue | 定时器队列 | 管理所有定时任务 |
Acceptor | 连接接收器 | 只干一件事:accept 新连接 |
TcpConnection | 连接对象 | 代表一条已建立的连接 |
TcpServer | 服务器 | 把上面所有零件组装起来 |
简记:
TcpServer是工厂,Acceptor是门卫,EventLoop是车间主任,Channel是工人,Buffer是工人手里的货筐。
一、Reactor 模型
1. 什么是 Reactor
通俗解释:传统 I/O 像"派个人守在门口,挨个问’你好了没’",Reactor 像"装了个门铃,谁按门铃我才去开门"。
Reactor 是一种基于事件驱动的 I/O 处理模式:
- 程序不主动轮询每个连接。
- 把连接交给内核的 I/O 复用机制(
epoll/poll)监听。 - 事件就绪了,内核通知程序,程序再去处理。

2. 三种 Reactor 变体
Reactor 按线程数量演进出三种形态,一张表对比:
| 变体 | 线程结构 | 谁 accept | 谁处理读写 | 瓶颈 |
|---|---|---|---|---|
| 单 Reactor 单线程 | 1 个线程 | 同线程 | 同线程 | 任意一处阻塞全卡死 |
| 单 Reactor 多线程 | 1 接收 + N 工作 | 接收线程 | 工作线程池 | accept 仍是单点 |
| 多 Reactor 多线程 | 1 主 + N 子 | 主 Reactor | 子 Reactor 各管一摊 | 无明显瓶颈,主流方案 |
Muduo采用的就是第三种:多 Reactor 多线程。
3. Muduo 的多 Reactor 分工

- 主 Reactor(Main Reactor):只
accept新连接,做最轻的事。 - 子 Reactor(Sub Reactor):每个子线程跑一个
EventLoop,各自负责一部分连接的读写。 - 连接处理器(Connection Handler):把新连接轮询分配到某个子 Reactor,实现负载均衡。
一句话:主 Reactor 只"接客",子 Reactor 负责"干活",各忙各的,互不阻塞。
4. Reactor vs Proactor
| 对比项 | Reactor | Proactor |
|---|---|---|
| I/O 操作 | 同步(内核只通知"就绪",程序自己 read/write) | 异步(内核直接完成 read/write,再通知"完成") |
| 代表 | libevent、libuv、Muduo、Netty、Redis | IOCP(Windows)、Boost.Asio |
| 复杂度 | 简单、主流 | 复杂,依赖操作系统异步 I/O 支持 |
5. 为什么底层选择使用 epoll
| 对比项 | select / poll | epoll |
|---|---|---|
| 查找就绪事件 | 每次遍历所有 fd(O(n)) | 直接返回就绪列表(O(1)) |
| fd 数量上限 | select 默认 1024 | 无上限 |
| 拷贝 | 每次把 fd 集合在用户态/内核态来回拷 | 只注册一次 |
| 触发方式 | 仅水平触发 | 支持水平 + 边缘 |
水平触发(LT):只要缓冲区还有数据,就反复通知,不漏数据但可能重复。 边缘触发(ET):只在状态变化时通知一次,高效但要一次读干净。
Muduo 用的是 LT + 非阻塞 I/O,稳妥不易出错。
简记:
- 为什么不用 select?—— O(n) 遍历 + fd 上限 + 每次拷贝。
- LT 和 ET 的区别?—— LT 反复通知,ET 只通知一次。
- Muduo 为什么选 LT?—— 简单可靠,配合非阻塞读也不会出错。
二、Socket 编程关键点
1. TCP 连接与 Socket API 的对应

| 步骤 | Socket API | 说明 |
|---|---|---|
| 建立连接 | connect() / accept() | 背后是 TCP 三次握手 |
| 传输数据 | send() / recv() | 背后是内核缓冲区的搬运 |
| 关闭连接 | close() | 背后是 TCP 四次挥手 |
2. TIME_WAIT 与 SO_REUSEADDR
通俗解释:服务器 close() 后,端口不会立刻释放,而是进入 TIME_WAIT 状态"停留几十秒",防止旧连接的迟到数据干扰新连接。如果此时重启服务器,会报 Address already in use。
SO_REUSEADDR:允许新 Socket 绑定到仍处于 TIME_WAIT 的端口。- 作用:服务器重启后能立刻再绑定同一个端口,不用干等几十秒。
服务器几乎必开
SO_REUSEADDR。
3. 粘包 / 拆包
通俗解释:TCP 是"字节流",没有消息边界。你发两个"包裹",对端可能一次收到一个半,或两个一起收到。

三种常见解决方案:
| 方案 | 做法 | 优缺点 |
|---|---|---|
| 定长包 | 每个包固定长度 | 简单,但浪费空间 |
| 分隔符 | 用特殊字符隔开(如 \r\n) | 简单,但数据里不能出现分隔符 |
| 长度前缀 | 包头 4 字节存长度 + 数据体 | 最通用,Muduo 推荐 |
4. SO_KEEPALIVE vs 应用层心跳
| 对比项 | SO_KEEPALIVE | 应用层心跳 |
|---|---|---|
| 谁实现 | 内核 | 应用程序 |
| 默认探测间隔 | 很长(通常 2 小时) | 自定义(如 30 秒) |
| 能发现对端"进程死了" | 能 | 能 |
| 能发现对端"进程活着但业务卡死" | 不能 | 能 |
结论:SO_KEEPALIVE 是兜底,业务上通常还要自己做应用层心跳。
5. send / recv 的正确姿势
| 情况 | 返回值 | 正确处理 |
|---|---|---|
recv 返回 0 | 对端关闭了连接 | 关闭连接 |
recv 返回 -1 且 errno==EAGAIN | 缓冲区暂时无数据 | 等下次事件,不是错误 |
send 返回 -1 且 errno==EAGAIN | 缓冲区满 | 关注写事件,稍后再发 |
send 返回 < 预期长度 | 只发了一部分 | 把剩余数据存起来继续发 |
关键:非阻塞 I/O 下,
EAGAIN不是错误,是"现在没数据,别急"。这是最容易误解的点。
三、Channel —— 事件的载体
1. Channel 是什么
通俗解释:每个 fd 配一个 Channel,它就像 fd 的"秘书"——记着"老板对什么事件感兴趣"(events_),以及"刚才发生了什么事件"(revents_)。

核心规则:一个 Channel 始终对应一个 fd,两者生命周期绑定。
2. 关键成员深挖
| 成员 | 类型 | 含义 | 大白话 |
|---|---|---|---|
fd_ | int | 监听的 fd | 盯哪个人 |
events_ | int(位掩码) | 感兴趣的事件 | 我想知道啥 |
revents_ | int(位掩码) | 就绪的事件 | 刚才发生了啥 |
index_ | int | 在 Poller 中的状态 | 门卫登记没 |
tie_ | weak_ptr | 防悬空 | 防止主人没了 |
events_ / revents_ 的位掩码(用二进制的位表示不同事件):
| 事件宏 | 含义 |
|---|---|
POLLIN / EPOLLIN | 可读 |
POLLOUT / EPOLLOUT | 可写 |
POLLERR | 出错 |
POLLHUP | 挂断(对端关闭) |
events_是我主动注册的,revents_是 epoll 返回的。epoll_wait 后,revents_会被内核填上实际就绪的事件。
3. tie_ 如何防悬空(重点)
场景:Channel 的回调里可能要用到所属的 TcpConnection 对象。如果连接已经销毁了,回调还在跑,就会访问已释放的内存(悬空指针),导致崩溃。
解决办法:tie_ 是一个 std::weak_ptr,指向所属的连接对象。

关键点:Channel 不拥有 TcpConnection,只持有
weak_ptr,这样连接销毁时 Channel 能感知到,避免悬空。
4. Channel 的状态机
Channel 在 Poller 中有三种状态,用 index_ 记录:
| 状态 | 值 | 含义 |
|---|---|---|
kNew | -1 | 还没注册到 Poller |
kAdded | >= 1 | 已注册 |
kDeleted | 2 | 已注册但待删除 |
常用方法:
| 方法 | 作用 |
|---|---|
enableReading() | 关注读事件,events_ |= POLLIN |
enableWriting() | 关注写事件,events_ |= POLLOUT |
disableAll() | 取消所有关注 |
setReadCallback() | 绑定读回调 |
5. 回调绑定与分发
Channel 上挂着几个回调函数,EventLoop 分发事件时直接调用:
| 回调 | 触发时机 |
|---|---|
readCallback_ | 可读事件就绪 |
writeCallback_ | 可写事件就绪 |
closeCallback_ | 连接关闭 |
errorCallback_ | 出错 |
分发逻辑一句话:
epoll_wait返回就绪事件 → 找到对应 Channel → 按revents_调用对应回调。
简记:
events_和revents_的区别?—— 前者是"关心",后者是"实际发生"。tie_为什么用 weak_ptr?—— 防连接销毁后回调悬空。- Channel 和 fd 的关系?—— 一一对应。
四、EventLoop —— 事件循环(核心)
1. 什么是 EventLoop
通俗解释:EventLoop 是"车间主任",它永远在一个循环里干活——“等事件 → 处理事件 → 再等”。

2. One loop per thread(核心原则)
一个线程至多有一个 EventLoop,一个 EventLoop 只属于一个线程。
为什么这样设计?
| 好处 | 说明 |
|---|---|
| 无锁 | 所有操作都在本线程,不需要加锁 |
| 简单 | 不用考虑跨线程访问数据的安全问题 |
| 高效 | 避免线程切换和锁竞争 |
这就是 Muduo 高性能的根基:用"一个线程一个 loop"换掉"一把大锁"。
3. runInLoop / queueInLoop(源码级)
因为 EventLoop 属于某个线程,别的线程不能直接操作它。所以提供两个安全入口:
void EventLoop::runInLoop(Functor cb) {
if (isInLoopThread()) { // 就在 loop 线程里
cb(); // 直接执行
} else {
queueInLoop(cb); // 塞进队列,唤醒 loop
}
}
void EventLoop::queueInLoop(Functor cb) {
pendingFunctors_.push_back(cb); // 加入任务队列
if (!isInLoopThread() || callingPendingFunctors_) {
wakeup(); // 唤醒阻塞中的 loop
}
}
两者的区别:
| 接口 | 语义 | 适用场景 |
|---|---|---|
runInLoop | 在 loop 线程里执行;不在则转投队列 | 用户想"确保在 loop 线程里跑" |
queueInLoop | 无条件丢进队列,异步执行 | 不需要立即执行,排队即可 |
4. wakeup() 与 eventfd(唤醒机制)
问题:loop 线程正阻塞在 epoll_wait 上睡觉,别的线程往队列塞了任务,怎么叫醒它?
答案:用 eventfd 创建一个"唤醒管道",把它也注册进 epoll。

eventfd比 pipe 轻量,且是 Linux 专门为"线程间唤醒"设计的。
5. EventLoopThread / EventLoopThreadPool
多 Reactor 需要多个 loop 线程,靠这两个类管理:
| 类 | 作用 |
|---|---|
EventLoopThread | 封装"一个线程 + 一个 EventLoop"的启动与停止 |
EventLoopThreadPool | 管理多个 EventLoopThread,提供轮询分配 |
分配策略:TcpServer 拿到新连接后,从池子里**轮询(round-robin)**挑一个 loop 给它。
新连接1 → 子loop0
新连接2 → 子loop1
新连接3 → 子loop2
新连接4 → 子loop0 ← 轮询回来了
简记:
- One loop per thread 为什么?—— 避免加锁,简化模型,提高性能。
runInLoop和queueInLoop区别?—— 前者同线程直接跑,后者总是排队。- 怎么唤醒阻塞的 loop?—— eventfd,写字节触发 epoll 可读。
五、Buffer —— 数据缓冲区
1. 为什么需要 Buffer
通俗解释:TCP 是字节流,一次 recv 读到的数据量不确定。你得有个"货筐"先把数据接着,攒够了再按业务处理。
Muduo 里,每个 TcpConnection 都有一个输入 Buffer 和一个输出 Buffer。
2. Buffer 的内存布局(核心)
Buffer 底层用 vector<char>,用两个下标管理数据,布局如下:

| 下标 | 含义 |
|---|---|
readIndex_ | 可读数据的起点 |
writeIndex_ | 可写空间的起点 |
readIndex_和writeIndex_之间的,是已写入、还没读走的数据。readIndex_前面留了一段prependable空间。
3. prepend 区的作用
为什么读数据前面要留 8 字节?
当需要在数据前面插入东西(比如粘包处理里补一个长度头)时,可以直接往 prepend 区写,不用把整个数据后移,省一次拷贝。
4. Buffer 的四种状态

关键:读写都是在原地滑动指针,不用反复
malloc/free,这就是 Buffer 高效的原因。
5. 分散读 readv(源码级)
readFd 用 readv 一次读多个缓冲区,减少系统调用:
ssize_t Buffer::readFd(int fd, int* savedErrno) {
char extrabuf[65536];
struct iovec vec[2];
vec[0].iov_base = writable(); // 主缓冲区可写区
vec[0].iov_len = writableBytes();
vec[1].iov_base = extrabuf; // 栈上备用缓冲
vec[1].iov_len = sizeof(extrabuf);
// 一次 readv,能读多少读多少
ssize_t n = readv(fd, vec, 2);
...
}
好处:数据量小时直接读进 Buffer;数据量大时溢出到
extrabuf,一次系统调用搞定,不用先探测再读。
6. 扩容策略
- Buffer 的
writable空间不够时,会自动扩容(vector增长)。 - 扩容时通常按需增长,而不是固定翻倍,避免内存浪费。
7. 基于 Buffer 拆包
结合第二章的粘包问题,用 Buffer 的 readIndex_ 就能实现拆包:
recv 数据 → 追加到 Buffer
→ 检查 Buffer 里可读数据够不够一个完整包(长度前缀)
→ 够 → 取出来处理,readIndex_ 前移
→ 不够 → 继续等下一次事件
这就是"长度前缀"方案在 Muduo 上的落地:Buffer 负责攒数据,业务负责判断边界。
简记:
- 为什么用两个下标不用一个?—— 支持"读了不消费"和 prepend 预留。
- prepend 区干嘛的?—— 前面插数据不用搬移。
- readv 干嘛的?—— 一次系统调用读多块缓冲。
六、Timer —— 定时器
1. Timer 是什么
通俗解释:让程序能"到点了自动干某件事",是心跳、超时、限流等功能的地基。
2. 常见应用场景
| 场景 | 说明 |
|---|---|
| 定时发送 | 周期上报、定时推送 |
| 会话超时 | 连接长时间无响应就清理 |
| 心跳包 | 定期发心跳,检测对端是否存活 |
| 限流控制 | 按时间窗口控制速率 |
| 延迟重试 | 失败后隔一段时间再试 |
3. Timer 相关组件
| 组件 | 作用 |
|---|---|
Timer | 单个定时器:到期时间 + 回调 + 是否周期 |
TimerQueue | 定时器队列,按到期时间排序 |
TimerId | 定时器句柄,用于取消 |
timerfd | 内核定时器,用于唤醒 loop |
4. TimerQueue 的底层结构(源码级)
TimerQueue 内部用 std::set 存储定时器,按到期时间排序:
std::set<Timer> (按 expiration 排序)
├── Timer(10:00:01) ← 最近到期
├── Timer(10:00:05)
└── Timer(10:00:30)
↑
每次只看最前面一个,到期就执行
用
set的好处:最早到期的永远在最前面,取出来 O(1),插入/删除 O(log n)。
5. timerfd_create 的作用
问题:loop 线程阻塞在 epoll_wait,怎么在"定时器到期"时精确醒来?
答案:用 timerfd_create 创建一个内核定时器,到期时它对应的 fd 会变可读,正好能被 epoll 监听到。
定时器到期 → 内核把 timerfd 置为可读 → epoll 返回 → loop 醒来处理定时任务
这样定时器事件和 I/O 事件统一了——都通过 epoll 通知,loop 不需要额外 sleep 或轮询时间。
6. 定时器的生命周期
| 操作 | 行为 |
|---|---|
addTimer | 新建 Timer 插入 set,更新 timerfd 的到期时间 |
cancel | 从 set 移除 |
| 到期 | 执行回调;若是周期定时器,重新计算时间再插回 set |
7. 时间精度与开销
timerfd精度可达纳秒,实际使用毫秒级足够。- 定时器太多会带来 set 维护开销,但 Muduo 场景下可忽略。
简记:
- TimerQueue 用什么结构?——
std::set,按到期时间排序。 - 为什么要 timerfd?—— 让定时事件统一走 epoll,精确唤醒 loop。
七、TcpConnection —— 一条连接的完整生命周期
1. 什么是 TcpConnection
通俗解释:TcpConnection 是"一条已建立连接的化身",代表你和某个客户端的通话。它拥有这条连接的 Socket、Channel、输入/输出 Buffer。
TcpConnection 拥有的东西:
├── Socket(套接字)
├── Channel(事件载体)
├── input Buffer(收数据)
└── output Buffer(发数据)
2. 状态机(四态)
一条连接从建立到关闭,经历四种状态:
| 状态 | 含义 | 触发 |
|---|---|---|
kConnecting | 正在建立 | 刚 accept(客户端是 connect) |
kConnected | 已连接 | 握手完成 |
kDisconnecting | 正在断开 | 主动 close 发起 |
kDisconnected | 已断开 | 断开完成 |

为什么有 kDisconnecting 这个"中间态"?—— 因为发送缓冲里可能还有数据没发完,不能立刻断开,得先等数据发完。
3. 关键回调
TcpConnection 对外暴露几个回调,用户实现业务逻辑:
| 回调 | 触发时机 |
|---|---|
connectionCallback | 连接建立/断开时 |
messageCallback | 收到完整数据时 |
writeCompleteCallback | 数据全部写完时 |
4. 优雅关闭(重点)
通俗解释:不能"啪"地挂电话,要先把手头的话说完(把缓冲区数据发完)再挂。
close() 请求
↓
如果 output buffer 还有数据 → 进入 kDisconnecting,先发完
如果 buffer 空了 → 真正 close
这解释了为什么需要 kDisconnecting 状态——优雅关闭的关键。
5. 连接销毁时机(配合 Channel 的 tie_)
连接关闭时,TcpConnection 会被销毁。但因为 Channel 持有的是 weak_ptr(tie_),不会导致悬空,回调里能安全判断连接是否还活着。
简记:
- TcpConnection 有哪些状态?—— 四态:connecting/connected/disconnecting/disconnected。
- 为什么要有 kDisconnecting?—— 等输出缓冲发完再关,实现优雅关闭。
八、Acceptor —— 迎接新连接
1. 什么是 Acceptor
通俗解释:Acceptor 是"门卫",只干一件事——守监听套接字,来了新连接就 accept。
2. 内部组成
Acceptor
├── listenfd(监听套接字,绑定在 listen 端口)
├── acceptChannel(监听 listenfd 的 Channel)
└── newConnectionCallback(拿到新连接后回调)
3. accept 流程

4. 处理并发到达
- 一次只 accept 一个:用 LT 触发,只要还有连接没 accept,epoll 会反复通知,不会漏。
- 也可能在循环里连续 accept 多个,直到
EAGAIN。
为什么用 LT?—— LT 保证"只要还有未 accept 的连接,就会再通知",天然不会漏接。
九、TcpServer —— 把所有零件组装起来
1. 什么是 TcpServer
通俗解释:TcpServer 是"工厂总装线",把前面所有零件组装成一个能跑的服务器。
2. 组装清单
| 零件 | 数量 | 职责 |
|---|---|---|
Acceptor | 1 | 主 loop 里 accept 新连接 |
EventLoopThreadPool | 1 | 管理 N 个子 loop |
TcpConnection | 每连接 1 | 代表一条连接 |
3. 启动流程
TcpServer 构造:传入主 loop、端口
↓
start():
├── 启动 EventLoopThreadPool(子 loop 们跑起来)
└── 启动 Acceptor 监听
↓
新连接到来 → accept → 从池子轮询选子 loop → 建 TcpConnection → 绑定到该子 loop
4. 对外接口
| 方法 | 作用 |
|---|---|
setConnectionCallback | 设置连接建立/断开回调 |
setMessageCallback | 设置收到消息回调 |
setThreadNum | 设置子 Reactor 数量 |
start | 启动服务器 |
十、完整调用链
把前面所有零件串起来,一条数据从进到出的完整路径:

一条数据经历:accept → 分配 loop → 注册 Channel → epoll 通知 → Buffer 缓冲 → 业务处理 → 回写。
十一、Echo 服务器完整示例
一个最简的 echo 服务器,对照上面所有概念:
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/base/Logging.h>
#include <string>
using namespace muduo;
using namespace muduo::net;
// 收到消息的回调:原样回发
void onMessage(const TcpConnectionPtr& conn,
Buffer* buf,
Timestamp time)
{
std::string msg(buf->retrieveAllAsString()); // 取出 Buffer 里全部数据
conn->send(msg); // 原样发回(echo)
}
// 连接建立/断开的回调
void onConnection(const TcpConnectionPtr& conn)
{
if (conn->connected()) {
LOG_INFO << "新连接来自 " << conn->peerAddress().toIpPort();
} else {
LOG_INFO << "连接断开";
}
}
int main()
{
EventLoop loop; // 主 Reactor
InetAddress addr(8888); // 监听 8888 端口
TcpServer server(&loop, addr, "EchoServer");
server.setConnectionCallback(onConnection); // 绑定连接回调
server.setMessageCallback(onMessage); // 绑定消息回调
server.setThreadNum(4); // 4 个子 Reactor
server.start(); // 启动
loop.loop(); // 主 loop 跑起来
return 0;
}
代码对照概念:
| 代码 | 对应概念 |
|---|---|
EventLoop loop | 主 Reactor |
server.setThreadNum(4) | 4 个子 Reactor(多 Reactor 多线程) |
setMessageCallback | 注册 TcpConnection 的消息回调 |
buf->retrieveAllAsString() | 从 Buffer 读数据(readIndex 前移) |
conn->send | 写 output Buffer |
问题解决
- 什么是 Reactor 模式? —— 事件驱动 + I/O 复用,epoll 监听就绪后回调处理。
- One loop per thread 有什么好处? —— 避免加锁,简化模型,提高性能。
- epoll 的 LT 和 ET 区别?Muduo 用哪个? —— LT 反复通知,ET 只通知一次;Muduo 用 LT。
events_和revents_的区别? —— 前者是"关心的事件",后者是"实际就绪的事件"。- Channel 的
tie_为什么用 weak_ptr? —— 防连接销毁后回调访问悬空内存。 runInLoop和queueInLoop的区别? —— 同线程直接执行 vs 总是排队。- 怎么唤醒阻塞在 epoll 的 loop? —— eventfd,写字节触发可读。
- Buffer 为什么要 prepend 区? —— 前面插数据(如长度头)不用搬移。
- TimerQueue 用什么数据结构? ——
std::set,按到期时间排序。 - 为什么要 timerfd? —— 让定时事件统一走 epoll,精确唤醒 loop。
- TcpConnection 有哪些状态? —— 四态:connecting/connected/disconnecting/disconnected。
- 什么是优雅关闭? —— 等输出缓冲发完再 close,对应 kDisconnecting 态。
总结
| 概念 | 一句话 |
|---|---|
| Reactor | 事件驱动 + I/O 复用,epoll 监听、就绪后回调 |
| One loop per thread | 一个线程一个事件循环,天然无锁 |
| EventLoop | 事件循环,分发 I/O / 定时 / 用户任务 |
| Channel | fd + 事件 + 回调的封装,最小事件单位 |
| Poller | 封装 epoll,负责"等事件" |
| Buffer | 连接级缓冲,readIndex/writeIndex 双指针 |
| TimerQueue | set 排序的定时器队列 |
| Acceptor | 门卫,只 accept 新连接 |
| TcpConnection | 一条连接的化身,四态状态机 |
| TcpServer | 总装线,把所有零件拼成服务器 |
| eventfd | 唤醒阻塞中 loop 的"闹钟" |
| timerfd | 让定时事件走 epoll 的内核定時器 |