IO Runtime & Backend Abstraction — 跨平台 I/O 运行时与后端抽象
目标平台:Linux / Windows / macOS。 目标不是把
epoll / kqueue / IOCP / io_uring混成一个最低公共分母, 而是提供一层足够高的运行时抽象,让 Tick、Transport、Gateway、控制面都可以复用同一套 I/O 生命周期。
0.5 引擎实现对照与取舍
BigWorld 是怎么实现的
BigWorld 主要是 Mercury::EventDispatcher + NetworkInterface:
- 事件循环和网络接口强绑定
- 文件描述符注册模型清晰
- 更接近传统 Reactor 风格
它的优势在于成熟稳定,
但不是按今天的 Reactor / Completion 分层来建模。KBEngine 是怎么实现的
KBEngine 主要是 EventDispatcher -> EventPoller -> poller_*:
- Linux 下 epoll
- 兜底 select
- Windows 侧已有 IOCP 方向尝试
它比 BigWorld 更显式地把 poller 抽出来,
但顶层抽象仍然偏“fd readiness poller”,
不适合作为 io_uring / IOCP 的总抽象。优缺点
BigWorld / KBEngine 的优点:
- 事件循环简单直接
- 网络主路径容易理解
- 与单线程 tick 配合自然
共同缺点:
- 顶层抽象过于贴近 select/epoll 时代
- fd/register/deregister 不是 io_uring / IOCP 的理想语义
- timer/task/wakeup/completion 没被统一抽象成同一运行时theseed 的取舍
theseed 不再把 EventPoller 当总抽象,
而是拆成三层:
1. IORuntime
统一 run / wakeup / submit / cancel / drainCompletions
2. Backend Family
- ReactorBackend: select / epoll / kqueue
- CompletionBackend: io_uring / IOCP
3. Transport Adapter
TCP / UDP / WebSocket / Runtime Channel 在上层复用
这允许我们:
- 保留 BigWorld / KBEngine 的 tick 友好主循环
- 又不把现代完成式后端硬塞进 poller 语义0. 设计边界
本篇负责:
- Linux / Windows / macOS 的 I/O 后端抽象
- Reactor 与 Completion 两类后端语义
- 启动期能力探测与配置决策
- Tick Runtime 与 I/O Runtime 的衔接
- handle / request / completion / cancel 的统一接口本篇不负责:
- EntityCall / Mailbox 语义
- Runtime Data Plane 的业务可靠性分级
- Gateway / Login 的业务状态机
- MessageBus 控制面相关主题见:
../2-replication-and-space/03-runtime-communication-and-transport
../5-access-and-control-plane/01-gateway-and-login
../5-access-and-control-plane/02-message-bus-and-cross-realm1. 为什么不能继续叫 EventPoller
EventPoller 只适合表达:
- register fd for read
- register fd for write
- wait until ready
- 回调处理这套心智模型对:
select
epoll
kqueue是自然的。
但对:
io_uring
IOCP就不自然,因为它们更接近:
- submit operation
- operation completes later
- consume completion
- optionally cancel所以如果继续把总抽象命名为 EventPoller,会出现三个问题:
1. 顶层接口被 fd/register 语义绑死
2. io_uring / IOCP 只能被“伪装成 readiness”
3. timer/task/wakeup/completion 无法并入同一运行时结论:
EventPoller 可以保留为某一类后端的实现名,
但不能继续做 theseed 的总抽象。2. 推荐的总抽象
总抽象命名建议:
IORuntime它表达的是:
“驱动 I/O 请求、等待事件或完成、投递结果给上层 Tick 的运行时”而不是:
“某种 fd poller”2.1 核心接口
cpp
enum class IoOp : uint8_t {
Accept,
Connect,
Read,
Write,
RecvFrom,
SendTo,
};
struct IoHandle {
uint64_t value = 0;
uint32_t generation = 0;
};
struct IoBuffer {
void* data = nullptr;
uint32_t size = 0;
};
struct IoRequest {
IoOp op;
IoHandle handle;
IoBuffer buffer;
void* userData = nullptr;
};
struct IoToken {
uint64_t value = 0;
};
enum class IoStatus : uint8_t {
Ok,
Cancelled,
Timeout,
Closed,
Error,
};
struct IoCompletion {
IoToken token;
IoStatus status = IoStatus::Ok;
uint32_t bytesTransferred = 0;
int32_t osError = 0;
void* userData = nullptr;
};
class IIORuntime {
public:
virtual ~IIORuntime() = default;
virtual void runOnce(Duration maxWait) = 0;
virtual void wakeup() = 0;
virtual IoToken submit(const IoRequest& request) = 0;
virtual bool cancel(IoToken token) = 0;
virtual size_t drainCompletions(IoCompletion* out, size_t capacity) = 0;
};这里最重要的变化是:
统一语义是 submit / cancel / completion,
不是 registerRead / registerWrite / triggerRead。3. 两类后端家族
3.1 ReactorBackend
适用:
- select
- epoll
- kqueue语义:
- 后端报告“句柄已就绪”
- runtime 再把“就绪”翻译成可继续推进的 IoRequest3.2 CompletionBackend
适用:
- io_uring
- IOCP语义:
- 先提交具体 I/O 操作
- 后端直接产出完成事件
- runtime 消费 completion queue3.3 为什么不用写死 Proactor
严格说:
IOCP 更典型地接近 Proactor
io_uring 既能做 poll,也能做 completion所以文档里更准确的叫法应是:
Reactor / Completion而不是过度教条地只写:
Reactor / Proactor4. 启动期后端选择
后端选择只允许发生在启动期,不允许运行时热切换。
4.1 选择流程
1. 探测系统能力
2. 读取配置策略
3. 选择最优后端
4. 输出最终决策日志
5. 初始化对应 runtime4.2 能力探测
必须探测:
- OS 类型
- kernel / API 是否支持目标后端
- 当前权限或资源限制是否允许启用示例:
Linux:
- 先探测 io_uring
- 不可用则退回 epoll
macOS:
- kqueue
Windows:
- IOCP4.3 配置策略
toml
[runtime.io]
backend = "auto"
# auto
# force:io_uring
# force:epoll
# force:kqueue
# force:iocp规则:
auto:
自动选最优可用后端
force:*:
必须启用指定后端
若系统不支持,启动失败4.4 选择接口
cpp
enum class IoBackendKind : uint8_t {
Select,
Epoll,
Kqueue,
IoUring,
Iocp,
};
struct IoBackendCapability {
bool supported = false;
std::string reason;
};
class IIORuntimeFactory {
public:
virtual IoBackendCapability probe(IoBackendKind kind) = 0;
virtual std::unique_ptr<IIORuntime> create(
IoBackendKind kind,
const RuntimeConfig& config) = 0;
};5. 与 Tick 的关系
theseed 仍然保留:
单线程 Tick RuntimeI/O Runtime 不是另起一套业务线程模型,而是:
为 Tick 的 Network 阶段提供输入和完成事件5.1 Tick 侧约束
1. 所有 completion 只在 owning tick thread 消费
2. 后台系统线程可以存在,但不能直接改 Entity
3. completion 必须先入 runtime queue,再进 Network phase5.2 推荐时序
text
Tick::NetworkPhase
├─ ioRuntime.runOnce(maxWait=0)
├─ ioRuntime.drainCompletions(...)
├─ translate completion -> packet / channel event
└─ dispatch to runtime message queue如果某些后端内部需要辅助线程:
允许存在,
但这些线程不拥有 Entity,也不直接进入脚本层。6. Handle / Request / Completion 的分离
这层抽象必须避免把 fd 暴露成上层核心概念。
6.1 为什么不能只暴露 fd
1. Windows 不天然等价于 Unix fd 语义
2. IOCP / io_uring 的重点不在 fd readiness
3. 后续如果有 TLS、KCP、WebSocket 封装,逻辑句柄更稳定6.2 推荐对象层次
IoHandle
表示运行时管理的底层句柄身份
IoRequest
表示一次待执行的 I/O 操作
IoCompletion
表示一次已完成的 I/O 结果6.3 取消语义
必须明确:
cancel(token) 只保证“尽力取消”
不保证调用返回后一定不会再收到 completion因此上层必须接受:
completion 可能晚到
completion 必须携带 token / generation
上层按 epoch / handle generation 做幂等过滤7. 与 Transport 的衔接
Transport 不应直接依赖具体后端, 而应依赖:
IIORuntime7.1 传输适配层
cpp
class ITransportEndpoint {
public:
virtual ~ITransportEndpoint() = default;
virtual void start(IIORuntime& ioRuntime) = 0;
virtual void stop() = 0;
virtual void enqueueSend(PacketSpan packet) = 0;
virtual void onCompletion(const IoCompletion& completion) = 0;
};这样:
TCP / UDP / WebSocket / Runtime Channel都只面对统一 completion 语义。
7.2 旧式 poller 的位置
如果需要兼容旧风格实现,可以保留:
cpp
class IReactorBackend {
public:
virtual bool watchRead(IoHandle handle) = 0;
virtual bool watchWrite(IoHandle handle) = 0;
virtual void unwatchRead(IoHandle handle) = 0;
virtual void unwatchWrite(IoHandle handle) = 0;
};但它只属于:
ReactorBackend 内部不能再向上传播成总接口。
8. MVP 边界
MVP 建议支持:
Linux:
- epoll 必做
- io_uring 作为 Phase 2 或实验特性
Windows:
- IOCP 设计边界先立住
- 实现优先级晚于 Linux epoll 主路径
macOS:
- kqueue 设计边界先立住
- 实现优先级晚于 Linux epoll 主路径这样做的原因是:
1. MVP 先保证主平台可跑
2. 文档层先把长期抽象立住
3. 不把 io_uring 早期实现风险扩散进所有上层模块9. 与 BigWorld / KBEngine 的最终关系
BigWorld
可借鉴:
- EventDispatcher 和主循环的稳定边界
- NetworkInterface 与上层业务的清晰衔接
不直接照搬:
- 老式 dispatcher 直接作为总抽象KBEngine
可借鉴:
- EventDispatcher -> Poller 的显式拆层
- Linux epoll / fallback select 的工程现实
不直接照搬:
- 用 poller 语义覆盖 IOCP / io_uringtheseed
theseed 的新增点不是“发明一种新 socket API”,
而是把老引擎的事件循环经验,
升级成:
- 上层统一 IORuntime
- 中层 Reactor / Completion 家族
- 下层平台后端选择这才适合 Linux / Windows / macOS 的长期支持目标。