Rust in Everything
基于 loom 0.7.2

泄漏检测

主线位置:主线 1 每一轮的最后一步。scheduler.run 返回(所有模型线程都结束、lazy_static 值已 drop)之后,Builder::check 调 execution.check_for_leaks(),然后才 step 到下一条路径。

什么时候查、查什么

model.rs:193-204:模型闭包外面包了一层,闭包返回后先 drop 本轮的 lazy_static 值,再结束主线程;run 返回后立刻检查泄漏。检查本身遍历对象仓库(rt/object.rs:249-260):

    pub(crate) fn check_for_leaks(&self) {
        for (index, entry) in self.entries.iter().enumerate() {
            match entry {
                Entry::Alloc(entry) => entry.check_for_leaks(index),
                Entry::Arc(entry) => entry.check_for_leaks(index),
                Entry::Channel(entry) => entry.check_for_leaks(index),
                _ => {}
            }
        }
    }
对象判定报错
Alloc(Track<T> 与 loom::alloc::alloc)!is_droppedAllocation leaked.(rt/alloc.rs:82-94)
Arc(loom::sync::Arc)ref_cnt != 0Arc leaked.(rt/arc.rs:170-182)
Channel(loom::sync::mpsc)msg_cnt != 0Messages leaked.(rt/mpsc.rs:145-163)

