用 BFS 找出循环锁依赖的短路径
Linux lockdep 在加入新的锁依赖前,用 BFS 查找反向可达路径;若形成环,报告一条较短的依赖链,帮助定位潜在死锁。
工程问题#
某条执行路径持有 A 再获取 B,另一条路径可能以相反次序获取锁;依赖关系也可能经过多个锁类间接成环。调试器需要从已经观察到的依赖中找到冲突,并给出便于阅读的路径。
实际实现#
lockdep 把锁类之间的获取顺序组织为依赖图。check_noncircular 从待获取锁出发,调用 check_path 和 __bfs_forwards 搜索已持有锁;搜索根据读写锁依赖类型过滤无效路径,并利用访问标记、父指针与环形队列逐层扩展。找到匹配后,由父链生成循环依赖报告。
处理流程#
- 获取新锁时,初始化反向可达性检查所需的搜索根
- 用环形队列按层展开锁类的依赖列表,跳过已处理状态
- 按强依赖规则过滤不构成死锁路径的读写锁组合
- 若搜索到已持有锁,则回溯父链并输出循环锁依赖诊断
源码片段#
固定版本 · 原文摘录2180check_noncircular(struct held_lock *src, struct held_lock *target,2181 struct lock_trace **const trace)2182{2183 enum bfs_result ret;2184 struct lock_list *target_entry;2185 struct lock_list src_entry;21862187 bfs_init_root(&src_entry, src);21882189 debug_atomic_inc(nr_cyclic_checks);21902191 ret = check_path(target, &src_entry, hlock_conflict, NULL, &target_entry);21922193 if (unlikely(ret == BFS_RMATCH)) {建立搜索根并调用 check_path,目标是确认新增依赖是否会与已记录路径成环;匹配后的报告逻辑位于函数后半段。
2154static noinline enum bfs_result2155check_path(struct held_lock *target, struct lock_list *src_entry,2156 bool (*match)(struct lock_list *entry, void *data),2157 bool (*skip)(struct lock_list *entry, void *data),2158 struct lock_list **target_entry)2159{2160 enum bfs_result ret;21612162 ret = __bfs_forwards(src_entry, target, match, skip, target_entry);21632164 if (unlikely(bfs_error(ret)))2165 print_bfs_bug(ret);21662167 return ret;2168}把冲突匹配函数与搜索根交给 __bfs_forwards,出现搜索错误时单独报告,不把错误当成“未发现路径”。
1743 lockdep_assert_locked();17441745 __cq_init(cq);1746 __cq_enqueue(cq, source_entry);17471748 while ((lock = __bfs_next(lock, offset)) || (lock = __cq_dequeue(cq))) {1749 if (!lock->class)1750 return BFS_EINVALIDNODE;17511752 /*1753 * Step 1: check whether we already finish on this one.1754 *1755 * If we have visited all the dependencies from this @lock to1756 * others (iow, if we have visited all lock_list entries in1757 * @lock->class->locks_{after,before}) we skip, otherwise go1758 * and visit all the dependencies in the list and mark this1759 * list accessed.1760 */1761 if (lock_accessed(lock))1762 continue;1763 else1764 mark_lock_accessed(lock);初始化环形队列,按当前兄弟项或出队顺序推进,跳过已处理的依赖列表。
1809 first = true;1810 head = get_dep_list(lock, offset);1811 list_for_each_entry_rcu(entry, head, entry) {1812 visit_lock_entry(entry, lock);18131814 /*1815 * Note we only enqueue the first of the list into the1816 * queue, because we can always find a sibling1817 * dependency from one (see __bfs_next()), as a result1818 * the space of queue is saved.1819 */1820 if (!first)1821 continue;18221823 first = false;18241825 if (__cq_enqueue(cq, entry))1826 return BFS_EQUEUEFULL;18271828 cq_depth = __cq_get_elem_count(cq);1829 if (max_bfs_queue_depth < cq_depth)1830 max_bfs_queue_depth = cq_depth;为依赖设置访问关系,只将列表首项入队,其余兄弟项由 __bfs_next 访问;显式检查队列容量并记录最大深度。
四段原文从循环检查入口追到 BFS 调用与队列扩展。强依赖过滤、命中判定和诊断回溯在同一完整文件的相邻代码中,未把整个锁验证器等同于一个通用 BFS 函数。
收益与取舍#
工程分析带来的收益
- 在实际线程卡死前,根据已观察到的锁获取顺序发现潜在循环依赖
- BFS 优先得到边数较少的依赖路径,便于定位导致环路的锁获取位置
付出的代价
- 普通邻接表 BFS 的遍历成本为 O(V+E);此实现还包含强依赖状态过滤、诊断与锁链缓存,不能把该式当成一次获取锁的完整开销
- 环形队列容量由配置限制,队列满会报搜索错误,不能当成不存在依赖路径
- 结论依赖已经观察到的锁类和获取顺序,以及正确的锁注解;未执行路径不自动获得验证
可比较的选择
深度优先搜索也能判断可达性,但首次找到的路径不保证边数最少,可能使诊断链更长。
只检查直接的 A→B、B→A逻辑简单,但会漏掉经由多个锁类构成的循环依赖。
版本与适用范围#
对应 Linux v6.12、启用相应 lockdep 调试配置的锁类依赖检查;报告潜在锁序冲突,不代表当前线程已处于实际死锁。源码中 __bfs_next 先遍历同一依赖列表的兄弟项,队列仅保存列表首项以节省空间;不能把它简化成不加过滤地遍历任意原始有向图。
参考来源#
源码、官方文档与原始论文kernel/locking/lockdep.c · check_noncircular / check_path
相关实现与比较#
1 个相关案例TensorFlow 从本次请求的输出和目标操作出发,沿输入边做 BFS,只保留这些目标依赖的计算节点。