登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

Linux cgroup v2 怎么冻结并恢复一组进程

来源:17golang原创

时间:2026-10-04 13:11:00 467浏览 收藏

Linux cgroup v2 冻结一组进程的最小做法是:先把这些进程迁入同一个非根 cgroup,再向该组的 cgroup.freeze 写入 1;恢复时写入 0。不过写入只是发出请求,真正完成要看 cgroup.events 中的 frozen 是否变成目标值。

要点速览
  • cgroup.freeze 只存在于非根 cgroup,允许值为 0 和 1。
  • 冻结会覆盖当前 cgroup 及其所有后代,适合按工作负载整体暂停。
  • 冻结与恢复可能需要时间,脚本不能只检查写操作是否成功。

官方文档:https://docs.kernel.org/admin-guide/cgroup-v2.html

先用最小配方完成一次冻结和恢复

下面假设系统把统一层级挂载在 /sys/fs/cgroup,并且当前用户对示例子树有写权限。生产环境通常应在 systemd 或容器运行时明确委派的子树里操作,不要随意改动系统服务已经管理的层级。

# 确认挂载点是 cgroup v2;预期文件系统类型为 cgroup2fs。
stat -fc %T /sys/fs/cgroup

# 在有写权限的委派子树中创建一个非根 cgroup。
CG=/sys/fs/cgroup/my-workload
mkdir "$CG"

# 每次写入一个 PID;写入任意线程 ID 会迁移该进程的全部线程。
echo "$PID_A" > "$CG/cgroup.procs"
echo "$PID_B" > "$CG/cgroup.procs"

# 请求冻结当前 cgroup 及全部后代。
echo 1 > "$CG/cgroup.freeze"

# 查看 frozen 字段;变为 1 才表示冻结已经完成。
grep '^frozen ' "$CG/cgroup.events"

# 请求恢复运行,并确认 frozen 最终回到 0。
echo 0 > "$CG/cgroup.freeze"
grep '^frozen ' "$CG/cgroup.events"

如果第一条命令没有显示 cgroup2fs,或者目标目录里没有 cgroup.freeze,先不要继续写文件。常见原因是系统仍在使用 cgroup v1、查看的是错误挂载点,或者当前目录就是根 cgroup。内核文档明确规定 cgroup.freeze 和带有 frozen 字段的 cgroup.events 都属于非根 cgroup 接口。

Linux cgroup v2 中进程归属、cgroup.freeze 控制与 cgroup.events 状态确认的静态关系图
图1:目标进程先通过 cgroup.procs 归入工作 cgroup,冻结请求写入 cgroup.freeze,完成状态由 cgroup.events 的 frozen 字段确认;这是静态关系图。

为什么写入 1 之后还要读取 cgroup.events

我第一次把这段操作写进维护脚本时,最容易漏掉的就是“冻结可能不是瞬间完成”。echo 1 成功只能证明内核接受了请求,不能证明组内所有进程此刻都已经停止。内核在冻结完成后才把 cgroup.events 的 frozen 更新为 1,并触发对应的文件变更通知。

因此自动化脚本应把状态确认做成有超时的等待,而不是固定睡眠几秒:

# 等待指定 frozen 状态,超时后返回失败,避免脚本无限阻塞。
wait_frozen() {
    target="$1" # 目标状态:冻结为 1,恢复为 0。
    tries=50     # 最多等待约 5 秒。
    while [ "$tries" -gt 0 ]; do
        # 只读取内核报告的 frozen 字段,不用进程表状态猜测。
        current=$(awk '$1 == "frozen" {print $2}' "$CG/cgroup.events")
        [ "$current" = "$target" ] && return 0
        sleep 0.1 # 给异步状态转换留出时间。
        tries=$((tries - 1))
    done
    return 1 # 上层据此报警或执行回退。
}

恢复也要同样确认。向 cgroup.freeze 写 0 后,只有观察到 frozen 0,才能把该工作负载视为已经恢复。这样做还能及时发现祖先 cgroup 仍处于冻结状态的情况。

冻结范围是整棵子树,不只是目录里的直接进程

向某个 cgroup 的 cgroup.freeze 写 1,会冻结该组及所有后代 cgroup 中的进程。它和逐个对 PID 发送 SIGSTOP 的思路不同:cgroup freezer 依据层级边界管理整组工作负载,后代层级也受父组约束。

祖先规则尤其重要。如果某个祖先 cgroup 仍被冻结,即使对子组自己的 cgroup.freeze 写 0,子组也不会真正恢复。排查“写了 0 但进程不运行”时,要沿层级向上检查,而不能只看当前文件的请求值。

