systemd/服务管理器 · 启动事务与作业顺序

检测服务启动事务中的顺序环

systemd 在应用启动事务前,用 DFS 检查任务之间的先后关系;发现环时尝试移除不影响主任务的事务任务,无法处理则拒绝该事务。

v257调度与资源分配核验 2026-10-12
相关概念

工程问题#

启动一个服务可能同时拉起多个服务,而它们的 Before/After 配置可能相互矛盾,形成彼此等待的环。管理器需要在任务进入执行阶段前识别这种冲突,同时区分用户请求必须保留的任务和可以放弃的附带任务。

实际实现#

manager_add_job_full 先按需求依赖扩展任务事务。transaction_activate 标记对主任务不可缺少的任务,再沿实际执行顺序做递归 DFS:generation 区分本轮访问,marker 同时记录当前路径和前驱;再次遇到本轮 marker 非空的任务就找到了环。沿前驱链查到可删除候选后,移除该 unit 在本次事务中的任务并重新检查;没有候选则返回循环顺序错误。检查通过后安装任务,运行时仍由 job_is_runnable 按相同的 job_compare 规则决定是否等待前置任务。

处理流程#

  1. 以用户请求的任务为主任务,扩展依赖,并标记必须随主任务保留的任务
  2. 按 Before/After 关系和任务类型确定实际执行方向,用 DFS 标记当前访问路径
  3. 遇到路径内已访问任务时,沿前驱链寻找本次事务中可舍弃的 unit 任务
  4. 可删除则移除并重新检查;否则报告循环依赖,拒绝事务
  5. 通过后安装任务,执行时继续等待尚未完成的前置任务

源码片段#

固定版本 · 原文摘录
src/core/transaction.cv257 · L729–L745
查看原文件
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        }
transaction_activate · 调用与重试

事务激活阶段实际调用顺序校验。删除候选后返回的 -EAGAIN 触发重新检查和必要的垃圾清理;其他错误直接拒绝事务。

src/core/transaction.cv257 · L368–L388
查看原文件
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;
transaction_verify_order_one · 识别环与选择候选

同一 generation 下,marker 为空表示此节点已退出当前路径;marker 非空才说明再次进入活动路径。沿 marker 反向查找,仅考虑仍在当前事务中且不影响主任务的候选。

src/core/transaction.cv257 · L443–L464
查看原文件
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                        }
transaction_verify_order_one · 建立路径并选取相邻任务

把前驱写入 marker,根节点指向自己;先从新事务找相邻 unit 的任务,没有时再查看已经安装的任务。实际遍历对象是任务,而非无条件遍历所有 unit 文件。

src/core/transaction.cv257 · L466–L480
查看原文件
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;
transaction_verify_order_one · 递归展开与退出路径

job_compare 筛出当前任务实际先于相邻任务的边,再递归检查;正常返回时清空 marker,避免把已经完成的分支误报成环。

src/core/transaction.cv257 · L421–L436
查看原文件
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.");
transaction_verify_order_one · 删除或拒绝事务

有可删除候选时,移除该 unit 在本次事务中的任务并返回 -EAGAIN;否则设置 TransactionOrderIsCyclic 错误。删除还可能清理依赖它的事务任务,具体传播见 transaction_unlink_job。

src/core/job.cv257 · L550–L566
查看原文件
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;
job_is_runnable · 按顺序等待前置任务

实际执行前再次检查 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 等显式忽略依赖的模式不在本例范围。

参考来源#

源码、官方文档与原始论文
01
systemd · manager_add_job_full · 建立并激活事务

src/core/manager.c · manager_add_job_full · 建立并激活事务

源码v257
02
systemd · 需求依赖与主任务重要性标记

src/core/transaction.c · 需求依赖与主任务重要性标记

源码v257
03
systemd · Requires / Wants 对应的依赖行为

src/core/unit-dependency-atom.c · Requires / Wants 对应的依赖行为

源码v257
04
systemd · transaction_add_job_and_dependencies · 强弱需求依赖

src/core/transaction.c · transaction_add_job_and_dependencies · 强弱需求依赖

源码v257
05
systemd · transaction_activate · 检查、重试与安装

src/core/transaction.c · transaction_activate · 检查、重试与安装

源码v257
06
systemd · transaction_verify_order_one · DFS 环检测

src/core/transaction.c · transaction_verify_order_one · DFS 环检测

源码v257
07
systemd · job_compare · 启停任务的实际先后顺序

src/core/job.c · job_compare · 启停任务的实际先后顺序

源码v257
08
systemd · job_is_runnable · 运行时前置任务检查

src/core/job.c · job_is_runnable · 运行时前置任务检查

源码v257
09
systemd · Wants / Requires 的需求依赖语义

man/systemd.unit.xml · Wants / Requires 的需求依赖语义

官方文档v257
10
systemd · Before / After 的顺序依赖语义

man/systemd.unit.xml · Before / After 的顺序依赖语义

官方文档v257

相关实现与比较#

4 个相关案例
Linux Kernel·lockdep 锁依赖检查v6.12

Linux lockdep 在加入新的锁依赖前,用 BFS 查找反向可达路径;若形成环,报告一条较短的依赖链,帮助定位潜在死锁。

附源码片段 · 并发与一致性
19 个匹配案例