检测服务启动事务中的顺序环
systemd 在应用启动事务前,用 DFS 检查任务之间的先后关系;发现环时尝试移除不影响主任务的事务任务,无法处理则拒绝该事务。
工程问题#
启动一个服务可能同时拉起多个服务,而它们的 Before/After 配置可能相互矛盾,形成彼此等待的环。管理器需要在任务进入执行阶段前识别这种冲突,同时区分用户请求必须保留的任务和可以放弃的附带任务。
实际实现#
manager_add_job_full 先按需求依赖扩展任务事务。transaction_activate 标记对主任务不可缺少的任务,再沿实际执行顺序做递归 DFS:generation 区分本轮访问,marker 同时记录当前路径和前驱;再次遇到本轮 marker 非空的任务就找到了环。沿前驱链查到可删除候选后,移除该 unit 在本次事务中的任务并重新检查;没有候选则返回循环顺序错误。检查通过后安装任务,运行时仍由 job_is_runnable 按相同的 job_compare 规则决定是否等待前置任务。
处理流程#
- 以用户请求的任务为主任务,扩展依赖,并标记必须随主任务保留的任务
- 按 Before/After 关系和任务类型确定实际执行方向,用 DFS 标记当前访问路径
- 遇到路径内已访问任务时,沿前驱链寻找本次事务中可舍弃的 unit 任务
- 可删除则移除并重新检查;否则报告循环依赖,拒绝事务
- 通过后安装任务,执行时继续等待尚未完成的前置任务
源码片段#
固定版本 · 原文摘录729 for (;;) {730 /* Fourth step: Let's remove unneeded jobs that might731 * be lurking. */732 if (mode != JOB_ISOLATE)733 transaction_collect_garbage(tr);734735 /* Fifth step: verify order makes sense and correct736 * cycles if necessary and possible */737 r = transaction_verify_order(tr, &generation, e);738 if (r >= 0)739 break;740 if (r != -EAGAIN)741 return log_warning_errno(r, "Requested transaction contains an unfixable cyclic ordering dependency: %s", bus_error_message(e, r));742743 /* Let's see if the resulting transaction ordering744 * graph is still cyclic... */745 }事务激活阶段实际调用顺序校验。删除候选后返回的 -EAGAIN 触发重新检查和必要的垃圾清理;其他错误直接拒绝事务。
368 if (j->generation == generation) {369 Job *k, *delete = NULL;370 _cleanup_free_ char **array = NULL, *unit_ids = NULL;371372 /* If the marker is NULL we have been here already and decided the job was loop-free from373 * here. Hence shortcut things and return right-away. */374 if (!j->marker)375 return 0;376377 /* So, the marker is not NULL and we already have been here. We have a cycle. Let's try to378 * break it. We go backwards in our path and try to find a suitable job to remove. We use the379 * marker to find our way back, since smart how we are we stored our way back in there. */380 for (k = from; k; k = ((k->generation == generation && k->marker != k) ? k->marker : NULL)) {381382 /* For logging below */383 if (strv_push_pair(&array, k->unit->id, (char*) job_type_to_string(k->type)) < 0)384 log_oom();385386 if (!delete && hashmap_contains(tr->jobs, k->unit) && !job_matters_to_anchor(k))387 /* Ok, we can drop this one, so let's do so. */388 delete = k;同一 generation 下,marker 为空表示此节点已退出当前路径;marker 非空才说明再次进入活动路径。沿 marker 反向查找,仅考虑仍在当前事务中且不影响主任务的候选。
443 j->marker = from ?: j;444 j->generation = generation;445446 /* Actual ordering of jobs depends on the unit ordering dependency and job types. We need to traverse447 * the graph over 'before' edges in the actual job execution order. We traverse over both unit448 * ordering dependencies and we test with job_compare() whether it is the 'before' edge in the job449 * execution ordering. */450 FOREACH_ELEMENT(d, directions) {451 Unit *u;452453 UNIT_FOREACH_DEPENDENCY(u, j->unit, *d) {454 Job *o;455456 /* Is there a job for this unit? */457 o = hashmap_get(tr->jobs, u);458 if (!o) {459 /* Ok, there is no job for this in the transaction, but maybe there is460 * already one running? */461 o = u->job;462 if (!o)463 continue;464 }把前驱写入 marker,根节点指向自己;先从新事务找相邻 unit 的任务,没有时再查看已经安装的任务。实际遍历对象是任务,而非无条件遍历所有 unit 文件。
466 /* Cut traversing if the job j is not really *before* o. */467 if (job_compare(j, o, *d) >= 0)468 continue;469470 r = transaction_verify_order_one(tr, o, j, generation, e);471 if (r < 0)472 return r;473 }474 }475476 /* Ok, let's backtrack, and remember that this entry is not on477 * our path anymore. */478 j->marker = NULL;479480 return 0;job_compare 筛出当前任务实际先于相邻任务的边,再递归检查;正常返回时清空 marker,避免把已经完成的分支误报成环。
421 unit_status_printf(delete->unit,422 STATUS_TYPE_NOTICE,423 status,424 "Ordering cycle found, skipping %s",425 unit_status_string(delete->unit, NULL));426 transaction_delete_unit(tr, delete->unit);427 return -EAGAIN;428 }429430 log_struct(LOG_ERR,431 LOG_UNIT_MESSAGE(j->unit, "Unable to break cycle starting with %s/%s",432 j->unit->id, job_type_to_string(j->type)),433 "%s", strna(unit_ids));434435 return sd_bus_error_setf(e, BUS_ERROR_TRANSACTION_ORDER_IS_CYCLIC,436 "Transaction order is cyclic. See system logs for details.");有可删除候选时,移除该 unit 在本次事务中的任务并返回 -EAGAIN;否则设置 TransactionOrderIsCyclic 错误。删除还可能清理依赖它的事务任务,具体传播见 transaction_unlink_job。
550 UNIT_FOREACH_DEPENDENCY(other, j->unit, UNIT_ATOM_AFTER)551 if (other->job && job_compare(j, other->job, UNIT_ATOM_AFTER) > 0) {552 log_unit_debug(j->unit,553 "starting held back, waiting for: %s",554 other->id);555 return false;556 }557558 UNIT_FOREACH_DEPENDENCY(other, j->unit, UNIT_ATOM_BEFORE)559 if (other->job && job_compare(j, other->job, UNIT_ATOM_BEFORE) > 0) {560 log_unit_debug(j->unit,561 "stopping held back, waiting for: %s",562 other->id);563 return false;564 }565566 return true;实际执行前再次检查 Before/After 关系;如果尚存应先执行的任务,就暂不可运行。这里按任务就绪情况放行,不消费一张预先生成的全局拓扑序。
六段原文包含事务激活、DFS 的路径状态、删除与失败分支,以及运行时顺序约束。主任务重要性由 transaction_find_jobs_that_matter_to_anchor 计算;manager_add_job_full、事务安装和 job_compare 的完整上下文见参考来源。
收益与取舍#
工程分析带来的收益
- 在真正执行事务前发现彼此等待的任务,错误报告可以指出形成环的任务链
- 允许舍弃不影响主任务的附带任务,使部分存在弱依赖冲突的请求仍有机会执行
- 事务校验和运行时就绪检查共享任务顺序比较规则,覆盖启动与停止方向不同的情况
付出的代价
- 访问标记避免在同一轮中反复展开已完成的路径,但每次删除候选都会触发新一轮检查,另有依赖清理和任务合并成本;不能把整个事务处理说成一次 O(V+E) 的拓扑排序
- 递归调用栈随当前依赖路径长度增长;环的定位、日志记录和删除传播也需要工作
- 被移除的附带服务任务不会随这次事务启动;该策略处理任务冲突,不会修改原来的 unit 配置文件,也不承诺删除任务数最少
可比较的选择
遇到顺序环就拒绝整个事务处理策略直接,但也会拒绝本可通过舍弃附带任务完成的主请求。
按入度消减检测环Kahn 算法可通过是否消去全部节点判断环;若还需报告具体环并选择不影响主任务的删除候选,仍需补充遍历和策略。
版本与适用范围#
固定为 systemd v257 的正常任务事务路径,分析重点是启动请求;算法也会按 job_compare 处理停止、重启任务,并可能访问已经安装的任务。Requires/Wants 决定需求依赖和拉入关系,Before/After 决定同时存在任务时的顺序,单独设置 After 不会拉起另一服务。Wants 是弱需求,但能否删除仍要看该 unit 的所有事务任务是否经其他重要依赖路径关联到主任务;“不重要”不是服务的全局优先级。只能删除当前事务中符合条件的候选,不能任意删除已安装任务。DFS 只检查和修正顺序环,不生成完整拓扑序,不把所有服务串行排列;ignore-order 等显式忽略依赖的模式不在本例范围。
参考来源#
源码、官方文档与原始论文src/core/unit-dependency-atom.c · Requires / Wants 对应的依赖行为
src/core/transaction.c · transaction_add_job_and_dependencies · 强弱需求依赖
src/core/transaction.c · transaction_activate · 检查、重试与安装
src/core/transaction.c · transaction_verify_order_one · DFS 环检测
相关实现与比较#
4 个相关案例Linux lockdep 在加入新的锁依赖前,用 BFS 查找反向可达路径;若形成环,报告一条较短的依赖链,帮助定位潜在死锁。
HNSW 从稀疏上层快速定位区域,再在底层扩大候选搜索。
借助计算依赖图与使用者计数,清理既不影响输出也无副作用的节点。
Linux BPF 验证器的 check_cfg 使用显式指令栈执行非递归 DFS,检查跳转目标、可达性及控制流结构。