Linux cgroup v2 委派子树中父子 cgroup 与进程冻结覆盖边界的静态结构图
图2:父 cgroup 的冻结覆盖直接进程和后代 cgroup;可写委派子树限制了普通操作者能够组织和迁移进程的范围,这是静态结构图。

进程迁移还有两个容易忽略的事实:

  • 每次向 cgroup.procs 写入只能迁移一个进程;多 PID 要逐个写。
  • 进程可以被显式移入或移出冻结组。移入冻结组会停止,移出后会继续运行;冻结与 fork() 竞态时也可能出现成员变化。

这意味着 freezer 适合暂停一棵已经组织好的工作负载,但它不是阻止管理员迁移进程的安全边界。若操作期间不允许成员漂移,调用方还需要在自己的调度或服务管理层串行化迁移动作。

把冻结与恢复封装成可复用脚本

下面的片段只操作一个已经存在、已经获授权的 cgroup。它不负责创建或删除系统层级,因此更适合嵌入运维脚本:

#!/bin/sh
# 遇到未定义变量或命令失败立即退出。
set -eu

CG="$1"      # 第一个参数是现有 cgroup 的绝对路径。
ACTION="$2"  # 第二个参数只能是 freeze 或 thaw。

# 确认核心接口存在,避免误操作普通目录或根 cgroup。
[ -f "$CG/cgroup.freeze" ]
[ -f "$CG/cgroup.events" ]

case "$ACTION" in
    freeze)
        value=1 # 请求冻结整棵 cgroup 子树。
        ;;
    thaw)
        value=0 # 请求恢复整棵 cgroup 子树。
        ;;
    *)
        # 参数错误时给出用法并终止。
        echo "用法: $0 CGROUP_PATH freeze|thaw" >&2
        exit 2
        ;;
esac

# 提交请求;写入成功并不等于状态转换已经完成。
echo "$value" > "$CG/cgroup.freeze"

# 在超时窗口内等待内核报告目标状态。
i=0
while [ "$i" -lt 50 ]; do
    current=$(awk '$1 == "frozen" {print $2}' "$CG/cgroup.events")
    [ "$current" = "$value" ] && exit 0
    sleep 0.1 # 每 100 毫秒复查一次。
    i=$((i + 1))
done

# 超时表明状态仍未到位,交给上层处理报警或回退。
echo "cgroup 状态切换超时: 期望 frozen=$value" >&2
exit 1

调用示例也保持简单:

# 冻结工作负载,并等待 frozen 变为 1。
./cgroup-freeze.sh /sys/fs/cgroup/my-workload freeze

# 恢复工作负载,并等待 frozen 变为 0。
./cgroup-freeze.sh /sys/fs/cgroup/my-workload thaw

权限、systemd 与委派边界

直接在 /sys/fs/cgroup 下创建目录通常需要较高权限,而且 systemd 管理的主机已经在维护自己的层级。更稳妥的做法是让服务管理器或容器运行时提供一个委派子树,再在该范围内创建子 cgroup、迁移进程和写入核心接口。

内核的委派模型不仅要求目标 cgroup.procs 可写;对非 root 用户迁移进程时,还要求操作者能够写源组与目标组共同祖先的 cgroup.procs。所以看到 Permission denied 时,不要只检查目标文件权限,还要检查迁移路径是否跨出了委派边界。

常见故障怎么判断

现象优先检查处理方向
没有 cgroup.freeze是否为 cgroup v2、是否位于根 cgroup找到统一层级并创建非根子组
写入时报 Permission denied目录、目标 cgroup.procs、共同祖先权限使用正确的委派子树
写 1 后 frozen 仍为 0是否仍在异步转换、是否等待超时读取 cgroup.events,不用固定 sleep 代替确认
写 0 后进程仍不运行祖先 cgroup 是否被冻结沿父层级检查并恢复祖先
部分 PID 没进组是否把多个 PID 一次写入逐个写入并复查 cgroup.procs

冻结前后的核对清单

  1. 确认目标位于 cgroup v2 的非根、可写委派子树。
  2. 逐个迁移 PID,再读取 cgroup.procs 核对成员。
  3. 写入 cgroup.freeze 后等待 cgroup.events 达到目标状态。
  4. 恢复失败时检查祖先层级和是否发生了进程迁移。
  5. 把等待逻辑设置超时,并让上层记录失败与回退动作。

小结:cgroup v2 冻结一组进程并不复杂,真正可靠的关键是把“写入请求”和“状态完成”分开:用 cgroup.procs 组织进程,用 cgroup.freeze 发出冻结或恢复请求,再以 cgroup.events 的 frozen 字段作为完成依据。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>