执行器
这一部分讲 embassy-executor 0.10.0(tag embassy-executor-v0.10.0)和它的宏 crate embassy-executor-macros 0.8.0。前者 src/ 下 3258 行(不算 tests/),后者 839 行。核心在 src/raw/ 里:任务的内存布局、状态位、运行队列、waker;src/platform/ 下每个文件是一种平台的主循环,几十到两百多行;spawner.rs 和 metadata.rs 是对外的 API。
整个执行器回答三个问题:任务的内存从哪来(编译期定好大小的 static),醒来的任务排在哪(任务头里内嵌的链表节点),没事做时 CPU 干什么(平台决定:WFE、条件变量、WFI,或者空转)。
- 任务存储:
TaskStorage是「任务头 + 未初始化的 future」,TaskPool是它的数组;每个任务函数一个static,稳定版 Rust 上靠 const fn 从函数项类型推出大小,放在.bss;实测 Cortex-M4 上任务头 40 字节,其中 24 字节是给定时器队列留的;池满在调用任务函数时就返回SpawnError::Busy。 - 状态与运行队列:状态只有
SPAWNED和RUN_QUEUED两位,按目标三选一实现;运行队列是 cordyceps 的TransferStack,一次取走整批、批内后进先出;同一个任务被唤醒三次只 poll 一次;waker 就是任务指针加一张静态 vtable。 - Spawner:
Spawner只能在执行器线程用,SendSpawner可以跨线程但只收Send的令牌;async fn任务即使 future 不是Send,只要参数是Send就能跨线程 spawn;for_current_executor从 waker 反查执行器。 - 各平台的执行器:所有平台共用
raw::Executor,差别只在主循环和__pender;Cortex-M 的线程模式执行器用WFE/SEV,InterruptExecutor借一个空闲中断;QEMU 上实测,同一执行器里高优先级任务要等低优先级任务忙完 24 ms,放进InterruptExecutor后是 214 个 SysTick 周期;stm32 的低功耗执行器把WFE换成进 STOP 模式。 - 宏:
#[task]把函数拆成内层的async fn和外层返回Result<SpawnToken, SpawnError>的函数,static POOL藏在外层函数体里;#[main]在栈上建执行器、把main也变成一个任务;宏自己检查泛型、impl Trait、非'static引用。 - 调度策略:
scheduler-priority和scheduler-deadline把整批出队换成「每次取一个最优的」,代价是失去批次公平,实测高优先级任务连续 yield 时把其他任务全部推后;开scheduler-deadline后所有任务池从.bss挪进.data;trace的七个钩子由用户定义。
flowchart TB macro["#[task] 宏"] --> pool["static TaskPool"] pool --> storage["TaskStorage"] spawner["Spawner"] --> exec["raw::Executor"] storage --> exec waker["Waker"] --> wake["wake_task"] wake --> rq["RunQueue"] rq --> pender["__pender"] pender --> plat["平台主循环"] plat --> poll["Executor::poll"] poll --> rq
前两页是数据结构,第三页是 API,第四页是「谁来调用 poll」,第五页回到编译期,第六页是 0.10.0 的新东西。定时器怎么借用任务头里的那块内存,见定时器队列;和 tokio 对照:tokio 的任务是一次堆分配加引用计数(见 任务的内存布局),embassy 的任务是编译期就定下来的一格 static,没有引用计数,也不会被释放。
💬 关联讨论
发起讨论加载中...