项目当前进展 (Project Status)
本文档汇总了 Veloq 项目各模块的开发状态、已完成特性以及待办事项列表。
最后更新时间: 2026-02-15 总体状态: 🚧 Alpha 阶段 (核心架构已定型,API 尚未冻结)
仪表盘 (Dashboard)
pie title 核心模块完成度估算
"Runtime (调度/Mesh)" : 85
"IO Drivers (Uring/IOCP)" : 75
"Memory (Buffer Pools)" : 80
"Network (TCP/UDP)" : 60
"FileSystem" : 50
"Ecosystem/Docs" : 40
1. 核心运行时 (Runtime Core)
状态: ✅ 大部分完成 (Mostly Completed)
建立了基于 Thread-per-Core 的模型,实现了高效的混合调度算法。
- Local Executor: 主循环、Budget 机制、Park/Unpark 状态机。
- Task System: 手动 VTable 实现的轻量级 Task,支持
Waker唤醒。 - 调度算法:
- P2C (Power of Two Choices): 发送端负载均衡。
- Work Stealing: 接收端负载均衡。
- Blocking Pool: 处理 CPU 密集型或同步系统调用的全局线程池 (
runtime/blocking.rs)。 - 待办 (TODO):
- 协作式抢占: 防止死循环任务卡死 Worker。
- Task Debugging: 增加任务追踪和调试视图。
2. I/O 驱动层 (IO Drivers)
状态: ⚠️ 完善中 (Stabilizing)
统一了 Linux (io_uring) 和 Windows (IOCP) 的 Proactor 抽象。
2.1 通用抽象
- Driver Trait: 定义了
submit,poll_op等统一接口。 - StableSlab: 实现了地址稳定的 Slab 分配器,用于存储 In-Flight Operations。
- OpRegistry: 类型擦除的操作注册表。
2.2 Linux (io_uring)
- 基础支持: 提交队列 (SQ) 和完成队列 (CQ) 的管理。
- 特性开关: 支持
Single Issuer和Defer Taskrun等新内核特性。 - Backlog: 简单的链表 Backlog 处理 SQ 满的情况。
- Fixed Files: 支持文件描述符注册 (
IoFd::Fixed) 以减少内核开销。 - 待办 (TODO):
- Zero Copy: 集成
IORING_OP_SEND_ZC。 - Multishot: 利用
IORING_RECV_MULTISHOT优化吞吐。
- Zero Copy: 集成
2.3 Windows (IOCP)
- 基础支持: 基于
GetQueuedCompletionStatus的事件循环。 - 扩展函数: 动态加载
ConnectEx,AcceptEx。 - 阻塞回退: 对于
Open/Close等同步 API 实现了线程池分流。 - Registered I/O (RIO): 支持 Registered I/O 以降低网络 I/O 延迟。
- 待办 (TODO):
- SyncFileRange: 寻找比
FlushFileBuffers更细粒度的刷盘方案。
- SyncFileRange: 寻找比
3. 内存管理 (Memory)
状态: ✅ 成熟 (Mature)
独立为 veloq-buf crate,架构分离为 Heap Layer 和 Buffer Layer。
- PoolTopology: 支持
GlobalSlotPool+Superblock的分片全局池架构。 - Dynamic Expansion: 支持 Chunk 的动态分配,并自动通知 Driver 注册新内存。
- FixedBuf: 实现了拥有权的缓冲区抽象,内嵌
ChunkID实现 O(1) 路由。 - 分配器:
- BuddyAllocator: 分片化的伙伴系统 (Sharded Buddy System)。
- Superblock: 针对 4KB 对象的 TLS 无锁缓存。
- 对齐: 强制 4KB 对齐,满足
O_DIRECT。 - 待办 (TODO):
- Huge Page: 完善大页分配支持。
- NUMA 感知: 优化跨 NUMA 节点的内存分配。
4. 上层 API (Net & FS)
状态: 🛠️ 基础功能 (Basic)
4.1 网络 (Net)
- TCP:
TcpListener,TcpStream(Connect, Read, Write)。 - UDP:
UdpSocket(SendTo, RecvFrom)。 - 待办 (TODO):
- Socket Options: 暴露
TTL,NoDelay等配置。 - Split API: 提供
ReadHalf/WriteHalf拆分。 - Zero-Copy: 暴露零拷贝读写接口。
- Socket Options: 暴露
4.2 文件系统 (FS)
- File:
open,read_at,write_at,close。 - Direct I/O: 支持
O_DIRECT模式的打开选项。 - 待办 (TODO):
- Metadata: 实现
metadata,set_len等操作。 - Directory: 实现异步目录遍历 (
read_dir)。 - Path Optimization: 优化路径对象的内存分配。
- Metadata: 实现
5. 总结与路线图 (Summary & Roadmap)
当前版本 Veloq 已经具备了一个高性能异步运行时的雏形,核心的 Context-Driver-Task 链路已经打通,且在微基准测试中展现了 Thread-per-Core 的潜力。
近期重点 (Next Steps):
- 稳定性: 完善测试用例,特别是 Windows 下的边界情况和 Linux 旧内核兼容性。
- 功能补全: 实现
Blocking Pool和Directory操作,使其能承载真实的业务逻辑。 - 生态: 提供与
AsyncRead/AsyncWrite的兼容层,以便复用现有 Rust 生态库。
Veloq 架构设计文档 (Architecture Design)
本文档提供了 Veloq 运行时的高层架构概览,阐述了其核心设计理念、组件交互流程以及针对不同操作系统的适配策略。
1. 核心理念 (Core Philosophy)
Veloq 的设计深受 Seastar 和 Glommio 的影响,旨在为 Rust 生态带来极致的 I/O 性能。核心原则如下:
1.1 Thread-per-Core & Shared-Nothing
传统的线程池模型(如 Tokio 的默认多线程运行时)虽然通用性强,但在极高吞吐场景下,跨核同步(Mutex/RwLock)、原子操作(Atomic CAS)以及 CPU 缓存抖动(Cache Bouncing)会成为显著瓶颈。
Veloq 采用 Thread-per-Core 模型:
- 每个 CPU 核心绑定一个独立的 Worker 线程。
- 每个 Worker 拥有独立的任务队列、I/O 驱动 (Driver) 和 缓冲区池 (Buffer Pool)。
- 线程间不共享任何可变状态。唯一的交互方式是通过 Mesh Network 进行消息传递。
1.2 Proactor I/O 模型
不同于基于 epoll 的 Reactor 模型(就绪通知),Veloq 拥抱现代操作系统的 Proactor 模型(完成通知):
- Linux: io_uring
- Windows: IOCP
这意味着 I/O 缓冲区的所有权必须在提交时移交给内核,从而实现真正的 Zero-Copy 和内核级批量处理。
2. 系统架构 (System Architecture)
下图展示了 Veloq 运行时的顶层视图:
graph TD
subgraph "Machine / Node"
subgraph "Core 0 (Thread Local)"
W0[Worker 0]
D0["Driver (uring/iocp)"]
BP0[Buffer Pool]
Mesh0[Mesh Rx]
W0 -->|Submit| D0
W0 -->|Alloc| BP0
D0 -->|Completion| W0
end
subgraph "Core 1 (Thread Local)"
W1[Worker 1]
D1["Driver (uring/iocp)"]
BP1[Buffer Pool]
Mesh1[Mesh Rx]
end
subgraph "Inter-Core Communication"
M[Mesh Network]
M -.->|SPSC Channel| Mesh0
M -.->|SPSC Channel| Mesh1
W0 -->|Send Msg| M
W1 -->|Send Msg| M
end
end
2.1 组件层级
Veloq 的代码组织结构如下:
-
Interface Layer (
src/net/,src/fs/):- 提供类似
std的高级 API (TcpStream,File)。 - 负责将用户请求封装为
Op并提交给 Runtime。
- 提供类似
-
Runtime Layer (
src/runtime/):- Executor: 负责 Task 的调度与执行。
- Context: 管理线程局部状态 (TLS),确保 I/O 操作都能找到对应的 Driver。
- Mesh: 实现跨线程的高效通信(SPSC Ring Buffer)。
-
I/O Layer (
src/io/):- Driver: 屏蔽 OS 差异的 Proactor 抽象。
- Buffer: 基于 Slab 或 Buddy System 的内存管理,保证物理地址稳定。
3. 关键流程 (Key Flows)
3.1 任务调度与执行
Veloq 采用混合调度策略,结合了 Work Stealing 和 Power of Two Choices (P2C)。
sequenceDiagram
participant User
participant Spawner
participant WorkerA
participant WorkerB
User->>Spawner: spawn(Task)
rect rgb(200, 220, 240)
note right of Spawner: Load Balancing (P2C)
Spawner->>Spawner: Check Load(WorkerA) vs Load(WorkerB)
Spawner->>WorkerA: Push Task (Less Loaded)
end
WorkerA->>WorkerA: Loop Check Budget
WorkerA->>WorkerA: Poll Mesh & Local Queue
WorkerA->>Task: Task::poll()
alt Task Blocks on I/O
Task->>WorkerA: Return Pending
else Task Completes
WorkerA->>User: Notify JoinHandle
end
3.2 异步 I/O 生命周期
以 File::read_at 为例,展示从用户调用到数据返回的全过程:
sequenceDiagram
participant App
participant Op
participant Driver
participant Kernel
App->>Op: read_at(buf)
Note over App, Op: Buffer ownership moved to Op
Op->>Driver: driver.submit(ReadOp, user_data)
Driver->>Kernel: io_uring_enter / ReadFile
App->>Op: await (Poll::Pending)
Note over Kernel: DMA / Disk I/O executes...
Kernel->>Driver: Completion Event (CQE / IOCP Packet)
Driver->>Op: Wake Task (Store Result)
Op->>App: Return (Result, Buffer)
Note over Op, App: Buffer ownership returned
3.3 跨平台适配
Veloq 通过 Driver Trait 和 PlatformOp 实现跨平台。
| 特性 | Linux (io_uring) | Windows (IOCP) | 抽象策略 |
|---|---|---|---|
| 模型 | Submission/Completion Rings | Completion Queue | Driver::submit / Driver::process_completions |
| 内存要求 | 推荐注册缓冲区 (Fixed Buf) | 需要重叠结构 (OVERLAPPED) 稳定 | StableSlab 分配器 |
| Socket创建 | socket() syscall | WSASocket | OpLifecycle::pre_alloc |
| Accept | 返回新 fd | 需预先创建 Socket (AcceptEx) | OpLifecycle::into_output |
| 阻塞操作 | 完美异步 (大部分) | 部分文件操作需线程池 | Driver 内部集成 ThreadPool |
4. 内存管理 (Memory Management)
Veloq 强制使用 FixedBuf 进行 I/O,这与常规 Rust AsyncRead (&mut [u8]) 不同。
- User Space: 用户从
BufPool申请FixedBuf。 - Kernel Space:
- Linux: 缓冲区可预注册 (
IORING_REGISTER_BUFFERS),内核只通过索引访问,极大减少开销。 - Windows: 缓冲区地址必须锁定。
- Linux: 缓冲区可预注册 (
- Lifecycle:
FixedBuf实现了 RAII,Drop 时自动归还 Pool。
classDiagram
class BufPool {
<<Trait>>
+alloc(size) FixedBuf
}
class FixedBuf {
-ptr: NonNull
-pool: WeakRef
+drop()
}
class Driver {
-register_buffers()
}
BufPool <|-- BuddyPool
BufPool <|-- HybridPool
FixedBuf --> BufPool : Return on Drop
Driver --> FixedBuf : Zero-Copy Access
5. 总结
Veloq 是一个为极致性能而生的运行时。它牺牲了一定的易用性(如强制的缓冲区管理、非标准的 I/O Trait),换取了对硬件资源的最大化利用。它通过严格的 Thread-per-Core 模型和精细的 Proactor 抽象,为构建下一代高性能数据库、存储系统和网络服务提供了坚实的基础。
Veloq Runtime 核心架构文档
本文档主要介绍 veloq-runtime 核心运行时层 (src/runtime) 的架构设计、核心组件及实现原理。
1. 概要 (Overview)
Veloq Runtime 是一个基于 Thread-per-Core 模型的高性能异步运行时,同时集成了 Work-Stealing 机制以实现更好的计算负载均衡。它旨在充分利用现代多核硬件的特性,通过减少跨核通信、锁竞争和缓存抖动来最大化 I/O 和计算吞吐量。
2. 理念和思路 (Philosophy and Design)
2.1 Thread-per-Core 与 混合任务模型
我们坚信在现代高并发场景下,数据局部性 (Data Locality) 是性能的关键。为此,Veloq 实现了双任务系统:
- Pinned Task (Task): 绑定在特定核心,拥有独立的 I/O 驱动和缓冲区池。用于 I/O 密集型操作,保持零锁。
- Stealable Task (Runnable): 实现了
Send,可以在核心间窃取和迁移。用于计算密集型操作,平衡负载。
2.2 Handle-based Distribution (基于句柄的分发)
传统的运行时通常使用全局的 Mutex<Queue> 或 SegQueue 来进行跨线程任务调度的高负载下争用。
Veloq 使用 Executor Handles:
- 每个 Worker 维护一个
ExecutorShared状态,包含一个专用的 MPSC (Multi-Producer Single-Consumer) 通道 (pinned) 用于接收定向任务。 - 当 Worker A 需要将任务发给 Worker B 时 (
spawn_to),它持有 B 的ExecutorHandle,直接将任务发送到 B 的通道。 - 这种方式简化了拓扑结构,同时通过
ExecutorRegistry保持了灵活性。
2.3 显式上下文 (Explicit Context)
为了避免隐式的全局状态(如 TLS 中的隐藏变量),Veloq 提供了 RuntimeContext:
- 显式通过上下文访问
Spawner(用于负载均衡生成任务) 和Driver(I/O)。 spawn: 创建一个可窃取的任务 (Runnable),优先本地执行,支持被窃取。spawn_local: 创建一个绑定任务 (Task),仅限本地执行。spawn_to: 将任务直接发送到指定 Worker 的pinned通道。
3. 模块内结构 (Internal Structure)
代码位于 src/runtime/:
src/runtime/
├── runtime.rs // Runtime 主入口,包含 Runtime 结构体、RuntimeBuilder 及 Worker 线程启动逻辑
├── blocking.rs // 全局阻塞线程池,处理 BlockingTask
├── context.rs // 线程局部上下文 (TLS),提供 spawn 接口和资源访问 (RuntimeContext)
├── task.rs // Pinned Task 定义,手动实现的 RawWakerVTable
└── harness.rs // Stealable Task (Runnable) 定义
├── join.rs // JoinHandle 实现,管理任务结果的异步等待
├── executor.rs // LocalExecutor 定义,Worker 线程主循环
└── executor/ // 执行器内部实现细节
└── spawner.rs // 任务生成器、注册表 (Registry) 和负载均衡逻辑
4. 代码详细分析 (Detailed Analysis)
4.1 Runtime & Initialization (runtime.rs)
Runtime 结构体持有所有 Worker 线程的句柄 (JoinHandle) 和全局注册表 (ExecutorRegistry)。
在 RuntimeBuilder::build() 过程中,会进行如下关键初始化:
- Shared States: 预分配所有 Worker 的共享状态 (
ExecutorShared),包含注入队列 (Injector)、Pinned 通道 (mpsc) 和负载计数器。 - Dynamic Memory Listener: 连接
PoolTopology的扩容监听器。当缓冲池动态分配新 Chunk 时,自动通知全局注册表,触发所有 Worker 的 Driver 进行内存注册。 - Thread Spawning: 启动 Worker 线程,每个线程运行一个
LocalExecutor,并绑定 Buffer Pool。
4.2 Context (context.rs)
RuntimeContext 是运行时与任务之间的桥梁。它包含:
- Driver: 指向底层
PlatformDriver的弱引用。 - ExecutorHandle: 当前执行器的句柄,包含共享状态 (Shared State)。
- Spawner: 全局生成器,用于
spawn和spawn_to。
4.3 Task System (task.rs & harness.rs)
Veloq 的任务系统经过了深度优化,分为两类:
-
Stealable Task (Runnable) (
harness.rs):- 专为 Work-Stealing 设计。
- 包含
[Header][Scheduler][Future]布局。 - 支持原子状态机 (
IDLE,RUNNING,NOTIFIED) 和跨线程调度 (Scheduletrait)。
-
Pinned Task (Task) (
task.rs):- 专为本地执行设计。
- 手动内存布局
[Header][Future],无 Scheduler 指针,更节省内存。 - 使用
VecDeque进行调度,极低开销。
4.4 JoinHandle (join.rs)
实现了任务结果的异步获取。
- 无锁状态机: 使用
AtomicU8维护状态 (IDLE->WAITING->READY),结合UnsafeCell存储结果。 - Local vs Send:
LocalJoinHandle: 单线程内使用,基于Rc<RefCell<...>>,零原子开销。JoinHandle: 跨线程使用,基于Arc<JoinState>和原子操作,支持Send。
5. 存在的问题和 TODO (Issues and TODOs)
-
Task Debugging: 目前的 Task 结构体非常精简,缺乏调试信息。 TODO: 在 Debug 模式下注入追踪信息。
-
Local Task 饿死: 虽然有 Budget 机制,但在极端混合场景下(大量远程注入任务 + 本地任务),调度策略可能仍需微调。
6. 未来的方向 (Future Directions)
-
结构化并发 (Structured Concurrency): 实现类似
TaskScope的机制。 -
协作式抢占 (Cooperative Preemption): 目前依赖用户代码中的
.await点进行调度。如果用户写了死循环,Worker 会卡死。未来可考虑结合编译器插件或计时器信号进行强制让出检测。
Veloq Executor 与调度系统
本文档详细阐述 src/runtime/executor 模块的内部工作机制。这是 Veloq 运行时的“引擎”,负责任务的调度、负载均衡和执行循环。
1. 概要 (Overview)
Executor 模块实现了 Work-Stealing 和 P2C (Power of Two Choices) 相结合的混合调度算法。
它由两部分组成:
- LocalExecutor (
executor.rs): 运行在每个 Worker 线程上的主循环,负责驱动 I/O、处理队列和任务窃取。 - Runtime & Spawning (
runtime.rs/spawner.rs): 负责线程的生命周期管理和任务分发策略。
为了实现这一目标,执行器支持三种类型的任务执行:
- Pinned Tasks (绑定任务): 必须在特定线程运行的任务(如
spawn_local)。由task.rs定义的Task。 - Stealable Tasks (可窃取任务): 实现了
Send,可以在任意线程运行的任务(通过spawn/spawn_eager/Runtime::spawn创建)。由runtime/task/harness.rs定义的Runnable。 - Blocking Tasks (阻塞任务): 长时间运行的 CPU 密集型任务或同步 I/O 任务。由
runtime/blocking.rs的全局线程池处理。
2. 理念和思路 (Philosophy and Design)
2.1 混合调度策略 (Hybrid Scheduling)
Veloq 结合了发送端和接收端的负载均衡:
- 发送端 (Direct Push): 绑定任务 (
spawn_local/spawn_to) 直接通过 MPSC 通道发送到目标 Worker。 - 本地优先 (Local Injection): 当 Worker 内部调用
spawn/spawn_eager时,Send任务会直接进入当前 Worker 的Stealable队列,优先由本地消费,但也允许被窃取。 - 远程注入缓冲 (Remote Injection Buffer): 跨线程创建或远程唤醒
Runnable时,任务先进入目标 Worker 的future_injector,再由目标线程转存到本地Stealable队列。 - 接收端 (Work-Stealing): 当 Worker 空闲时,主动去“窃取”其他 Worker 的
Stealable队列中的任务。
2.2 优先级倒置 (Priority Inversion for Latency)
在 LocalExecutor 的主循环中,显式定义了轮询顺序:
- Local Stealable: 优先执行当前线程刚刚生成的
Send任务。这利用了 CPU 缓存热度。 - Local Pinned Queue: 处理要求被绑定在当前 Worker 执行的任务 (
spawn_local)。 - Injectors: 处理外部注入的任务(远程唤醒的绑定任务、全局注入的任务)。
- Work Stealing: 最后尝试去偷任务。
2.3 动态注册 (Dynamic Registry)
ExecutorRegistry 支持通过 Arc 指针共享 Worker 列表,允许 Spawner 在运行时动态选择目标 Worker。
3. 模块内结构 (Internal Structure)
-
runtime.rs:Runtime: 运行时顶层入口。RuntimeBuilder: 负责初始化所有 Worker 和共享状态,并启动线程。
-
executor.rs:LocalExecutor: Worker 线程的本地执行器。block_on: 创建一个临时的LocalExecutor在当前线程运行 Future。
-
context.rs:RuntimeContext: 提供给用户的 TLS 上下文,包含spawn,spawn_eager,spawn_local,spawn_to等接口。
-
executor/spawner.rs:Spawner: 封装任务分发逻辑。ExecutorRegistry: 全局注册表。ExecutorShared: 跨线程共享的原子状态(负载计数、注入队列、Pinned 通道)。
-
task/harness.rs:Runnable: Stealable Task 的运行时句柄。封装了Future、状态机 (AtomicUsize) 和调度器 (ScheduleTrait)。
4. 代码详细分析 (Detailed Analysis)
4.1 主循环 (LocalExecutor::run)
核心是一个带有 Budget (预算) 的循环:
#![allow(unused)]
fn main() {
loop {
// 0. Check Memory Updates
// 检查并注册新扩展的内存块 (Pull Model)
self.check_for_memory_updates();
let mut executed = 0;
while executed < BUDGET {
// 1. Local Stealable (Send Tasks - LIFO)
if let Some(task) = self.stealable.pop() { ... }
// 2. Local Pinned Queue
// 处理本地任务
if let Some(task) = self.queue.pop() { ... }
// 3. Injectors (Pinned/Remote)
// try_poll_injector 内部会依次检查:
// - pinned queue (绑定任务)
// - remote receiver (唤醒的绑定任务)
if self.try_poll_injector() { ... }
// future_injector 只负责远程注入的 Runnable,下沉到本地 Stealable 后再执行
if self.try_poll_future_injector() { ... }
// 4. Stealing
// 从其他 Worker 偷任务
if self.try_steal(executed) { ... }
}
// 5. IO Wait & Park
self.park_and_wait(&main_woken);
}
}
BUDGET 机制(默认 64)防止计算密集型任务饿死 I/O 事件的轮询。
4.2 停车与唤醒 (park_and_wait)
这是一个精细的状态机:
- Set PARKING: 标记状态为 PARKING。
- Double Check: 再次检查队列。
- Commit PARKED: 状态设为 PARKED。
- Driver.wait(): 调用底层的
epoll_wait/GetQueuedCompletionStatus。
当其他线程通过 pinned.send() 发送任务时,会主动唤醒目标 Executor。
4.3 任务生成 (context.rs & spawner.rs)
spawn/spawn_eager: 创建一个Runnable(Harness)。若在 Worker 内部调用,优先推入当前 Worker 的Stealable队列;若跨线程创建或远程唤醒,则先进入目标 Worker 的future_injector,再由目标线程转存到本地Stealable队列。spawn_to/SpawnerPinned Spawn: 明确目标 Worker,将任务通过 MPSC 通道发送到目标的pinned队列。spawn_local: 创建SpawnedTask并推入本地VecDeque,永不离开线程。
4.4 注册表实现 (Registry Implementation)
目前 ExecutorRegistry 使用静态初始化的 Arc<Vec<ExecutorHandle>>。动态扩缩容(及之前的 smr-swap 方案)已简化为启动时配置,以减少运行时开销。
5. 存在的问题和 TODO (Issues and TODOs)
-
负载倾斜后的 Ping-Pong: P2C 可能导致任务抖动。 TODO: 实现指数退避 (Exponential Backoff) 的 Stealing 策略。
-
NUMA 感知: TODO: 实现分层 Stealing。
-
Worker ID 回收: ID 目前单调递增。
6. 未来的方向 (Future Directions)
- 时间片轮转: 防止单个任务霸占 Budget。
- 自适应 Budget: 动态调整 Budget。
IO 模块文档
本文档详细介绍了 veloq-runtime 的核心 I/O 模块 (src/io)。作为运行时的基石,该模块提供了一套基于 Proactor 模式的高性能异步 I/O 抽象。
1. 概要 (Overview)
src/io 模块不仅仅是文件和网络操作的集合,它定义了 Veloq 运行时的 I/O 交互模型。与 Rust 标准库或 Tokio 中常见的 poll_read / poll_write (Reactor 模型) 不同,Veloq 采用 所有权传递 (Ownership Passing) 的方式进行 I/O。
核心差异在于:
- 传统模型: 借用缓冲区 (
&mut [u8])。这在 io_uring 等异步接口上很难实现零拷贝,因为内核可能会在 Future 被 drop 后继续写入该缓冲区。 - Veloq 模型: 传递缓冲区所有权 (
FixedBuf)。用户将缓冲区交给运行时,I/O 完成后运行时归还缓冲区和结果。
该模块导出了所有必要的组件来构建上层网络协议和文件系统操作。
2. 理念和思路 (Philosophy and Design)
2.1 基于所有权的异步 I/O Trait
在 src/io.rs 中定义了两个核心 Trait:AsyncBufRead 和 AsyncBufWrite。
#![allow(unused)]
fn main() {
pub trait AsyncBufRead {
fn read(&self, buf: FixedBuf) -> impl Future<Output = (io::Result<usize>, FixedBuf)>;
}
}
这种设计强制要求调用者在发起 I/O 时放弃对缓冲区的控制权。这完美契合 io_uring 和 IOCP 的工作方式——一旦操作提交,内存区域必须保持稳定且由内核独占,直到完成信号到达。
2.2 模块化分层
io 模块采用严格的分层架构:
- 顶层 (
io.rs): 定义通用的行为 (Traits) 和入口。 - 操作层 (
op.rs): 定义具体的 I/O 意图(Op<T>,如“读取文件”、“连接 Socket”)。这些定义是严格平台无关的数据载体,实现了从意图到执行的解耦。 - 资源层 (
socket.rs,buffer.rs): 管理 I/O 所需的资源(句柄、内存)。 - 驱动层 (
driver.rs): 处理与操作系统的脏活累活,实现PlatformOp到系统调用的映射。
3. 模块内结构 (Internal Structure)
src/io.rs // 核心入口,定义 AsyncBufRead/AsyncBufWrite Traits
src/io/
├── buffer.rs // 内存管理 (FixedBuf, BufPool)
├── driver.rs // 驱动抽象 (Driver Trait, PlatformDriver)
├── op.rs // 操作定义 (Op<T>, OpSubmitter, Local/Remote Submission)
└── socket.rs // Socket/Handle 抽象
buffer: 提供FixedBuf,这是所有 I/O 操作的数据载体。详见docs/src/io/buffer/README.md。driver: 定义Drivertrait,这是 Reactor/Proactor 的核心接口。详见docs/src/io/driver/README.md。op: 核心操作抽象。Op<T>: 通用数据载体,表示 I/O 意图。不包含任何驱动/运行时引用。OpSubmitter: 统一提交接口。LocalSubmitter: 将Op提交到当前线程的驱动 (submit_local)。RemoteSubmitter: 将Op注入到远程驱动 (submit_remote)。
- 具体操作结构体: 如
ReadFixed,Connect,Open等平台无关结构。
socket: 提供跨平台的句柄封装。
4. 代码详细分析 (Detailed Analysis)
4.1 统一的操作提交模型 (Op<T>)
Veloq 的 Op<T> 设计实现了数据与执行的彻底分离。
- 构建阶段: 用户构建一个
Op::new(ReadFixed { ... })。此时它只是一个纯数据结构,可以在线程间只有移动。 - 提交阶段:
- Local: 使用
submit_local。操作由当前线程的 Driver 直接处理,生命周期和结果直接通过LocalOpFuture 关联。高性能,无跨线程开销。 - Remote: 使用
submit_remote。操作被打包并通过Injector发送到持有资源的 Driver 线程。返回RemoteOpFuture,通过 channel 接收结果。 这种对偶性(Duality)使得同一个 I/O 逻辑可以无缝运行在 Thread-Per-Core 模型(Local)或 Work-Stealing 模型(Remote)下。
- Local: 使用
4.2 AsyncBufRead / AsyncBufWrite
这两个 Trait 是 Veloq I/O 生态的一等公民。
- 设计权衡: 相比于
AsyncRead(poll_read),AsyncBufRead对编译器更友好(无需处理复杂的生命周期),但对用户代码有侵入性(需要管理 Buffer 池)。 - 返回值:
(Result<usize>, FixedBuf)。即使 I/O 失败,缓冲区也必须归还给用户,以便重用或释放。如果设计成只返回Result,那么在错误发生时缓冲区就会泄漏(被 drop 掉),这是不可接受的。
4.3 模块重导出
io.rs 充当了 Facade(门面):
#![allow(unused)]
fn main() {
pub mod buffer;
pub mod driver;
pub mod op;
pub(crate) mod socket; // socket 内部实现细节较多,对外主要暴露类型
}
这种结构隐藏了内部实现的复杂性,用户在使用时通常只需要引入 veoq_runtime::io 即可访问大部分功能。
5. 存在的问题和 TODO (Issues and TODOs)
-
生态兼容性:
- 现有的 Rust 异步生态(Tokio, Hyper 等)严重依赖
AsyncRead/AsyncWrite。Veloq 的AsyncBufRead无法直接与它们互通。 - TODO: 提供适配层 (
Compat),使用内部缓冲区来模拟AsyncRead,但这会引入一次内存拷贝,牺牲部分性能以换取兼容性。
- 现有的 Rust 异步生态(Tokio, Hyper 等)严重依赖
-
Trait 方法的泛化:
- 目前
read接受FixedBuf具体类型。未来可能通过泛型支持任何实现了AsIoSlice的类型,但这会增加 VTable 调度的复杂性。
- 目前
-
Vectored I/O:
- 目前的 Trait 仅支持单个 buffer (
read,write)。对于readv/writev(Scatter/Gather) 的支持尚未在顶层 Trait 中体现。 - TODO: 添加
read_vectored和write_vectored支持。
- 目前的 Trait 仅支持单个 buffer (
6. 未来的方向 (Future Directions)
-
统一流抽象 (Unified Stream Abstraction):
- 构建基于
AsyncBufRead的BufReader和BufWriter等高级工具,提供行读取、按分隔符读取等功能。
- 构建基于
-
Pipeline I/O:
- 探索类似 Linux
splice或sendfile的高级 Trait,允许数据在两个文件描述符之间直接传输,完全绕过用户态缓冲区。
- 探索类似 Linux
-
Completion-based Traits:
- 考虑引入
AsyncReadCom等 Trait,直接返回impl Future,进一步利用 Rust 2024 的async fnin trait 特性(目前 Veloq 手写了 Future 返回值以保证向后兼容或特殊控制)。
- 考虑引入
Veloq Driver 异步 I/O 驱动架构文档
本文档详细阐述了 veloq-driver 核心 I/O 驱动层的设计哲学、架构结构及实现细节。该层旨在屏蔽操作系统底层的异步 I/O 差异(Linux io_uring 与 Windows IOCP),为上层运行时提供统一的高性能 Proactor 接口。
1. 概要 (Overview)
veloq-driver 是运行时与操作系统内核交互的独立 Crate。与 Rust 生态中常见的 Reactor 模型(如 Tokio 基于 mio/epoll)不同,Veloq 采用了纯 Proactor 模型。
- Reactor: 关注“就绪”事件(Readiness)。当 socket 可读时通知应用,应用再调用
read。 - Proactor: 关注“完成”事件(Completion)。应用直接提交
read操作,内核将数据写入缓冲区后通知应用完成。
这种设计是为了原生适配现代高性能 I/O 接口(io_uring 和 IOCP 均为 Proactor 性质),避免中间层的模拟开销,并最大化利用内核的零拷贝和批量处理能力。
统一的核心抽象是 Driver Trait,它定义了操作提交、轮询、取消和资源管理的行为。
2. 理念和思路 (Philosophy and Design)
2.1 统一的 Proactor 抽象
尽管 io_uring(基于环形队列)和 IOCP(基于完成端口队列)在 API 形式上差异巨大,但在逻辑上它们都遵循:
提交请求 (Submit) -> 等待 (Wait) -> 处理完成 (Process Completion)
Veloq 抽象出了这一公共流程:
- Reserve: 在用户态分配一个 Slot (User Data),用于关联上下文。
- Submit: 将操作描述符提交给内核,并传入 Slot Index 作为 User Data。
- Poll: 上层
Future在poll时检查共享 Slot 的状态。 - Complete: 驱动收到内核完成通知,通过 User Data 找到 Slot,填入结果并唤醒 Waker。
2.2 零分配与共享槽位 (Zero-Allocation & Shared Slots)
异步 I/O 的一个核心挑战是资源的生命周期管理和跨线程通知。
- 旧设计问题: 传统
DetachedOp往往依赖oneshot通道或Arc<State> + Box<Completer>来实现跨线程结果返回,导致每次 I/O 都有额外的堆分配。 - 新设计方案: Shared Slot Table。
- 驱动维护一个
Arc<SlotTable<Op>>,其中包含预分配的、地址固定的Slot数组。 - 零分配提交:
DetachedOpFuture 直接持有Arc<SlotTable>和索引。提交时,I/O 资源(如Op)直接移动到 Slot 中,无需额外分配。 - 无锁状态机: Slot 内部维护原子状态机 (
EMPTY->SUBMITTED->COMPLETED)。Future 直接轮询 Slot 状态,驱动完成时直接更新 Slot 并唤醒 Waker,无需中间通道。
- 驱动维护一个
2.3 零开销类型擦除 (Zero-Cost Type Erasure)
驱动需要支持多种 I/O 操作(Read, Write, Connect, Accept, Close 等)。
- 传统做法: 使用巨大的
Enum包裹所有可能的操作。缺点是内存浪费(结构体大小取决于最大的那个变体)。 - Veloq 做法: 使用 Union + VTable (
PlatformOpTrait 和具体的Op实现)。- Payload: 使用
union存储不同操作的数据载荷。 - VTable: 每个操作携带一个静态虚函数表(VTable),包含构建提交项、处理完成回调、销毁逻辑等指针。
- 这种类似 C++ 虚函数的机制是在编译期生成的,避免了运行时的动态内存分配(Heap Allocation),同时保持了数据结构的紧凑。
- Payload: 使用
2.4 Mesh 网络协同与远程注入
驱动不仅处理本地 I/O,还通过 Injector 机制深度集成了跨线程调度能力。
- Injector: 每个驱动实例暴露一个线程安全的注入器 (
Injector<D>)。 - Closure Injection: 允许其他线程将闭包 (
Box<dyn FnOnce(&mut Driver) + Send>) 发送到驱动线程执行。这构成了RemoteOp的基础——远程线程如果不持有 Driver 的资源的线程所有权,可以通过注入闭包,“委托” Driver 线程提交操作。 - Inner Handle / Notify: 暴露底层的
RawFd或Handle并提供唤醒机制,用于实现高效的事件通知。
3. 模块内结构 (Internal Structure)
veloq-driver/src/
├── driver.rs // Driver 模块定义与接口 (Trait, PlatformOp)
└── driver/ // Driver 具体实现与组件
├── op_registry.rs // 动静分离的操作注册表
├── slot.rs // 核心 Slot 定义与状态机
├── iocp.rs // Windows 平台实现入口
├── iocp/ // Windows 子模块目录
│ ├── blocking.rs
│ ├── op.rs
│ └── ...
├── uring.rs // Linux 平台实现入口
└── uring/ // Linux 子模块目录
├── submit.rs
└── ...
driver.rs: 定义了驱动必须实现的接口规范。slot.rs: 定义了核心的Slot<Op>结构。它是CachePadded的,包含原子状态、Waker、Result 和 UnsafeCell 包裹的 Op 资源。op_registry.rs: 管理驱动的本地状态 (local) 和共享状态 (shared)。shared:Arc<SlotTable>,供 Future 和 Driver 共享访问。local: Driver 线程私有的状态(如 Timer ID、Backlog 链表节点),无锁开销。
4. 代码详细分析 (Detailed Analysis)
4.1 Driver Trait (driver.rs)
#![allow(unused)]
fn main() {
pub trait Driver {
type Op: PlatformOp;
// 核心生命周期
fn reserve_op(&mut self) -> io::Result<(usize, u32)>; // 返回 (index, generation)
fn slot_table(&self) -> Arc<SlotTable<Self::Op>>;
fn submit(&mut self, user_data: usize, op: Self::Op) -> Result<Poll<()>, ...>;
fn poll_op(&mut self, user_data: usize, cx: &mut Context) -> Poll<...>;
// 驱动循环
fn wait(&mut self) -> io::Result<()>;
fn process_completions(&mut self);
// 资源管理
fn register_files(...) -> ...;
fn unregister_files(...) -> ...;
fn register_chunk(&mut self, id: u16, ptr: *const u8, len: usize) -> io::Result<()>;
fn submit_background(&mut self, op: Self::Op) -> ...;
}
}
4.2 Slot & SlotTable (slot.rs)
- Slot:
state:AtomicU8(EMPTY, SUBMITTED, COMPLETED)。generation:AtomicU32,防止 ABA 问题(Slot 被回收复用后,旧 Future 仍尝试访问)。op:UnsafeCell<Option<Op>>。在SUBMITTED状态下由 Driver 访问(提交给内核),在COMPLETED状态下由 Future 取走(获取所有权)。waker:AtomicWaker,用于唤醒等待的 Future。overlapped(Windows): 嵌入的 IOCP 重叠结构,利用指针反推技术定位 Slot。
4.3 OpRegistry (op_registry.rs)
它是连接 Driver 和 Future 的桥梁,采用了动静分离设计。
- Shared (SlotTable): 存储重量级的
Op资源和同步原语。DetachedOp持有它的引用。 - Local: 存储驱动内部的轻量级状态(如
lifecycle,timer_id)。 - 流程:
- Submit: Driver 分配索引,将
Op放入 Shared Slot,初始化 Local 状态。 - Poll: Future 检查 Shared Slot 的
state和generation。 - Complete: Driver 收到内核事件,更新 Shared Slot
result和state,唤醒 Waker。 - Take: Future 被唤醒,从 Shared Slot
take()走 Result 和 Op。
- Submit: Driver 分配索引,将
5. 存在的问题和 TODO (Issues and TODOs)
-
Backlog 策略差异:
- Linux: io_uring 的 SQ 也是环形队列,会满。
UringDriver必须在用户态实现一个 Backlog 链表来暂存无法提交的操作。 - Windows: IOCP 本身没有“提交队列满”的概念(它是直接调 API),但为了防止内存无限增长,我们人为限制了
SlotTable的大小。 - TODO: 统一 Backpressure(背压)策略,当驱动过载时,向上层返回明确的错误或挂起信号,而不是无限缓冲。
- Linux: io_uring 的 SQ 也是环形队列,会满。
-
Buffer Registration 抽象泄漏 (已解决):
- 引入了逻辑区域映射 (Logical Region Mapping) 层。
BufferPool将内存暴露为带索引的 Region。- 驱动层(IOCP/Uring)分别将这些索引映射为 RIO Buffer ID 或 io_uring fixed indices。
- 结果:
FixedBuf只需携带一个通用的region_index,实现了跨平台的 O(1) 提交,完全屏蔽了底层差异。
-
同步文件 I/O:
- 在 io_uring 上文件 I/O 是真异步的。在 IOCP 上,部分文件操作(特别是打开/关闭)仍通过线程池模拟。这种差异导致性能特性的不一致。
- TODO: 在 Linux 上对于不支持 io_uring 的动作也应有统一的线程池回退机制(目前可能有隐式阻塞)。
6. 未来的方向 (Future Directions)
-
支持更多后端:
- 虽然目前专注高性能 Proactor,但为了兼容性(如 macOS),未来可能需要引入
kqueue后端。但这需要适配层模拟 Proactor 行为(类似 Tokio 的做法,但在 Driver 层内部封装)。
- 虽然目前专注高性能 Proactor,但为了兼容性(如 macOS),未来可能需要引入
-
Direct I/O 与 Zerocopy:
- 进一步深挖
IORING_OP_SEND_ZC和 Windows RIO。 - 目标是实现网络栈的零拷贝发送和接收,这对于高吞吐场景(如 100Gbps 网络)是必须的。
- 进一步深挖
-
内核旁路 (Kernel Bypass) 集成:
- 随着 io_uring 支持
IORING_OP_URING_CMD,未来可以直接对接 NVMe 驱动或用户态网络栈(如 AF_XDP),进一步绕过内核协议栈开销。
- 随着 io_uring 支持
Linux io_uring 驱动文档
本文档详细介绍了 veloq-driver 中基于 Linux io_uring 的异步驱动实现。
1. 概要 (Overview)
veloq-driver 的 Linux 驱动层位于 src/driver/uring/ 目录下。它实现了 Driver trait,利用 Linux 内核最新的 io_uring 接口提供高性能的异步 I/O 能力。该驱动采用 Proactor 模式,通过共享内存的提交队列 (SQ) 和完成队列 (CQ) 与内核进行零拷贝交互,避免了传统系统调用(syscall)的频繁上下文切换开销。
2. 理念和思路 (Philosophy and Design)
2.1 高性能配置
我们在初始化 io_uring 时启用了一系列高级特性(依赖较新的内核版本),以榨取极致性能:
setup_coop_taskrun: 减少核间中断 (IPI)。setup_single_issuer: 针对单线程提交优化,进一步减少锁开销 (Kernel 6.0+)。setup_defer_taskrun: 推迟内核任务执行直到io_uring_enter,优化批处理能力 (Kernel 6.1+)。sqpoll(可选): 如果启用 polling 模式,内核线程会轮询 SQ,实现真正的零系统调用提交。
2.2 类型擦除与动态分发 (Type Erasure)
为了支持多种 I/O 操作(Read, Write, Connect, Accept 等)而不引入枚举 (Enum) 的巨大内存开销,本驱动采用了与 IOCP 驱动类似的类型擦除技术 (src/driver/uring/op.rs)。
- Union Payload: 使用
union UringOpPayload存储各种操作的具体参数结构。这确保了UringOp的大小仅等于最大负载的大小,而不是所有变体之和。 - VTable: 每个操作类型通过宏
define_uring_ops!自动生成对应的OpVTable。包含make_sqe(构建提交项),on_complete(处理完成),drop(资源清理) 等静态函数指针。 - 生命周期管理: 使用
ManuallyDrop手动管理 Union 中字段的生命周期,确保在操作完成或取消时正确释放资源(如CString,Vec等)。
2.3 基于 Slot 的零拷贝提交 (Slot-Based Submission)
新架构引入了共享的 SlotTable。即便是在 io_uring 这种环形队列模型下,Slots 依然扮演着关键角色:
- 资源持有:
UringOp(包含缓冲区、文件描述符等)被存放在Slot.op中,所有权属于Slot。 - SQE 构建: 驱动在构建 SQE (Submission Queue Entry) 时,直接从
Slot中借用UringOp的指针。传递给内核的user_data即为 Slot 的索引。 - 完成处理: 当内核返回 CQE 时,驱动根据
user_data索引找到 Slot,写入结果并唤醒从 Slot 等待的 Future。 - 优势: 这种机制允许
DetachedOp直接从 Slot 等待结果,无需额外的 Channel 分配。
2.4 提交积压处理 (Backlog Handling)
io_uring 的提交队列 (SQ) 大小是固定的。当 SQ 满时,驱动必须暂存无法立即提交的操作。
- 我们在
UringOpState(Driver Local) 中维护了一个侵入式单向链表 (backlog_head,backlog_tail,next)。 - 当
push_entry失败(SQ 满)时,驱动不会尝试不断重试,而是将该 Slot 索引加入 backlog 链表。 - 此时
Op依然安全地驻留在Slot中,等待下一次提交。 - 每次
submit或wait后,驱动会尝试flush_backlog,将暂存的操作重新推入 SQ。
2.5 唤醒机制 (Waker)
由于 driver.wait() 通常会阻塞在 io_uring_enter 系统调用上,我们需要一种机制从其他线程唤醒它(例如当新的任务通过 Mesh 通道发送过来时)。
- 驱动使用
eventfd创建一个特殊的唤醒文件描述符。 - 注册一个
Poll或Read操作 (Wakeup) 到io_uring监听该 fd。 RemoteWaker的实现只是简单地向该eventfd写入 8 字节,从而触发io_uring完成事件,唤醒驱动主循环。
2.6 内核兼容性与降级策略 (Kernel Compatibility)
veloq-driver 优先使用较新内核的特性以获得最佳性能,但也提供了针对较旧内核的回退支持。
功能降级矩阵 (Degradation Matrix)
驱动初始化时会尝试启用所有高级特性 (SingleIssuer, DeferTaskRun, CoopTaskRun)。如果内核不支持(返回 EINVAL),将自动回退到基础模式。
| 内核版本 (Kernel) | 模式 (Mode) | 特性支持 (Features) | 性能影响 (Performance) |
|---|---|---|---|
| ≥ 6.1 | 高性能 (High Perf) | SingleIssuer + DeferTaskRun + CoopTaskRun | 最佳。最小化系统调用开销和 IPI。 |
| 5.6 - 6.0 | 基础 (Basic) | 标准 io_uring | 良好。无 DeferTaskRun 批处理优化,可能有少量多余 IPI。 |
| < 5.6 | 不支持 (Unsupported) | - | 无法运行。缺少 IORING_OP_RECV / IORING_OP_SEND 等核心指令支持。 |
注:虽然 5.10+ (LTS) 是推荐的生产环境最低版本,但在 5.6+ 上理论上可运行基础功能。
3. 模块内结构 (Internal Structure)
src/driver/uring/
├── mod.rs // 驱动入口 (UringDriver),主循环,Backlog 管理
├── op.rs // UringOp 定义,VTable 定义,Union Payload 宏
├── inner.rs // 内部实现细节 (State, Lifecycle)
├── submit.rs // 各个 Op 的具体实现 (make_sqe_*, on_complete_*)
└── tests/ // (如果存在) 单元测试
外部依赖:
../op_registry.rs:OpRegistry负责存储所有飞行中的操作状态 (UringOpState)。../slot.rs: 提供SlotTable,持有实际的UringOp资源。
4. 代码详细分析 (Detailed Analysis)
4.1 UringDriver (uring.rs)
核心结构体 UringDriver 维护了:
ring:io_uring::IoUring实例。ops:OpRegistry<UringOp, UringOpState>,所有 In-Flight 操作的仓库。backlog_head/tail: 积压队列指针。pending_cancellations: 等待取消的操作队列。
生命周期:
- 提交 (submit): 用户调用
submit,驱动分配 Slot,保存资源到Slot.op,调用vtable.make_sqe构建 SQE ( Submission Queue Entry),尝试推入 Ring。若 Ring 满,加入 Backlog。 - 等待 (wait): 调用
ring.submit_and_wait(1)。 - 处理 (process_completions):
- 遍历 CQE (Completion Queue Entry)。
- 根据
user_data找到对应的Slot和OpEntry。 - 调用
vtable.on_complete处理结果。 - 更新
Slot.state为COMPLETED,唤醒Slot.waker。 Future醒来后,从Slot取走资源和结果,并释放Slot。
4.2 操作定义 (op.rs)
UringOp 是驱动中流转的核心数据结构:
#![allow(unused)]
fn main() {
#[repr(C)]
pub struct UringOp {
pub vtable: &'static OpVTable, // 8 bytes
pub payload: UringOpPayload, // union
}
}
宏 define_uring_ops! 极大地简化了新操作的添加。开发者只需定义 Payload 结构和几个静态方法,宏会自动生成 IntoPlatformOp 实现和 Union定义。
4.3 静态分发 (submit.rs)
该文件包含所有具体操作的逻辑。
- make_sqe_*: 将高层 Op 转换为
io_uring::squeue::Entry。处理IoFd::Fixed(注册文件) 和IoFd::Raw的区别。由于此时驱动拥有 Slot 的所有权,它可以安全地从 Slot 中读取 Op 数据构建 SQE。 - on_complete_*: 处理内核返回的
i32结果。例如Accept操作在此处将内核写入的sockaddr字节解析为 Rust 的SocketAddr。 - drop_*: 安全地释放 Union 中的资源。
4.4 缓冲区管理 (Sparse Buffer Registration)
为了支持动态内存扩展并避免全量重新注册的开销,驱动实现了 稀疏缓冲注册 (Sparse Registration):
- 初始化: 在
UringDriver::new中,驱动会使用IORING_REGISTER_BUFFERS注册一个全空的iovec数组,大小为MAX_CHUNKS(默认为 1024)。 - 增量更新: 当调用
register_chunk时,驱动使用IORING_REGISTER_BUFFERS_UPDATE指令,仅更新指定ChunkID对应的槽位。这使得新内存块的注册成本极低,且不影响正在进行的 I/O。 - 零拷贝: 在
make_sqe中,如果检测到 Op 携带的FixedBuf指向有效的注册 Chunk,会自动使用opcode::ReadFixed/WriteFixed等变体,指示内核直接使用预注册的缓冲区,实现零拷贝。
5. 存在的问题和 TODO (Issues and TODOs)
-
内核版本依赖:
- 当前使用了许多较新的 io_uring 特性(如
Single Issuer,Defer Taskrun)。在旧内核(< 5.10)上虽然有回退逻辑,但性能和功能可能受限。
- 当前使用了许多较新的 io_uring 特性(如
-
Backlog 性能:
- 目前 Backlog 是一个单向链表。如果 Ring 长期处于满载状态,大量的 Backlog 插入/弹出可能导致 CPU 开销增加。
- TODO: 考虑在极端压力下引入背压 (Backpressure) 机制,暂时拒绝新任务。
-
取消机制的可靠性:
AsyncCancel发出后,原操作可能正好完成。需要仔细处理ECANCELED和正常完成的竞态条件,确保资源不被双重释放或泄露。
-
Send/Recv Msg:
SendTo/RecvFrom目前使用了SendMsg/RecvMsg。对于单纯的 UDP 发送,可能可以直接优化为Send/Recv配合地址连接,或者使用IORING_OP_SEND_ZC(Zero Copy)。
6. 未来的方向 (Future Directions)
-
Zero Copy (IORING_OP_SEND_ZC):
- 随着内核支持的完善,引入零拷贝发送将显著提升大包吞吐量。
-
io_uring_cmd:
- 支持
IORING_OP_URING_CMD,为 NVMe Passthrough 或其他内核子系统提供直接通道,绕过文件系统层。
- 支持
-
多重 Shot (Multishot):
- 利用
IORING_RECV_MULTISHOT(Provide Buffers),允许一个系统调用接收多个数据包,极大减少网络密集型应用的 syscall 数量。
- 利用
-
Ring Sharing:
- 探索在多个 Worker 线程间安全共享 Ring 的可能性(虽然目前的设计是每线程一个 Ring 以避免锁),或者利用
IORING_SETUP_ATTACH_WQ共享内核侧工作队列。
- 探索在多个 Worker 线程间安全共享 Ring 的可能性(虽然目前的设计是每线程一个 Ring 以避免锁),或者利用
Windows IOCP 驱动文档
本文档详细介绍了 veloq-driver 中基于 Windows I/O Completion Ports (IOCP) 的异步驱动实现。
1. 概要 (Overview)
veloq-driver 的 Windows 驱动层实现了 Driver trait,为上层运行时提供异步 I/O 支持。该驱动采用了 Proactor 模式,利用 Windows 内核提供的 IOCP 机制来处理网络和文件 I/O 事件。
一个显著的特点是它实现了 混合 I/O 模型:对于标准句柄使用传统的 IOCP 模型,而对于支持 Registered I/O (RIO) 的网络操作,则自动尝试升级到更高效的 RIO 路径,以降低内核开销。
2. 理念和思路 (Concepts)
2.1 Proactor 模型
IOCP 是原生的 Proactor 模型。应用程序“投递”一个 I/O 请求(如 ReadFile 或 RIOReceive),内核在操作完成后通知应用程序。在此期间,驱动必须保证传递给内核的数据结构(如 OVERLAPPED 或 RIO_BUF)地址固定且有效。
2.2 内存稳定性与 Slot Embedding
由于异步操作通过指针引用 OVERLAPPED,我们需要确保其在整个生命周期内地址固定。
- SlotTable: 驱动使用
SlotTable存储飞行中(In-Flight)的操作。Slot数组预先分配并固定在内存中。 - Overlapped Embedding:
Slot<Op>结构体中直接嵌入了 Windows 的OVERLAPPED结构。#![allow(unused)] fn main() { pub struct Slot<Op> { // ... #[cfg(windows)] pub overlapped: UnsafeCell<OverlappedEntry>, // 包含 OVERLAPPED 和额外元数据 } } - 指针反推 (Pointer Back-tracing): 当 IOCP 完成时,我们得到的是指向
OVERLAPPED的指针。由于OVERLAPPED嵌入在Slot中,且Slot大小固定,我们可以通过指针算术(container_of逻辑)反推算出不仅是Slot对象的地址,还能算出该Slot在 Table 中的 索引 (Index)。这也允许我们不再依赖completion_key来传递上下文。
2.3 类型擦除与 VTable (Type Erasure)
为了避免使用巨大的 Enum 定义所有 I/O 操作,驱动使用了自定义的类型擦除技术 (src/driver/iocp/op.rs)。
- Union Payload: 所有具体的 Op 负载(如
ReadFixed,Connect,SendToPayload)存储在一个union中。 - VTable Logic: 配合
define_iocp_ops!宏,自动为每个 Op 生成OpVTable(包含submit,on_complete,drop,get_fd等函数指针)。 - 这使得
IocpOp结构体保持紧凑,同时支持动态分发,无需堆分配。
2.4 阻塞操作分流 (Blocking Offload)
文件系统的某些元数据操作(如 Open, Close, Fsync)在 Windows 上往往是同步阻塞的。为了不阻塞 Reactor 线程,这些操作被自动分流到内部的 ThreadPool (src/driver/iocp/blocking.rs)。
- 线程池任务完成后,手动调用
PostQueuedCompletionStatus向 IOCP 端口发送完成通知,使其在主循环中表现为普通的异步事件。
2.5 Registered I/O (RIO) 集成
为了追求极致的网络性能,驱动集成了 Windows RIO 扩展,并实现了多项优化:
- 缓冲区注册: 实现了
Driver::register_chunk接口。驱动内部维护chunk_registry,将veloq-buf的ChunkID映射到 Windows RIO 的RIO_BUFFERID。这允许内存块的动态与增量注册。 - 逻辑区域映射 (Logical Region Mapping): Veloq 引入了创新的缓冲区管理设计。Windows 驱动将逻辑区域索引直接映射到 RIO Buffer ID,实现了 O(1) 的缓冲区解析。
- O(1) 请求队列 (RQ) 查找: 对于注册文件 (
IoFd::Fixed),驱动在RioState中维护了一个直接映射表registered_rio_rqs。这消除了哈希表查找的开销,使得获取 Request Queue 的操作也是 O(1) 的。 - Slab 页注册: 为了支持
SendTo/RecvFrom等操作中的地址结构体(SOCKADDR),驱动会自动将SlotTable占用的内存页通过register_buffer注册到 RIO。这允许Slot内的元数据结构也通过 RIO 零拷贝地传递。 - 自动降级: 在提交操作时 (
submit.rs),驱动会自动检测是否满足 RIO 条件(Buffer 已注册、RQ 获取成功)。如果满足则走 RIO 路径;否则无缝回退到普通 IOCP。
3. 模块内结构 (Internal Structure)
src/driver/iocp/
├── op.rs // IocpOp 定义, VTable, Union Payload, 宏定义
├── submit.rs // 核心提交逻辑,包含各 Op 的 submit_* 函数及 RIO 升级检查
├── blocking.rs // 阻塞任务线程池
├── ext.rs // Winsock 扩展加载 (ConnectEx, AcceptEx, RIO Table)
├── inner.rs // 内部实现细节 (State, IocpDriver 实现)
├── rio.rs // RIO 状态管理 (RioState), Buffer 注册, RQ 管理
└── tests.rs // 单元测试
外部交互:
iocp.rs: 驱动入口,包含IocpDriver结构体定义。../slot.rs: 定义Slot结构体及OVERLAPPED的嵌入方式。
4. 代码详细分析 (Detailed Analysis)
4.1 IocpDriver (iocp.rs & inner.rs)
IocpDriver 是整个驱动的核心,它负责协调 IOCP 和 RIO:
- 资源管理:
port: IOCP 句柄。rio_state:Option<RioState>,封装了所有 RIO 相关资源。ops:OpRegistry,其中的shared部分持有SlotTable。
- 文件注册:
register_files不仅更新 IOCP 的文件表,还会通知RioState调整其内部映射表,确保存储 RQ 的 vector 足够大。 - 主循环 (
get_completion):- 计算定时器超时。
- 调用
GetQueuedCompletionStatus等待事件。 - 如果收到
RIO_EVENT_KEY,则调用rio_state.process_completions处理 RIO 完成事件。 - 如果是普通 IOCP 事件,通过指针反推(Pointer Back-tracing)从
OVERLAPPED指针计算出Slot索引,然后处理完成逻辑。
4.2 RIO 状态管理 (rio.rs)
RioState 集中管理 RIO 资源:
- RQ 管理:
rio_rqs:HashMap<HANDLE, RIO_RQ>用于原始句柄。registered_rio_rqs:Vec<Option<RIO_RQ>>用于注册文件,提供 O(1) 访问。
- Buffer 管理: 维护
chunk_registry: Vec<RIO_BUFFERID>以及slab_rio_pages(用于内部地址缓冲区的 RIO 注册)。 - 完成处理:
process_completions使用RIODequeueCompletion批量获取完成结果,并更新对应Slot的状态。
4.3 智能提交 (submit.rs)
该模块定义了所有操作的提交逻辑。宏 submit_io_op! 和 impl_lifecycle! 简化了代码。
- Recv/Send:
检查
ctx.rio是否存在。如果存在,尝试调用rio.try_submit_recv/send。这些函数会尝试进行 O(1) 的 Buffer 和 RQ 查找。 - UDP 支持 (
SendTo/RecvFrom): 驱动实现了try_submit_send_to和try_submit_recv_from。利用RIOSendEx和RIOReceiveEx,以及预先注册的 Slab 页面(用于存放地址信息),实现了全路径的 RIO UDP 支持。
4.4 操作定义 (op.rs)
通过 define_iocp_ops! 宏定义了如 Accept, SendTo, Wakeup 等操作。
- SubmitContext: 包含
rio: Option<&'a mut RioState>,使得提交逻辑可以访问 RIO 状态。 - 复杂 Payload: 如
SendToPayload包含了WSABUF和地址存储。对于 RIO 路径,这些结构体在Slot中的位置已被注册,可直接作为RIO_BUF使用。
5. 存在的问题和 TODO
-
SyncFileRange 语义:
- Windows 缺乏对应 Linux
sync_file_range的细粒度刷新 API。目前实现为全文件 Flush,性能可能低于预期。
- Windows 缺乏对应 Linux
-
线程池策略:
blocking.rs中的线程池虽然支持动态伸缩,但缺乏基于负载的精细化调度,海量阻塞文件操作可能导致线程数抖动。
6. 未来的方向 (Future Directions)
-
完成通知批处理:
- 目前
GetQueuedCompletionStatus是一次处理一个。可以引入GetQueuedCompletionStatusEx批量获取,减少系统调用。注:RIO 的process_completions已经实现了批量出队 (MAX 128)。
- 目前
-
错误码标准化:
- 进一步完善 WSA Error 到
std::io::Error的映射,提供更跨平台一致的错误语义。
- 进一步完善 WSA Error 到
缓冲区管理模块 (veloq-buf)
本文档详细介绍了 veloq-buf crate。该模块负责高性能异步 I/O 的内存管理,特别针对 io_uring 和 IOCP 的需求进行了优化,提供了一套能够保持地址稳定、支持类型擦除且对零拷贝友好的内存池抽象。
注意: 原有的 io::buffer 模块已独立为 veloq-buf crate,以提供更好的复用性和隔离性。
1. 概要 (Overview)
veloq-buf 不仅仅是一个内存分配器,它是连接用户态内存与内核 I/O 的桥梁。其核心设计目标包括:
- 地址稳定 (Address Stability): 异步 I/O 提交期间,缓冲区物理地址不可变。
- 注册友好 (Registration Friendly): 为了支持 io_uring 的
IORING_REGISTER_BUFFERS或 Windows RIO,底层内存必须易于提取并以大块形式注册。 - 灵活的池拓扑 (Flexible Pool Topology): 通过
PoolTopologytrait,支持多种内存管理策略(如全局共享池、独立池等)。 - 动态扩展 (Dynamic Expansion): 支持运行时动态增加内存块 (Chunk),突破静态内存限制。
- 类型擦除: 通过
AnyBufPool和手动 VTable,使得上层应用无需关心底层的具体分配策略。 - 高内聚架构: 所有的内存分配实现细节(Heap)与对外接口(Buffer)分离,互不干扰。
核心组件结构:
FixedBuf: 面向用户的最终句柄,拥有底层内存块的所有权,通过 VTable 进行释放。内部内嵌了 64位的 context 用于路由释放逻辑。BufPoolTrait: 面向用户的顶层接口,提供alloc方法返回FixedBuf。PoolTopologyTrait: 定义运行时内存池的初始化、构建和监听逻辑。UniformSlot: 标准的拓扑实现,采用 Sharded Global Pool + Superblock Cache 策略。heap::GlobalSlotPool: 全局内存管理器,管理多个物理内存块 (Chunk)。heap::Chunk: 单个连续内存块,内部被切分为多个分片 (Shards) 以减少锁竞争。SlotBasedPool: 线程本地的 Pool 句柄,指向全局的GlobalSlotPool。heap::buddy::BuddyAllocator: 底层分配算法,管理分片内的内存,支持 Order 0 (4KB) 到 Order 18 (1GB) 的分配。heap::superblock::SuperblockState: 针对 4KB 小对象的快速分配缓存,使用原子操作管理 64 个 Slot。
2. 理念和思路 (Philosophy and Design)
2.1 架构分离:Heap vs Buffer
veloq-buf 采用了高内聚低耦合的架构设计:
- Heap Layer (
src/heap.rs): 负责所有与“如何分配内存”相关的逻辑。包含物理内存申请 (MemoryChunk)、伙伴系统算法 (BuddyAllocator)、全局分片管理 (GlobalSlotPool) 以及无锁缓存 (Superblock)。 - Buffer Layer (
src/buffer.rs): 负责所有与“如何使用内存”相关的逻辑。包含句柄定义 (FixedBuf)、池接口 (BufPool)、类型擦除 (AnyBufPool) 以及拓扑定义 (PoolTopology)。
这种分离确保了底层分配算法的变更不会影响上层接口,同时使得代码结构更加清晰。
2.2 全局分块与分片架构 (Sharded Global Pool Strategy)
UniformSlot 拓扑摒弃了旧的单一 Block 策略,采用了更具扩展性的 Dynamic Chunks + Sharded Slot 架构:
- 动态 Chunk 管理:
GlobalSlotPool维护一个Vec<Arc<Chunk>>。初始时分配一个大块内存,当内存不足时,自动申请新的Chunk(默认扩展大小 64MB)。 - 分片 (Sharding): 每个
Chunk内部将内存切分为 $K$ 个分片 (Shard),$K$ 根据 CPU 核心数动态调整(最小 16)。每个分片拥有独立的BuddyAllocator和互斥锁。 - Superblock 缓存 (L0 Cache):
- 针对最常用的 4KB 分配 (Order 0),每个线程维护一个本地的 Superblock 引用 (TLS Cache)。
- 一个 Superblock 包含 64 个 Slot (256KB, Order 6)。
- 线程在 Superblock 内分配只需原子操作 (
AtomicU64),无需全局锁,实现极高的热路径性能。
- 负载均衡与窃取:
- 当本地 Shard 分配失败或争用严重时,分配器会根据线程 ID 哈希计算“步长 (Stride)”,尝试遍历其他 Shard 进行内存窃取 (Work Stealing)。
- 这保证了在个别 Shard 耗尽时,系统仍能利用整体剩余内存。
2.3 内存稳定性与生命周期
FixedBuf 句柄通过 VTable 和 Context (u64) 唯一确定了其归属的内存位置。无论句柄在线程间如何传递,其指向的物理地址恒定不变,满足 Proactor 模式下内核直接写入的需求。
2.4 Direct I/O 对齐
核心单元 Slot 大小固定为 4KB。所有分配均基于 4KB 对齐,天然满足 O_DIRECT / FILE_FLAG_NO_BUFFERING 以及各种 DMA 操作的严格对齐要求。
3. 模块内结构 (Internal Structure)
veloq-buf/src/
├── lib.rs // 模块导出与基础宏定义 (nz!)
├── buffer.rs // 接口层:FixedBuf, BufPool, PoolTopology, UniformSlot, SlotBasedPool
├── heap.rs // 实现层入口:GlobalSlotPool, MemoryChunk, Chunk
├── os.rs // OS 特定实现:Huge Page 分配
└── heap/ // 堆管理子模块
├── buddy.rs // BuddyAllocator: 伙伴系统实现 (0~1GB)
├── superblock.rs // SuperblockState: 4KB 对象的原子位图分配器
└── slot.rs // Slot 定义 (4KB) 与 SlotIndex, SLOT_SIZE
4. 代码详细分析 (Detailed Analysis)
4.1 PoolTopology 抽象 (buffer.rs)
PoolTopology 的职责进一步明确为“初始化全局状态”、“为 Worker 建池并注册内存”以及“监听动态扩展”:
#![allow(unused)]
fn main() {
pub trait PoolTopology: Clone + Send + Sync {
type State: Clone + Send + Sync;
fn init(&self, worker_count: usize) -> std::io::Result<Self::State>;
fn build(
&self,
state: &Self::State,
worker_idx: usize,
registrar: Box<dyn BufferRegistrar>,
) -> AnyBufPool;
// 新增:监听动态内存扩展事件
fn connect_listener(
&self,
state: &Self::State,
listener: Box<dyn Fn(crate::heap::ChunkInfo) + Send + Sync>,
);
}
}
在 UniformSlot 实现中,State 即为 Arc<crate::heap::GlobalSlotPool>。当 GlobalSlotPool 分配新 Chunk 时,会触发 listener 回调(通常由 Driver 用于注册新内存)。
4.2 Superblock 与原子分配 (heap/superblock.rs)
Superblock 是 4KB 分配的加速层。它管理一个 Order 6 (256KB) 的内存块。
- 状态管理:
free_mask(AtomicU64): 位图标记 64 个 Slot 的占用情况。is_active(AtomicBool): 标记该 Superblock 是否正被某个线程作为“活跃缓存”持有。
- 分配 (Alloc): 使用
compare_exchange_weak在free_mask上寻找并置零一位,实现无锁分配。 - 释放 (Free): 使用
fetch_or归还位。如果释放后 Superblock 变为空且非活跃,则将其归还给底层 Buddy System。
4.3 核心对象 (buffer.rs)
FixedBuf:
context 字段 (u64) 被深度利用,紧凑存储了元数据:
Layout: [ChunkID 16b] [Reserved 16b] [Order 8b] [SlotIndex 24b]
ChunkID: 对应GlobalSlotPool中的 Chunk 索引 (u16)。Reserved: 目前保留为 0 (16位)。Order: 分配时的阶数 (Buddy Order, 8位)。SlotIndex: Chunk 内唯一的 Slot 索引 (24位),支持最大 64GB 单 Chunk。
SlotBasedPool:
用户侧的 Pool 实现。
- Tiny Alloc (<=4KB): 获取当前活跃 Chunk(通常是 ID 0 或最近使用的),通过 TLS Cache 获取当前活跃的
Superblock,尝试原子分配。 - Large Alloc (>4KB) / Miss: 穿透到
GlobalSlotPool::alloc_slots,该方法会遍历所有 Chunk,并在必要时扩展新 Chunk。
4.4 Sharded BuddyAllocator (heap.rs & heap/buddy.rs)
每个 Chunk 内部维护了一组 BuddyAllocator。
- Sharding: 内存被均分。线程通过
hash(thread_id)决定首选 Shard。 - Buddy System:
- 维护
MAX_ORDER(18) 个双向链表 (LinkedList)。 - 使用
BitSet追踪分配状态,防止 Double-Free。 - 支持合并 (Coalescing) 与分裂 (Splitting)。
- 维护
- Work Stealing: 首选 Shard 锁争用时,通过线性同余法生成的步长 (Stride) 遍历其他 Shard,确保高并发下的吞吐量。
5. 存在的问题和 TODO (Issues and TODOs)
-
动态注册的同步:
- 目前
GlobalSlotPool支持动态扩展 Chunk。 - Driver 层已通过
register_chunk接口实现了对新 Chunk 的增量注册 (支持 io_uring update 和 RIO register)。 - 已实现 (Done):
PoolTopology的 listener 已对接 Runtime 的ExecutorRegistry。当GlobalSlotPool扩展时,通过回调通知 Global Registry 更新 Epoch;所有LocalExecutor在主循环中检查 Epoch 并在本地 Driver 上注册新 Memory Chunk,实现了完全自动化的内存扩展与注册。
- 目前
-
碎片化:
- 虽然 Buddy System 能合并内存,但在长期运行且分配模式复杂的情况下,仍可能产生碎片导致大块内存申请失败。
-
大对象回退:
- 超过 1GB (Order 18) 的分配目前不支持(代码中限制)。虽然对于网络 I/O 及其罕见,但通过
GlobalAlloc回退的机制尚未明确集成在SlotBasedPool中。
- 超过 1GB (Order 18) 的分配目前不支持(代码中限制)。虽然对于网络 I/O 及其罕见,但通过
-
NUMA 感知:
- 当前的分片策略基于线程 ID 哈希,未感知物理 NUMA 节点。在多路服务器上可能导致跨 NUMA 访问。
- TODO: 根据线程绑核情况将 Shard 绑定到特定的 NUMA 内存节点。
socket 模块文档
本文档详细介绍了 veloq-runtime 中的 io::socket 及其相关操作模块 (op.rs)。该模块旨在提供跨平台的网络和文件 I/O 原语抽象,屏蔽 Windows (IOCP) 和 Linux (io_uring) 之间的底层差异。
1. 概要 (Overview)
src/io/socket.rs 和 src/io/op.rs 构成了运行时与用户态 API 之间的操作桥梁。它们的主要职责是:
- 统一 API: 提供统一的结构体(如
Connect,Accept,Recv)来描述 I/O 操作,无论底层使用何种驱动。 - 生命周期管理: 管理异步操作从定义、提交到完成的全生命周期 (
OpLifecycle)。 - 平台适配: 处理不同操作系统在 socket 创建、地址解析和缓冲区管理上的细微差异(例如 Windows 的
AcceptEx需要预先创建 Socket)。
2. 理念和思路 (Philosophy and Design)
2.1 统一的操作封装 (The Op<T> Wrapper)
所有的异步操作都被封装在 Op<T> 结构中,这是一个 Rust Future。
- State Machine:
Op内部维护了一个简单的状态机 (Defined->Submitted->Completed)。 - Ownership: 当操作处于
Submitted状态时,Op将资源的所有权转移给驱动(Driver)。只有在操作完成或被驱动退回时,所有权才会返回给用户。这是为了满足 Proactor 模式下“缓冲区必须保持有效”的要求。
2.2 预分配策略 (Pre-allocation Strategy)
Windows IOCP 的某些 API(特别是 AcceptEx)要求调用者提供“输出参数”所需的资源。例如,AcceptEx 不会像 accept 系统调用那样返回一个新的 Socket fd,而是要求用户先创建一个 Socket 传进去,内核将新连接绑定到这个 Socket 上。
为了抹平这种差异,引入了 OpLifecycle trait:
pre_alloc: 在构造 Op 之前执行。- Windows: 预先调用
socket()创建句柄。 - Linux: 空操作 (No-op)。
- Windows: 预先调用
into_output: 操作完成后,将结果和预分配的资源组合成最终的返回值(如TcpStream)。
2.3 地址无关性
使用 SockAddrStorage (基于 libc::sockaddr_storage 或 Windows SOCKADDR_STORAGE) 来存储地址。这使得上层代码可以统一处理 IPv4、IPv6 甚至 Unix Domain Socket,而无需到处写 match 语句。
3. 模块内结构 (Internal Structure)
src/io/
├── socket.rs // 平台特定 Socket 实现的重导出 (Facade)
├── op.rs // 核心操作定义 (Read, Write, Connect, Accept...) 和 Op Future 实现
├── socket/
│ ├── unix.rs // #[cfg(unix)] 实现
│ └── windows.rs // #[cfg(windows)] 实现
socket.rs: 这是一个 Facade,根据编译目标重导出unix或windows子模块中的Socket类型及辅助函数。op.rs: 定义了所有 I/O 操作的数据结构(Payload)。这些结构体是跨平台的(Condition-less),但其实现细节(如pre_alloc)通过cfg宏进行区分。
4. 代码详细分析 (Detailed Analysis)
4.1 Op<T> Future (op.rs)
#![allow(unused)]
fn main() {
pub struct Op<T: IntoPlatformOp<PlatformDriver>> {
state: State,
data: Option<T>,
user_data: usize,
driver: Weak<RefCell<PlatformDriver>>,
}
}
poll方法实现了提交逻辑:- 如果状态是
Defined,尝试向driver提交操作 (driver.submit)。 - 如果提交失败(这通常不应该发生,除非 Ring 满了且无 Backpress),立即返回错误。
- 如果提交成功,状态变为
Submitted,随后轮询driver.poll_op等待完成。 - 完成时,调用
T::from_platform_op将驱动返回的底层 Op 还原为高级 Op 结构。
- 如果状态是
4.2 关键操作分析
Accept
- 结构: 包含
fd(监听 socket) 和accept_socket(仅 Windows)。 - 差异处理:
- 在 Windows 上,
AcceptEx极其高效,但需要预先创建 socket 并消耗一个重叠结构。OpLifecycle完美封装了这一复杂性。 - 在完成时 (
into_output),Linux 版本直接将 syscall 返回的 fd 包装;Windows 版本则返回预创建的accept_socket,并解析内核填充的地址 buffer。
- 在 Windows 上,
Connect
- 结构:
fd,addr(Raw bytes),addr_len. - 机制: 封装了
connect(Linux) 和ConnectEx(Windows)。
ReadFixed / WriteFixed
- 结构:
fd,buf(FixedBuf),offset. - 特点: 强制使用
FixedBuf,确保了缓冲区在异步过程中的地址稳定性,是实现 Zero-Copy 的基础。
4.3 IoFd 枚举
#![allow(unused)]
fn main() {
pub enum IoFd {
Raw(RawHandle),
Fixed(u32),
}
}
区分了普通文件描述符和 io_uring 的注册文件描述符 (Fixed File)。后者可以避免内核在每次 I/O 时查找文件表的开销,是高性能的关键。
5. 存在的问题和 TODO (Issues and TODOs)
-
VTable 样板代码:
- 目前
IoFd到平台特定 Op 的转换可能涉及较多的样板代码。 - TODO: 利用宏进一步简化
IntoPlatformOp的实现。
- 目前
-
Socket 选项配置:
- 目前的
socket.rs仅暴露了最基础的创建功能。设置TCP_NODELAY,SO_RCVBUF等选项通常需要通过原始句柄进行。 - TODO: 提供更丰富的、类型安全的 Socket 选项配置接口。
- 目前的
-
Buffer 传递:
Op拿走了FixedBuf的所有权。如果操作失败,用户需要能够方便地拿回 Buffer。目前虽然支持,但 API 易用性有待提升。
6. 未来的方向 (Future Directions)
-
Zero-Copy Networking:
- 结合
io_uring的IORING_OP_SEND_ZC,进一步优化Send操作,彻底消除用户态到内核态的数据拷贝。
- 结合
-
AF_XDP 集成:
- 探索通过
socket.rs暴露 AF_XDP 接口,实现内核旁路的高性能包处理。
- 探索通过
-
Completer-Based API:
- 目前是 Future-Based。考虑是否提供基于回调或 Completer 的底层 API,以供极致性能场景使用(减少 Waker 唤醒开销)。
File System (FS) 模块文档
本文档详细介绍了 veloq-runtime 的文件系统模块 (src/fs)。该模块旨在提供接近硬件极限的文件 I/O 性能,特别是在支持异步 I/O (io_uring / IOCP) 的现代操作系统上。
1. 概要 (Overview)
src/fs 模块提供了对底层文件描述符(或句柄)的异步封装。与标准库的 std::fs 不同,本模块的设计初衷是非阻塞 (Non-blocking) 和零拷贝 (Zero-Copy)。
核心组件包括:
File: 文件的异步句柄,实现了AsyncBufRead和AsyncBufWrite。OpenOptions: 高度可配置的文件打开构建器,支持跨平台的缓冲模式设置(如 Direct I/O)。
2. 理念和思路 (Concepts)
2.1 显式缓冲控制 (Explicit Buffering Control)
传统的缓冲 I/O (Buffered I/O) 依赖操作系统的 Page Cache。虽然简单,但在高吞吐场景下(如数据库 WAL、大文件传输)会导致双重拷贝和不可预测的延迟。
Veloq 通过 BufferingMode 提供了对 I/O 模式的精细控制:
Buffered: 标准模式,使用 Page Cache。Direct: 绕过 Page Cache (O_DIRECT / NO_BUFFERING),直接在用户态缓冲区和磁盘间传输数据。这要求缓冲区必须对齐(由BufPool保证)。DirectSync: Direct I/O 加上同步写入 (O_DSYNC / WRITE_THROUGH),确保数据落盘。
2.2 异步资源释放 (Async Drop)
在 Rust 中,Drop 是同步运行的。如果关闭文件描述符 (close/CloseHandle) 发生阻塞,会严重影响 Event Loop 的响应度。
File 实现了非阻塞 Drop 机制:在销毁时,它会尝试向 Runtime 提交一个后台关闭操作 (Close Op)。只有在 Runtime 无法访问(如已关闭)时,才会回退到同步关闭。
3. 模块内结构 (Internal Structure)
src/
├── fs.rs // 模块定义与导出
└── fs/
├── file.rs // 核心文件结构体
└── open_options.rs // 打开选项与标志位处理
fs.rs: 模块入口,负责导出子模块 (file,open_options) 和统一对外类型 (File,OpenOptions)。遵循 Rust 2018 模块标准,摒弃了mod.rs。file.rs: 封装了IoFd和对PlatformDriver的弱引用。提供了read_at,write_at,sync_range等高级操作。
4. 代码详细分析 (Detailed Analysis)
4.1 OpenOptions::open
这是文件打开的入口,采用了 Remote IO 架构以支持跨线程操作。
- Buffer 获取: 打开文件时,需要分配内存存储路径。这里使用了当前线程绑定的
BufPool。 - Op 构建: 调用
build_op,根据BufferingMode设置标志位。 - Remote Submission:
- 使用
RemoteSubmitter提交Open操作。 - 操作会被注入(Inject)到当前 Runtime 线程的 Driver 中执行。
- 关键点: 返回的
File对象会持有指向该 Driver 的Injector。这意味着该文件后续的所有 I/O 操作都会被“路由”回最初打开它的那个 Driver(线程)上执行。这保证了文件句柄(特别是 Windows Handle 或 io_uring 注册资源)的线程安全性。
- 使用
4.2 File 的异步读写与跨线程安全
File 结构体持有 RemoteSubmitter,因此它是 Send 的,可以在线程间传递。
- 路由机制: 当在一个新的线程上调用
file.read_at(...)时,操作并不是在当前线程执行,而是通过RemoteSubmitter发送回该文件绑定的源 Driver 执行。
4.3 File 接口
File 实现了原子偏移量的 I/O:
read_at/write_at: 这对实现数据库至关重要,允许并发地读写文件的不同部分而无需修改文件指针 (seek)。底层对应pread/pwrite。
4.4 SyncRangeBuilder
提供了细粒度的刷盘控制:
#![allow(unused)]
fn main() {
file.sync_range(0, 1024)
.wait_before(false)
.write(true)
.await
}
这段利用 Builder 模式的代码最终生成一个 SyncFileRange Op。
- Linux: 完美映射到
sync_file_range系统调用。 - Windows: 当前回退到
FlushFileBuffers(全文件 sync),因为 Win32 API 缺乏精确的范围同步机制。
5. 存在的问题和 TODO
- 文件元数据操作:
- 目前缺少
metadata(),set_permissions(),set_len()等标准操作。需要添加对应的异步 Op。
- 目前缺少
- 目录操作:
- 缺少
read_dir(I/O intensive) 的异步实现。
- 缺少
- Windows 范围同步:
- Windows 的范围同步回退到全量同步由于性能原因可能不可接受。TODO: 调研是否有未公开 API 或利用
LockFile等机制间接实现更细粒度的控制,或者明确文档警示。
- Windows 的范围同步回退到全量同步由于性能原因可能不可接受。TODO: 调研是否有未公开 API 或利用
- 路径处理:
- 目前路径处理涉及一次从
OsStr到BufPool缓冲区的拷贝。对于极高频打开操作,这可能存在微小开销。
- 目前路径处理涉及一次从
6. 未来的方向
- Registered Files (io_uring):
- 支持
io_uring的 “Fixed Files” 特性。允许用户注册文件描述符,减少内核层面的引用计数开销。这对高频 I/O 场景提效显著。
- 支持
- IO 优先级:
- 暴露 I/O 优先级设置(如
ioprio_set),允许关键任务抢占磁盘带宽。
- 暴露 I/O 优先级设置(如
Net 模块文档
本文档详细介绍了 veloq-runtime 的网络模块 (src/net)。该模块提供了高性能的 TCP 和 UDP 异步原语。
1. 概要 (Overview)
src/net 模块提供了类似于标准库 std::net 的接口,但完全基于 Veloq 的 Proactor 异步模型构建。它支持:
- TCP:
TcpListener和TcpStream,支持高性能的流式传输。 - UDP:
UdpSocket,支持无连接的数据报传输。
所有网络套接字在创建时(bind / connect)都会自动绑定到当前线程的 I/O 驱动 (PlatformDriver)。
2. 理念和思路 (Concepts)
2.1 零抽象成本 (Zero Cost Abstraction)
网络组件是非常薄的包装层。它们仅持有:
fd: 原始文件描述符/句柄。driver: 对驱动的弱引用。 这意味着TcpStream的大小仅相当于两个指针,且方法调用直接转换为 Op 提交,无额外中间层。
2.2 平台差异的屏蔽
- Linux (io_uring): 使用标准 socket API (
connect,accept,send,recv)。 - Windows (IOCP): 使用 Winsock 的扩展函数 (
ConnectEx,AcceptEx)。这些扩展函数通常比标准 BSD socket API 性能更高,但 API 签名差异巨大。src/net内部处理了这些差异(例如AcceptEx需要预分配缓冲区来存放地址信息)。
3. 模块内结构 (Internal Structure)
src/
├── net.rs // 模块定义与导出
└── net/
├── tcp.rs // TcpListener, TcpStream
└── udp.rs // UdpSocket
4. 代码详细分析 (Detailed Analysis)
4.1 套接字创建与绑定
以 TcpListener::bind 为例:
- 地址解析: 使用
std::net::ToSocketAddrs解析地址。 - Socket 创建: 内部调用
socket2或类似逻辑创建 OS socket。 - 驱动关联: 这一步至关重要。
它隐式获取了当前任务所在的 Runtime Driver。这意味着所有的网络对象必须在 Runtime 环境内创建,否则会 panic 或报错。#![allow(unused)] fn main() { let driver = crate::runtime::context::current().driver(); }
4.2 TcpListener::accept
跨平台差异最显著的地方:
- Windows: 使用
AcceptEx。这是一个完全异步的操作,不仅接受连接,还可以通过预读取(Pre-read)数据来减少系统调用。Veloq 的实现目前专注于接受连接,但为了兼容AcceptEx,必须预分配一块内存用于内核写入本地和远程地址。 - Linux: 简单的提交
AcceptOp。
4.3 UdpSocket::recv_from
该方法返回 (usize, SocketAddr)。
- 内部提交
RecvFromOp。 - 当 Op 完成时,驱动层已将数据写入
FixedBuf,并将源地址信息写入 OS 结构体。recv_from负责将这些原始字节转换为 Rust 的SocketAddr类型。
5. 存在的问题和 TODO
-
Socket 选项:
- 目前缺少设置
TTL,NoDelay,KeepAlive,ReusePort等常用选项的公开 API。用户目前可能需要通过std::os::unix::io::AsRawFd绕过 Veloq 去设置,这不优雅。 - TODO: 在
TcpStream/UdpSocket上直接暴露配置方法。
- 目前缺少设置
-
Split API:
- Tokio 提供了
split()方法将 Stream 拆分为ReadHalf和WriteHalf,允许在不同任务中并发读写。虽然TcpStream本身可以 clone(因为底层是引用计数或共享句柄),但提供明确的 Split API 更符合用户习惯。
- Tokio 提供了
-
IPv6 Dual Stack:
- 目前的
bind逻辑虽然支持 v4/v6,但对于双栈行为(IPV6_V6ONLY)的控制未明确暴露。
- 目前的
6. 未来的方向
-
Zero-Copy Networking:
- 集成 Linux
MSG_ZEROCOPY支持。对于大包发送,这能显著减少内核态内存拷贝。
- 集成 Linux
-
QUIC 支持:
- 基于
UdpSocket构建对 QUIC 协议(如quinn)的深度集成,利用io_uring的sendmsg/recvmsg批量处理能力 (GSO/GRO) 提升吞吐。
- 基于