Rust in Everything
基于 embassy-executor 0.10.0

执行器

这一部分讲 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,或者空转)。

  1. 任务存储:TaskStorage 是「任务头 + 未初始化的 future」,TaskPool 是它的数组;每个任务函数一个 static,稳定版 Rust 上靠 const fn 从函数项类型推出大小,放在 .bss;实测 Cortex-M4 上任务头 40 字节,其中 24 字节是给定时器队列留的;池满在调用任务函数时就返回 SpawnError::Busy。
  2. 状态与运行队列:状态只有 SPAWNED 和 RUN_QUEUED 两位,按目标三选一实现;运行队列是 cordyceps 的 TransferStack,一次取走整批、批内后进先出;同一个任务被唤醒三次只 poll 一次;waker 就是任务指针加一张静态 vtable。
  3. Spawner:Spawner 只能在执行器线程用,SendSpawner 可以跨线程但只收 Send 的令牌;async fn 任务即使 future 不是 Send,只要参数是 Send 就能跨线程 spawn;for_current_executor 从 waker 反查执行器。
  4. 各平台的执行器:所有平台共用 raw::Executor,差别只在主循环和 __pender;Cortex-M 的线程模式执行器用 WFE / SEV,InterruptExecutor 借一个空闲中断;QEMU 上实测,同一执行器里高优先级任务要等低优先级任务忙完 24 ms,放进 InterruptExecutor 后是 214 个 SysTick 周期;stm32 的低功耗执行器把 WFE 换成进 STOP 模式。
  5. 宏:#[task] 把函数拆成内层的 async fn 和外层返回 Result<SpawnToken, SpawnError> 的函数,static POOL 藏在外层函数体里;#[main] 在栈上建执行器、把 main 也变成一个任务;宏自己检查泛型、impl Trait、非 'static 引用。
  6. 调度策略: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,没有引用计数,也不会被释放。

💬 关联讨论

发起讨论
加载中...