报错第二行是 Index: n,即对象在仓库里的下标(这一轮里第几个被创建的 loom 对象);开了 LOOM_LOCATION 时多一行创建位置,Arc / Alloc 是「Allocated: 文件:行:列」,channel 是「Channel created: …」(0.5.5 的 #265)。只查第一个泄漏的对象,查到就 panic。

flowchart LR
  f["模型闭包返回"] --> ls["drop 本轮的 lazy_static 值"]
  ls --> td["thread_done:主线程结束"]
  td --> run["scheduler.run 返回"]
  run --> chk["check_for_leaks:遍历对象仓库"]
  chk -->|"Arc:ref_cnt != 0"| p1["Arc leaked."]
  chk -->|"Alloc:未 drop"| p2["Allocation leaked."]
  chk -->|"Channel:msg_cnt != 0"| p3["Messages leaked."]
  chk -->|"都没有"| step["execution.step():下一轮"]

原子、锁、UnsafeCell 这些对象不在检查之列:它们只是一段状态,丢了不算泄漏;std 的类型 loom 更看不见。

loom::sync::Arc 怎么计数

sync/arc.rs:9-12:

pub struct Arc<T: ?Sized> {
    obj: std::sync::Arc<rt::Arc>,
    value: std::sync::Arc<T>,
}

数据仍在 std 的 Arc 里,计数另记一份在 rt::Arc 的 ref_cnt。clone 先调 ref_inc(调度点,动作 RefInc),drop 调 ref_dec(调度点,动作 RefDec,做一次 release;减到 0 时再做一次 acquire,注释说真实实现里这是一道 fence),见 05 Arc、mpsc 与 future。泄漏检查看的就是这个 ref_cnt。

两条会碰到计数的路径:

  • Arc::new 就是 from_std。 from_std 要求传入的 std Arc 是唯一的,新建一个 rt::Arc,并把「数据指针 → rt::Arc」登记进 Execution.arc_objs(sync/arc.rs:103-118)。unsized 的 Arc<[T]>、Arc<dyn Trait> 只能这样构造,因为 std 的 unsize 强转靠不稳定的 CoerceUnsized(0.5.5 的 #226)。
  • into_raw / from_raw。 into_raw 是 mem::forget 加取指针,计数不变;from_raw 用指针去 arc_objs 里查回 rt::Arc(sync/arc.rs:176-212)。只 into_raw 不 from_raw,计数永远不归零。

Track 与 loom::alloc

Track<T>(alloc.rs:77-109)给任意值挂一个 rt::Allocation:构造时在仓库里登记一个 Alloc 对象,Allocation drop 时把它标成 is_dropped。into_inner 拆出值、drop 守卫,不算泄漏。

loom::alloc::{alloc, alloc_zeroed, dealloc}(alloc.rs:23-75)先调 std 的同名函数,再按指针地址登记:Execution.raw_allocations 是一张 HashMap<usize, Allocation>(rt/alloc.rs:18-51)。

  • alloc:插入登记表;同一地址已经登记过就 panic「pointer already tracked」。
  • dealloc:从登记表取出并 drop 那个 Allocation(标成已释放);找不到就 panic「pointer not tracked」。
  • 用 std 的 dealloc 释放 loom 分配的指针,登记表不知道,报泄漏。文档自己写了这一点(alloc.rs:52-58,「may report false positives」)。

实验 E8:十一个场景

方法(exp-loom/checkers/tests/leaks.rs):每个场景一个 #[test],用 run_each.sh 逐个在独立进程里跑,记录报错与退出码;再开 LOOM_LOCATION=1 跑一遍。

场景结果退出码
mem::forget(loom::sync::Arc::new(1))第 1 个交错「Arc leaked.」0(正常 panic)
mem::forget(Track::new(5))第 1 个交错「Allocation leaked.」0
mem::forget(std::sync::Arc::new(1))通过0
Arc::into_raw,不 from_raw「Arc leaked.」0
Arc::into_raw 后 from_raw 再 drop通过0
loom::alloc::alloc + loom::alloc::dealloc通过0
loom::alloc::alloc,不释放「Allocation leaked.」,随后第二次 panic,abort134
loom::alloc::alloc,用 std::alloc::dealloc 释放同上134
std::alloc::alloc 的指针交给 loom::alloc::dealloc「pointer not tracked」0
channel 里 send 一条消息,没人 recv,Receiver 正常 drop通过0
同上,但 mem::forget(rx)「Messages leaked.」,Index: 0、Messages: 10
  • std 的 Arc 泄漏 loom 看不见,这是 E8 原计划的对照组。
  • channel 那两行:Receiver 的 Drop 会把剩下的消息全部 recv 掉(sync/mpsc.rs:82-89),和 std 丢弃未读消息的效果一样,所以只有 Receiver 本身泄漏时才报「Messages leaked.」。
  • 开 LOOM_LOCATION=1 后,forget(loom Arc) 的报错是:
Arc leaked.
  Allocated: tests/leaks.rs:11:24
      Index: 0

原始分配泄漏之后为什么 abort

表里退出码 134 的两行,输出是:

Allocation leaked.
  Index: 0
cannot access Loom execution state from outside a Loom model. are you accessing a Loom synchronization primitive from outside a Loom test (a call to `model` or `check`)?

然后进程 abort。原因从回溯上看得很清楚:

  1. check_for_leaks 在 Builder::check 里调用(model.rs:204),此时已经在调度器之外,STATE 没有设置。
  2. 它 panic 后展开 Builder::check 的栈帧,局部变量 execution 被 drop,其中的 raw_allocations 登记表还存着那个没释放的 Allocation。
  3. Allocation::drop(rt/alloc.rs:68-80)要调 location!() 和 rt::execution,在模型外调用就 panic「cannot access Loom execution state …」(scheduler.rs:123-132)。
  4. 展开期间的第二次 panic,进程 abort。

Track 泄漏不会这样:被 forget 的是 Track 本身,它的 Allocation 不在任何登记表里,展开时没有东西要 drop。所以同样报「Allocation leaked.」,Track 是正常的测试失败,loom::alloc 是整个测试进程退出。实际影响:一个 #[should_panic] 的泄漏测试、或者同一个测试二进制里的其他测试,都会被这次 abort 带走。

小结

  • 每轮结束时检查 Arc(计数不为 0)、Alloc(未释放)、Channel(还有消息)三类对象,只报第一个。
  • loom::sync::Arc 另记一份计数;Arc::new 就是 from_std;into_raw 不配 from_raw 就泄漏。
  • Track<T> 给任意值挂泄漏检查;loom::alloc 按地址登记,用 std 释放会被误报泄漏,释放未登记的指针 panic。
  • E8:loom Arc、Track 的泄漏第 1 个交错报出,std Arc 看不见;Receiver drop 时会清空 channel。
  • loom::alloc 分配泄漏时,报告之后在展开中二次 panic,进程 abort(退出码 134)。

💬 关联讨论

发起讨论
加载中...