泄漏检测
主线位置:主线 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_dropped | Allocation leaked.(rt/alloc.rs:82-94) |
Arc(loom::sync::Arc) | ref_cnt != 0 | Arc leaked.(rt/arc.rs:170-182) |
Channel(loom::sync::mpsc) | msg_cnt != 0 | Messages 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 怎么计数
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要求传入的 stdArc是唯一的,新建一个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,abort | 134 |
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: 1 | 0 |
- 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。原因从回溯上看得很清楚:
check_for_leaks在Builder::check里调用(model.rs:204),此时已经在调度器之外,STATE没有设置。- 它 panic 后展开
Builder::check的栈帧,局部变量execution被 drop,其中的raw_allocations登记表还存着那个没释放的Allocation。 Allocation::drop(rt/alloc.rs:68-80)要调location!()和rt::execution,在模型外调用就 panic「cannot access Loom execution state …」(scheduler.rs:123-132)。- 展开期间的第二次 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 个交错报出,stdArc看不见;Receiverdrop 时会清空 channel。 loom::alloc分配泄漏时,报告之后在展开中二次 panic,进程 abort(退出码 134)。