学习文档:进程冻结技术:深入探究 Linux 内核中的 cgroup freezer 子系统 - 魅族内核团队,魅族内核团队很流弊啊。或者说做过内核冻结的大佬都很流弊。现在还在大厂的且做过内核冻结的大佬,听说年薪100w+是很正常的。也说明内核专家确实是市场稀缺高端人才,真的特别佩服哈。

一、背景介绍
cgroup 最初由 Google 工程师 Paul Menage 和 Rohit Seth 在 2006 年提出,是一种细粒度资源控制的Linux内核机制。于 2007 年合并到 Linux 内核主线中。然而 Google 原生系统直到 Android 11 或更高版本上才支持 CACHE 应用的 CPU 冻结功能。当应用切换到后台并且没有其他活动时,系统会在一定时间内通过状态判断,将进程 ID 迁移到冻结的 cgroup 节点上,实现冻结 CACHE 应用。这项功能可以减少活跃缓存应用在后台存在时所消耗的 CPU 资源,从而达到节电的目的。当应用再次切换到前台时,系统会将该应用的进程解冻,以实现快速启动。
对于后台进程冻结,有两套方案,一是 cgroup freezer,二是内核信号 signal SIGSTOP 和 SIGCONT,国内很多手机厂商其实是早于 Android 做进程冻结方案的。而使用 cgroup freezer 方案更成熟更完善,接入后其实仅仅第一步,还有 binder 的 BINDER_FREEZE 冻结,framework层还有很多需要特殊场景,如后台下载、应用中使用桌面小组件、正在播放音频等等。
cgroup中的Freezer子系统可以用来暂停或恢复控制组中的进程,主要作用如下:
1.暂停进程:冻结的进程会被暂停,其所有线程的执行将被停止,包括应用程序的主线程以及任何后台线程。
2.资源释放:冻结进程占用的资源,例如CPU、内存资源会被释放。这些资源将被系统重新分配给其他需要执行的进程或系统服务
3.功耗节省:被冻结的进程不会在后台运行,因此系统在休眠期间不会被频繁唤醒,可以节省设备的电池消耗。
4.快速恢复:冻结的进程可以快速恢复其执行状态。当需要重新激活进程时,系统可以迅速将其恢复到之前的运行状态,而无需重新启动或加载应用程序。 冻结进程并不会终止进程的执行或销毁应用程序。冻结只是暂时挂起进程,以优化资源使用。一旦系统需要再次运行该进程(例如用户重新打开应用程序或系统需要其提供服务),它会被解冻并恢复运行。
进程冻结是Android系统中重要的资源管理策略,也是目前主流手机厂商常用的后台管控策略之一,它有助于提高系统性能,同时最大限度地节省设备的资源和电量消耗。下面我们针对 cgroup freezer 的底层实现,看 Linux 内核是如何支撑 Android 的墓碑机制功能的。
二、cgroup相关组件
- 检查cgroup 2 文件系统是否已经加载
cat /proc/filesystems | grep cgroup2

- 挂载
可以用如下命令挂载cgroup文件系统到d目录
XPLORE_1_WT:/ # mount -t cgroup2 none d XPLORE_1_WT:/ # mount | grep cgroupnone on /dev/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)none on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)none on /dev/cpuctl type cgroup (rw,nosuid,nodev,noexec,relatime,cpu)none on /dev/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset,noprefix,release_agent=/sbin/cpuset_release_agent)none on /dev/memcg type cgroup (rw,nosuid,nodev,noexec,relatime,memory)none on /sys/kernel/debug type cgroup2 (rw,relatime)
系统启动后,默认system已经将cgroup v2的文件系统挂载到/sys/fs/cgroup下

- cgroup的父子关系
初始状态下只有一个root cgroup根节点,所有进程都归属于这个cgroup,可以使用mkdir指令创建新的子cgroup。cpu、memory、freezer等控制资源是自顶向下(top-down)分配的,只有当一个 cgroup 从 parent 获得了某种资源,它才可以继续向下分发。这意味着所有非根"cgroup.subtree_control"文件只能包含在父级的"cgroup.subtree_control"文件中启用的控制器。只有在父级cgroup中启用了控制器时,子级cgroup才能启用控制器,如果一个或多个子级已经启用了某个控制器,则不能禁用该控制器。子孙cgroup数量有限,内核中使用cgroup.max.depth和cgroup.max.descendants来限制,关系图如下:

- 进程与cgroup的关系
cgroup.procs是cgroup与task进程绑定的接口,当读取该文件时,它会逐行列出属于该cgroup的所有进程的PID。将进程的PID写入到cgroup.procs中即可将目标PID进程绑定到该cgroup。进程与cgroup是多对多的关系,一个进程可以绑定到多个cgroup中,一个cgroup可以被多个进程绑定。在kernel中进程的数据结构task_struct与cgroup有关的是如下cgroups、cg_list两个成员:
kernel-6.6/include/linux/sched.h
#ifdef CONFIG_CGROUPS // 条件编译:仅在启用内核cgroup功能时包含此部分 /* * Control Group info protected by css_set_lock: * 指向进程所属的cgroup子系统状态集合(css_set), * 通过RCU(Read-Copy-Update)机制保护,确保多线程安全读取。 * css_set包含进程关联的所有子系统的资源控制状态(如CPU、内存等)。 */ struct css_set __rcu *cgroups; /* * cg_list protected by css_set_lock and tsk->alloc_lock: * 链表节点,用于将当前进程链接到所属css_set的进程列表中。 * 同一css_set的所有进程通过cg_list串联,形成环形链表。 * 需同时持有css_set_lock(保护cgroup全局状态)和alloc_lock(保护进程描述符)才能修改。 */ struct list_head cg_list;#endif
cgroups、cg_list成员涉及到了css_set、cgroup_subsys_state、cgroup等几个关键数据结构,下面来分析这几个数据结构
4.1 css_set数据结构
task_struct中的*cgroups指针指向了一个css_set结构,而css_set是用来存储与进程相关的cgroups信息,定义如下:
include/linux/cgroup-defs.h
/* * css_set 是内核中用于管理进程与 cgroup 关联的核心数据结构。 * 它通过集中存储一组 cgroup_subsys_state 对象,优化了进程的资源控制效率: * 1. 节省 task_struct 空间(避免每个任务重复存储子系统状态) * 2. 加速 fork()/exit() 操作(通过引用计数批量操作整个 cgroup 集合) */struct css_set { /* * 子系统状态数组:存储当前 css_set 关联的所有子系统的状态对象。 * 初始化后不可变(除 init_css_set 在启动时注册子系统外)。 * 每个数组元素对应一个子系统(如 cpu_cgroup、memory_cgroup 等)。 */ struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT]; /* * 引用计数:跟踪有多少个进程或任务组使用此 css_set。 * 通过 refcount_inc()/refcount_dec() 原子操作保证线程安全。 */ refcount_t refcount; /* * 域 cset 指针:用于支持 cgroup v2 的线程模式(threaded mode)。 * - 如果是域 cgroup,指向自身; * - 如果是线程化 cgroup,指向最近的域祖先的 css_set。 * 通过 dom_cset 可以访问域级别的资源统计和控制。 */ struct css_set *dom_cset; /* * 默认关联的 cgroup:表示此 css_set 所属的顶层 cgroup。 * 在非线程模式下,dfl_cgrp 与 subsys[] 中的 cgroup 一致; * 在线程模式下,可能指向不同的层级。 */ struct cgroup *dfl_cgrp; /* * 内部任务计数器:记录使用此 css_set 的进程数量。 * 由 css_set_lock 保护,避免并发修改导致计数错误。 */ int nr_tasks; /* * 任务链表:组织所有使用此 css_set 的进程。 * - tasks: 正常状态的任务列表 * - mg_tasks: 正在迁移的任务列表(进出此 cset) * - dying_tasks: 正在退出的任务列表 * 注意:mg_tasks 和 dying_tasks 在迁移或退出期间由 cgroup_mutex 保护。 */ struct list_head tasks; struct list_head mg_tasks; struct list_head dying_tasks; /* * 迭代器链表:记录所有正在遍历此 css_set 的 css_task_iter 对象。 * 用于安全地处理并发迭代操作。 */ struct list_head task_iters; /* * 扩展 cset 节点:用于默认层级(cgroup v2)的祖先关联。 * 当 subsys[ssid] 指向祖先 cgroup 的 css 时,通过 e_cset_node 链表 * 可以遍历所有关联到特定 cgroup 的 css_set。 */ struct list_head e_cset_node[CGROUP_SUBSYS_COUNT]; /* * 线程化 cset 链表:记录所有 dom_cset 指向此 css_set 的线程化 csets。 * threaded_csets_node 用于将自身链接到父 css_set 的 threaded_csets 链表。 */ struct list_head threaded_csets; struct list_head threaded_csets_node; /* * 哈希表节点:用于将 css_set 链接到全局哈希表中(通过 css_set_table)。 * 由 css_set_lock 保护,用于快速查找和去重。 */ struct hlist_node hlist; /* * cgroup 链接链表:通过 cgrp_cset_link 结构记录所有引用此 css_set 的 cgroup。 * 由 css_set_lock 保护,用于维护 cgroup 与 css_set 的多对多关系。 */ struct list_head cgrp_links; /* * 迁移相关链表节点:用于处理 cgroup 迁移操作。 * - mg_src_preload_node: 作为迁移源时的预加载链表 * - mg_dst_preload_node: 作为迁移目标时的预加载链表 * - mg_node: 通用迁移链表 * 由 cgroup_mutex 保护,确保迁移操作的原子性。 */ struct list_head mg_src_preload_node; struct list_head mg_dst_preload_node; struct list_head mg_node; /* * 迁移上下文:当此 css_set 作为迁移源时,以下字段生效: * - mg_src_cgrp: 源 cgroup * - mg_dst_cgrp: 目标 cgroup * - mg_dst_cset: 目标 css_set * 由 cgroup_mutex 保护,避免并发迁移导致状态不一致。 */ struct cgroup *mg_src_cgrp; struct cgroup *mg_dst_cgrp; struct css_set *mg_dst_cset; /* * 死亡标记:表示此 css_set 是否已被标记为死亡并正在释放资源。 * 迁移操作会忽略 dead 为 true 的 css_set。 */ bool dead; /* * RCU 回调头:用于安全地延迟释放 css_set 内存。 * 通过 call_rcu() 机制确保无锁读操作不会访问已释放的内存。 */ struct rcu_head rcu_head;};
多对多关系管理:
一个 css_set可以被多个进程共享(若它们的 cgroup 配置完全相同)
一个 cgroup 可以关联多个 css_set(通过 cgrp_links链表维护)
4.2 cgroup_subsys_state数据结构
每个子系统都有属于自己的资源控制统计信息结构,而且每个cgroup中都绑定一个这样的结构,这种资源控制统计信息结构就是通过 cgroup_subsys_state 结构体实现的,其定义如下:
/* * 系统维护的每个子系统/每个cgroup的状态。这是控制器(子系统)操作的基础结构单元。 * 标记为 "PI:" 的字段是公共且不可变的,可以直接访问而无需同步。 */struct cgroup_subsys_state { /* PI: 当前css关联的cgroup,表示此状态属于哪个cgroup */ struct cgroup *cgroup; /* PI: 当前css关联的子系统,指向子系统描述符(如cpu、memory等) */ struct cgroup_subsys *ss; /* * 引用计数器:通过css_[try]get()增加引用,css_put()减少引用。 * 使用percpu_ref实现高性能的原子操作。 */ struct percpu_ref refcnt; /* * 兄弟节点链表:通过sibling链接到父cgroup的children链表, * children链接当前css的子节点(形成树状结构)。 */ struct list_head sibling; struct list_head children; /* * 统计刷新节点:链接到cgrp->rstat_css_list, * 用于资源统计的批量更新(如内存使用量统计)。 */ struct list_head rstat_css_node; /* * PI: 子系统唯一ID。0未使用,根节点始终为1。 * 可通过css_from_id()查找对应的css。 */ int id; /* 标志位,用于表示css的状态(如CSS_ROOT表示根节点) */ unsigned int flags; /* * 单调递增的唯一序列号,用于对所有css定义全局顺序。 * 保证所有->children链表按->serial_nr升序排列, * 支持迭代的中断和恢复。 */ u64 serial_nr; /* * 在线计数器:由当前css及其子css递增。 * 确保父节点不会在子节点之前被下线(offline)。 */ atomic_t online_cnt; /* * 销毁工作项:当引用计数归零时, * 通过工作队列异步释放资源,避免阻塞上下文。 */ struct work_struct destroy_work; struct rcu_work destroy_rwork; /* RCU保护的销毁工作项 */ /* * PI: 父css指针。放置在结构体末尾以提高缓存局部性, * 与包含此结构的子类字段相邻(如mem_cgroup等)。 */ struct cgroup_subsys_state *parent;};
典型使用场景:
进程迁移:当进程在 cgroup 间移动时,通过 css_get()/css_put()更新引用计数。
资源统计:内存子系统通过 rstat_css_node汇总各 cgroup 的内存使用量
4.3 cgroup数据结构
cgroup主要用来控制进程组对各种资源的使用。
典型应用场景
容器资源限制:通过 subsys[]中的内存/CPU 子系统状态实现 Docker 容器资源配额。
进程冻结:freezer字段管理进程组冻结状态,用于容器暂停/恢复
struct cgroup { /* * 自引用css:当 ->ss 为 NULL 时指向当前 cgroup 自身。 * 用于统一处理 cgroup 的通用操作(如资源统计)。 */ struct cgroup_subsys_state self; /* 状态标志位,使用 unsigned long 类型以支持位操作(如设置/清除标记) */ unsigned long flags; /* * 当前 cgroup 在层级树中的深度: * - 根 cgroup 深度为 0,每向下一层深度加 1。 * - 结合 ancestors[] 可快速判断 cgroup 的继承关系。 */ int level; /* 允许的最大子树深度,防止层级过深导致性能问题 */ int max_depth; /* * 子树统计计数器(受 cgroup_mutex 和 css_set_lock 保护): * - nr_descendants: 存活的子 cgroup 数量 * - nr_dying_descendants: 被删除但仍有引用的子 cgroup 数量 * - max_descendants: 允许的最大子 cgroup 数量 */ int nr_descendants; int nr_dying_descendants; int max_descendants; /* * 任务分布统计: * - nr_populated_csets: 关联的 css_set 数量(>0 表示有任务) * - nr_populated_domain_children: 非空域子 cgroup 数量 * - nr_populated_threaded_children: 非空线程化子 cgroup 数量 * - nr_threaded_children: 存活的线程化子 cgroup 数量 */ int nr_populated_csets; int nr_populated_domain_children; int nr_populated_threaded_children; int nr_threaded_children; /* 内核文件系统相关 */ struct kernfs_node *kn; /* 对应 kernfs 节点 */ struct cgroup_file procs_file; /* "cgroup.procs" 文件句柄 */ struct cgroup_file events_file; /* "cgroup.events" 文件句柄 */ struct cgroup_file psi_files[NR_PSI_RESOURCES]; /* 压力状态文件 */ /* * 子树控制位图(16位无符号整数): * - subtree_control: 用户配置的启用子系统 * - subtree_ss_mask: 实际生效的子系统(可能包含隐式启用) * - old_*: 用于临时保存配置变更前的状态 */ u16 subtree_control; u16 subtree_ss_mask; u16 old_subtree_control; u16 old_subtree_ss_mask; /* * 子系统状态数组: * 每个槽位存储对应子系统的 cgroup_subsys_state 指针, * 使用 RCU 机制保护并发访问。 */ struct cgroup_subsys_state __rcu *subsys[CGROUP_SUBSYS_COUNT]; /* 指向所属的 cgroup 层级根 */ struct cgroup_root *root; /* * 关联的 css_set 链表: * 通过 cgrp_cset_link 结构链接所有包含此 cgroup 任务的 css_set, * 受 css_set_lock 保护。 */ struct list_head cset_links; /* * 扩展 css_set 链表: * 对于默认层级,当某些子系统被禁用时,任务可能指向祖先的 css。 * 此数组记录所有通过此类方式引用当前 cgroup 的 css_set。 */ struct list_head e_csets[CGROUP_SUBSYS_COUNT]; /* * 域 cgroup 指针: * - 非线程化模式下指向自身 * - 线程化模式下指向最近的域祖先 * 域级别的资源消耗(非任务专属)会统计到 dom_cgrp。 */ struct cgroup *dom_cgrp; struct cgroup *old_dom_cgrp; /* 线程化启用时的临时存储 */ /* 每 CPU 资源统计 */ struct cgroup_rstat_cpu __percpu *rstat_cpu; struct list_head rstat_css_list; /* 需要统计的 css 链表 */ /* 缓存行填充,避免伪共享 */ CACHELINE_PADDING(_pad_); /* * 资源统计刷新链表: * 单向链表,供 cgroup_rstat_flush_locked() 批量更新统计, * 受 cgroup_rstat_lock 保护。 */ struct cgroup *rstat_flush_next; /* 基础资源统计(CPU/内存等) */ struct cgroup_base_stat last_bstat; /* 上次统计值 */ struct cgroup_base_stat bstat; /* 当前统计值 */ struct prev_cputime prev_cputime; /* CPU 时间记录 */ /* PID 列表(按命名空间隔离,按需创建) */ struct list_head pidlists; struct mutex pidlist_mutex; /* PID 列表操作锁 */ /* 等待离线队列(用于 css 下线同步) */ wait_queue_head_t offline_waitq; /* 释放代理工作项(异步执行 cgroup 清理) */ struct work_struct release_agent_work; /* 压力阻塞统计 */ struct psi_group *psi; /* eBPF 程序挂钩 */ struct cgroup_bpf bpf; /* 阻塞计数(用于判断 cgroup 是否发生 I/O 阻塞) */ atomic_t congestion_count; /* freezer 状态(用于进程冻结/解冻) */ struct cgroup_freezer_state freezer;#ifdef CONFIG_BPF_SYSCALL /* eBPF 本地存储 */ struct bpf_local_storage __rcu *bpf_cgrp_storage;#endif /* Android 向后兼容保留字段 */ ANDROID_BACKPORT_RESERVE(1); /* 祖先数组(包含自身,动态分配) */ struct cgroup *ancestors[];};
从task到其所属的cgroup之间是没有直接指针相连接的,但是task可以通过一个媒介来获取其所属的cgroup,这个媒介就是css_set和cgroup_subsys_state。通过task_struct -> cgroups -> subsys[ssid] ->cgroup即可访问到管理对应子系统的cgroup。之所以这么设计是因为获取子系统状态的操作预计会频繁发生,而且是在性能关键代码中。然而需要一个task实际的cgroup来执行的操作(尤其是task在cgroups之间迁移的操作)则并没有那么常见。task_struct中的cg_list则是用来连接使用同一个css_set的task的链表,css_set通过tasks来遍历访问此链表。

通俗来说,可以把整个设计想象成一个"班级管理系统",其中:
1.学生(task)和班级(cgroup)的关系
每个学生(进程)不会直接记住自己属于哪个班级(cgroup),而是通过一个"学生证"(css_set)来间接关联。学生证上记录了该学生在不同科目(子系统)的"科目成绩单"(cgroup_subsys_state),而每张成绩单会标明对应的班级(如数学班、英语班等)。这样,学生只要出示学生证,就能快速查到自己在各科的班级归属。
2.为什么这么设计?
•高频操作优化:学生每天要频繁查看自己的科目成绩(如CPU、内存使用情况),但很少转班(迁移cgroup)。通过学生证直接查成绩单(task->cgroups->subsys[ssid])比每次问班主任(直接查cgroup)更快。
•资源共享:多个学生可能共享同一套科目班级配置(如都参加数学A班和英语B班),他们的学生证(css_set)会通过cg_list串成链表,像班级花名册一样方便批量管理。
3.转班(迁移)的场景
当学生要转班时(比如从数学A班转到数学C班),需要更新学生证上的数学成绩单(修改subsys[ssid]指向新班级),同时从旧班级的花名册(cg_list)移除,并加入新班级的花名册。这类操作虽然复杂,但发生频率低,对性能影响小。
总结:
就像学生证既方便日常查成绩,又支持偶尔转班一样,css_set和cgroup_subsys_state的设计平衡了高频查询和低频迁移的需求,同时避免了每个进程直接维护大量cgroup指针的开销
- freeze子系统使用
做一个小实验:把微信冻结的实验。先top下,找到微信的pid和uid,例如pid为5221,uid为10283

PID为5212的进程状态为R【ps -Ae | grep 5212】

/sys/fs/cgroup/uid_10283/pid_5212 # echo 1 > cgroup.freeze

这个时候PID为5212的进程已经被我们的cgroup进程组给冻结掉,可以查看进程状态已经从R 切换到S

观察上面的WCHAN可以看到冻结的进程是阻塞在内核的do_freezer_trap函数中 , do_freezer_trap是cgroup freezer中最核心的函数.
后台查看5212被冻结之后,对应的进程已经不在后台运行,CPU的整体loading也降低下来。
三、cgroup freezer子系统的实现
- cgroup冻结整体流程
内核通过了一个比较巧妙的方式实现了冻结流程。它首先为该进程设置了一个信号pending标志TIF_SIGPENDING,但并不向该进程发送实际的信号,然后通过ipi唤醒该进程执行。由于ipi会进行进程内核的中断处理流程,当其处理完成后,会调用ret_to_user函数返回用户态,而该函数会调用信号处理函数检查是否有pending的中断需要处理,由于先前已经设置了信号的pending标志,因此会执行信号处理流程。在信号处理流程中检查进程冻结相关的全局变量是否设置,然后调用__set_current_state将task 设置为interrupt状态 将task 挂起,执行schedule() 让出cpu,进行上下文切换。

想象你是一个正在打游戏的玩家(用户进程),而游戏机是CPU。突然系统需要做一次大扫除(系统休眠/冻结),但直接关掉游戏机会导致存档损坏(内核死锁)。于是管理员(内核)用了以下巧妙方法:
1.设置提醒标志
管理员在你游戏手柄上悄悄贴了个"请暂停"的便利贴(设置TIF_SIGPENDING标志),但不会发出声音打扰你(不发送真实信号)。这时候你还在专注打游戏(内核态执行)。
2.温柔提醒
管理员轻拍你肩膀说"看看手柄"(通过IPI中断唤醒)。你暂停游戏去看手柄(中断处理流程),发现便利贴后决定先存档(准备返回用户态)。
3.安全暂停
在存档时(ret_to_user返回用户态前),系统检查便利贴并发现大扫除通知(冻结全局变量)。于是你主动:
•把游戏角色停在安全点(TASK_INTERRUPTIBLE状态)
•放下手柄(schedule()让出CPU)
•等大扫除结束再继续(冻结完成)
- 设置freezer冻结与解冻
前面实验通过对cgroup目录cgroup.freeze值的修改,来完成了freezer对task限制的设置。将cgroup.freeze值置为1则该cgroup里的进程会全被freeze,置为0则会被unfreeze。这个小节来看看这个设置过程。当用户读写cgroup.freeze这个文件的时候,cgroup中调用的是cgroup_freeze_write函数,内核数据结构如下
kernel/cgroup/cgroup.c
/* * cgroup 默认层级(dfl_cgrp)的核心接口文件定义 * 这些文件出现在每个cgroup目录中,用于控制进程分组行为 */static struct cftype cgroup_base_files[] = { /* * cgroup.procs 文件: * - 用于查看和修改属于该cgroup的进程 * - 可被命名空间委托(CFTYPE_NS_DELEGATABLE) * - 关联到cgroup结构体中的procs_file字段 * - 提供完整的进程管理功能:显示、遍历、迁移等 */ { .name = "cgroup.procs", .flags = CFTYPE_NS_DELEGATABLE, .file_offset = offsetof(struct cgroup, procs_file), .release = cgroup_procs_release, // 文件释放时的清理函数 .seq_start = cgroup_procs_start, // 开始遍历进程列表 .seq_next = cgroup_procs_next, // 获取下一个进程 .seq_show = cgroup_procs_show, // 显示当前进程PID .write = cgroup_procs_write, // 写入PID来迁移进程 }, /* 其他cgroup接口文件定义... */ /* * cgroup.freeze 文件: * - 用于冻结/解冻cgroup中的所有进程 * - 不能在根cgroup使用(CFTYPE_NOT_ON_ROOT) * - 提供冻结状态查看和设置功能 */ { .name = "cgroup.freeze", .flags = CFTYPE_NOT_ON_ROOT, .seq_show = cgroup_freeze_show, // 显示当前冻结状态 .write = cgroup_freeze_write, // 写入1冻结/0解冻 }, /* 其他cgroup接口文件定义... */};
文件读写处理函数cgroup_freeze_write函数调用cgroup_freeze_task将cgroup中所有task的jobctl位掩码置位为JOBCTL_TRAP_FREEZE,然后将要冻结的task设置为TIF_SIGPENDIN状态,如果是解冻这里会将task中的jobctl位掩码JOBCTL_TRAP_FREEZE清除,然后执行wake_up_process将task唤醒。调用链路如下:
vfs_write |->kernfs_fop_write_iter |->cgroup_file_write |->cgroup_freeze_write |->cgroup_kn_lock_live //获得文件所在的目录的cgroup实体 |->cgroup_freeze |->css_for_each_descendant_pre //循环体里对当前目录以及每个子孙目录所代表cgroup实体执行cgroup_do_freeze函数 |->cgroup_do_freeze |->set_bit //将cgroup及子孙cgroup设为freeze状态 |->cgroup_freeze_task |->signal_wake_up |->signal_wake_up_state |->wake_up_state |->try_to_wake_up |->kick_process //使进程陷入内核态,为返回用户态处理冻结信号做准备 |->wake_up_process
这里分析几个关键的函数cgroup_freeze、cgroup_do_freeze、cgroup_freeze_task。cgroup_freeze_write会调用到cgroup_kn_lock_live获取要冻结目录的cgroup实体,然后再调到cgroup_freeze中将父cgroup的冻结状态传给各子孙cgroup, cgroup_freeze定义在kernel/cgroup/freezer.c中
/* * Freeze or unfreeze the task by setting or clearing the JOBCTL_TRAP_FREEZE * jobctl bit. */static void cgroup_freeze_task(struct task_struct *task, bool freeze){ unsigned long flags; bool wake = true; /* * 检查任务是否即将退出,避免对正在退出的任务进行操作 * lock_task_sighand 会获取任务的 sighand 锁,防止并发修改信号处理结构 */ if (!lock_task_sighand(task, &flags)) return; /* * 调用 vendor hook,允许厂商自定义是否唤醒任务 * 主要用于 Android 等系统扩展冻结行为 */ trace_android_vh_freeze_whether_wake(task, &wake); if (freeze) { /* * 设置 JOBCTL_TRAP_FREEZE 标志位 * 该标志会触发任务在返回用户态时进入冻结流程 */ task->jobctl |= JOBCTL_TRAP_FREEZE; /* * 唤醒任务处理冻结信号(若未被 vendor hook 禁用) * signal_wake_up 会设置 TIF_SIGPENDING 标志但不发送实际信号 */ if (wake) signal_wake_up(task, false); } else { /* 清除冻结标志并唤醒任务 */ task->jobctl &= ~JOBCTL_TRAP_FREEZE; if (wake) wake_up_process(task); // 直接唤醒任务解除冻结 } /* 释放 sighand 锁 */ unlock_task_sighand(task, &flags);}
cgroup_freeze中通过css_for_each_descendant_pre循环将父cgroup的freeze状态传递到各子孙cgroup中,父cgroup被freeze或者unfreeze,其目录下的子cgroup也会被freeze或者unfreeze,执行动作是在函数cgroup_do_freeze中,cgroup_do_freeze会循环遍历cgroup中的task执行cgroup_freeze_task函数做下一步的冻结/解冻操作。如果遇到内核线程则直接跳出该次循环。注意在信号的设计机制里内核线程不会收到信号,信号是针对用户线程或者进程的一种异步机制。所以在cgroup的冻结流程里没有内核线程的处理,但在suspend的冻结中有针对内核线程的处理,有兴趣的可以去看看。这里只讲cgroup中的冻结。cgroup_do_freeze实现如下:
/* * Freeze or unfreeze all tasks in the given cgroup. * 冻结或解冻指定cgroup中的所有任务 * @cgrp: 目标控制组 * @freeze: true表示冻结,false表示解冻 */static void cgroup_do_freeze(struct cgroup *cgrp, bool freeze){ struct css_task_iter it; struct task_struct *task; /* 确保调用者已持有cgroup_mutex锁 */ lockdep_assert_held(&cgroup_mutex); /* * 原子化更新cgroup的冻结标志位 * CGRP_FREEZE标志用于跟踪整个cgroup树的冻结状态传播 */ spin_lock_irq(&css_set_lock); if (freeze) set_bit(CGRP_FREEZE, &cgrp->flags); // 设置冻结标志 else clear_bit(CGRP_FREEZE, &cgrp->flags); // 清除冻结标志 spin_unlock_irq(&css_set_lock); /* 记录冻结/解冻跟踪事件(用于调试) */ if (freeze) TRACE_CGROUP_PATH(freeze, cgrp); else TRACE_CGROUP_PATH(unfreeze, cgrp); /* * 遍历cgroup中的所有任务 * css_task_iter_start初始化迭代器,0表示从第一个任务开始 */ css_task_iter_start(&cgrp->self, 0, &it); while ((task = css_task_iter_next(&it))) { /* * 跳过内核线程(内核线程不支持冻结) * PF_KTHREAD标志标识内核线程 */ if (task->flags & PF_KTHREAD) continue; /* 对每个用户态任务执行冻结/解冻操作 */ cgroup_freeze_task(task, freeze); } css_task_iter_end(&it); // 释放迭代器资源 /* * 检查并更新cgroup的冻结状态: * 当所有后代cgroup都已冻结(nr_descendants == nr_frozen_descendants)时, * 调用cgroup_update_frozen更新当前cgroup的冻结状态 */ spin_lock_irq(&css_set_lock); if (cgrp->nr_descendants == cgrp->freezer.nr_frozen_descendants) cgroup_update_frozen(cgrp); // 同步cgroup的冻结状态 spin_unlock_irq(&css_set_lock);}
signal_wake_up会调用signal_wake_up_state先为进程设置TIF_SIGPENDING标志,表明该进程有延迟的信号要等待处理。然后再调用 wake_up_state()唤醒目标进程,如果目标进程在其他的CPU上运行,wake_up_state()将返回0,此时调用 kick_process()向该CPU发送一个处理器核间中断。当中断返回前时,直接调用do_notify_resume()处理该进程的信号。signal_wake_up_state函数实现如下:
/* * Freeze or unfreeze the task by setting or clearing the JOBCTL_TRAP_FREEZE * jobctl bit. * 通过设置/清除JOBCTL_TRAP_FREEZE标志位来冻结或解冻任务 * @task: 目标task_struct结构体指针 * @freeze: true表示冻结,false表示解冻 */static void cgroup_freeze_task(struct task_struct *task, bool freeze){ unsigned long flags; bool wake = true; // 默认需要唤醒任务处理冻结/解冻 /* * 如果任务正在退出(无法获取sighand锁),则放弃操作 * lock_task_sighand会获取任务的信号处理锁,防止并发修改 */ if (!lock_task_sighand(task, &flags)) return; /* * Android扩展点:允许厂商自定义是否唤醒任务 * 例如后台下载/音频播放等场景可能需要特殊处理[1,6](@ref) */ trace_android_vh_freeze_whether_wake(task, &wake); if (freeze) { /* * 设置冻结标志位,任务将在返回用户态时进入冻结状态 * 采用延迟冻结机制避免内核态持锁时冻结导致的死锁[2](@ref) */ task->jobctl |= JOBCTL_TRAP_FREEZE; if (wake) /* * 通过伪信号唤醒任务(设置TIF_SIGPENDING但不发送真实信号) * 任务会在ret_to_user路径中处理冻结请求[2,6](@ref) */ signal_wake_up(task, false); } else { /* 清除冻结标志并直接唤醒任务 */ task->jobctl &= ~JOBCTL_TRAP_FREEZE; if (wake) wake_up_process(task); // 直接唤醒任务解除冻结状态 } /* 释放信号处理锁 */ unlock_task_sighand(task, &flags);}
根据signal_wake_up_state函数的代码逻辑,如果目标进程不在runqueue上,则wake_up_state函数会将其放在runqueue上并返回true;如果进程已经处于runqueue上了,则返回false,才会执行kick_process。下面我们来看看kick_process函数的实现:
kernel/sched/core.c
/* * Notify a process about a new pending signal and wake it up if needed. * 通知进程有新信号待处理,并根据需要唤醒进程 * * @t: 目标进程的task_struct指针 * @state: 唤醒状态掩码(如TASK_WAKEKILL或__TASK_TRACED) * * 注意事项: * 1. 必须在持有t->sighand->siglock自旋锁且本地中断已关闭的上下文中调用 * 2. 依赖调用前的spin_lock保证中断安全(见函数头注释) * 3. 不设置need_resched标志,因为信号处理通过->blocked机制传递 */void signal_wake_up_state(struct task_struct *t, unsigned int state){ /* 验证调用环境是否持有正确的锁 */ lockdep_assert_held(&t->sighand->siglock); /* * 设置线程的TIF_SIGPENDING标志,表示有信号待处理 * 该标志会在进程返回用户态时触发信号处理流程 */ set_tsk_thread_flag(t, TIF_SIGPENDING); /* * 唤醒策略说明: * 1. TASK_WAKEKILL标志会唤醒处于stopped/traced/killable状态的进程 * 2. 不直接检查t->state是因为存在竞态条件(进程可能正在其他CPU上进入stopped状态) * 3. 组合使用state和TASK_INTERRUPTIBLE确保进程能正确处理致命信号 * * 如果wake_up_state失败(进程已在运行队列),且进程在其他CPU上运行, * 则通过kick_process发送处理器间中断强制重新调度 */ if (!wake_up_state(t, state | TASK_INTERRUPTIBLE)) kick_process(t);}
函数的注释已经写得很清楚,kick_process的目的就是让进程陷入内核态。而smp_send_reschedule本质就是给进程所在的核发个IPI中断,从而导致正在运行的进程被打断陷入内核态。到这里cgroup这边freeze的前置工作已经做完,所有即将进入freeze的task已经被kernel置位为JOBCTL_TRAP_FREEZE和处于TIF_SIGPENDING的状态,真正执行进程冻结挂起的操作是放在signal的信号处理这边来执行。
- 冻结信号处理
信号真正得到执行的时机是进程执行完异常/中断/系统调用,从内核态返回到用户态的时刻,所以永远不要指望你所发送的信号能像硬件中断那般随时处理,进程信号处理只是异步通信机制,没有像真正的硬件中断那样能随时改变cpu的执行流。正常的用户进程是会频繁的在用户态和内核态之间切换的 (这种切换包括:系统调用,缺页异常,系统中断..),所以信号能很快得到执行。
前面小节已经讲过cgroup组中要被freeze的task已经将其_TIF_SIGPENDING置位。进程的_TIF_SIGPENDING置位,表明该进程有pending信号需要处理。因此会执行信号处理流程。
信号处理过程中会检查task中的freeze标志位已设置,故进程将执行关键冻结函数do_freeze_trap。调用链路如下:
exit_to_user_modeprepare_exit_to_user_modedo_notify_resume |->do_signal |->get_signal |->do_freezer_trap |->__set_current_state //进程即将进入冻结休眠,将进程设置为TASK_INTERRUPTIBLE,可以被signal唤醒 |->clear_thread_flag |->cgroup_enter_frozen |->cgroup_update_frozen |->cgroup_propagate_frozen |->freezable_schedule |->schedule |->__schedule |->deactivate_task |->dequeue_task
调用栈打印:

这里分析几个关键函数do_notify_resume、get_signal、do_freezer_trap、freezable_shedule。函数do_notify_resume定义在arch/arm64/kernel/signal.c中,该函数检查如果当前任务设置了标志位_TIF_SIGPENDING,则调用do_signal()处理信号,代码如下:
/* * do_notify_resume - 内核在返回用户空间前的信号和事件处理入口 * @regs: 保存用户态寄存器现场的结构体指针,用于信号处理时恢复上下文 * @thread_flags: 线程标志位集合,指示需要处理的事件类型 * * 功能说明: * 1. 该函数是内核从系统调用/中断返回用户空间前的统一处理点,负责检查并处理待处理的信号(_TIF_SIGPENDING) * 和其他需要通知的事件(_TIF_NOTIFY_SIGNAL) * 2. 通过thread_flags判断处理类型,避免不必要的信号检查开销 * 3. 实际信号处理由do_signal()完成,包括用户态信号处理函数的栈帧构建和触发执行 * * 调用上下文: * - 必须在内核态调用,通常在ret_to_user路径中(如arch/arm/kernel/entry-armv.S) * - 调用时需保证regs完整保存用户态寄存器状态(通过pt_regs结构) * - thread_flags来自当前线程的thread_info结构,由signal_wake_up()等函数设置 * * 关键设计: * - _TIF_SIGPENDING标志由signal_wake_up()设置,表示有待处理信号 * - _TIF_NOTIFY_SIGNAL用于类似io_uring的异步事件通知(非传统信号) * - 信号处理延迟到返回用户态前执行,避免内核关键路径中的信号处理复杂性 */void do_notify_resume(struct pt_regs *regs, unsigned long thread_flags){ ... /* * 检查线程标志位,组合判断信号和事件通知: * - _TIF_SIGPENDING: 传统信号(如SIGINT/SIGTERM) * - _TIF_NOTIFY_SIGNAL: 内核子系统事件(如io_uring完成通知) * 两者共用相同的处理路径,但实际处理逻辑在do_signal()内有区分 */ if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL)) do_signal(regs); // 进入实际信号处理流程 ...}/* * 相关机制说明: * 1. 信号传递流程: * signal_wake_up() -> set TIF_SIGPENDING -> schedule() -> ret_to_user -> do_notify_resume() -> do_signal() * 2. 安全性保障: * - 通过pt_regs严格隔离用户态/内核态寄存器状态 * - 信号处理函数执行前会构建独立的用户态栈帧 * 3. 性能优化: * - 仅当标志位被设置时才调用do_signal(),减少无信号时的开销 * - 信号处理延迟到非关键路径(返回用户态前) */
do_signal中调用到get_signal函数从线程私有的pending链表或者线程组共享的pending链表中,找到pending信号,如果需要投递到用户态去执行,返回1。如果没有需要投递到用户态去执行的pending信号,返回0。如果遇到需要kernel处理的信号,在该函数内部就会消化掉。get_signal实现如下:
get_signal - 内核信号处理的核心函数,负责从进程的待处理信号队列中获取下一个需要处理的信号 * @ksig: 用于返回获取到的信号信息(包括信号编号、处理动作等) * * 返回值: * true - 成功获取到一个需要处理的信号 * false - 没有待处理的信号 * * 关键处理流程: * 1. 检查并处理进程状态变化通知(如CLD_CONTINUED/CLD_STOPPED) * 2. 从同步信号队列或常规队列中获取信号 * 3. 根据信号处理动作(忽略/默认/自定义)决定处理方式 * 4. 对致命信号执行终止进程或coredump操作 */bool get_signal(struct ksignal *ksig){ /* 获取当前进程的信号处理结构 */ struct sighand_struct *sighand = current->sighand; struct signal_struct *signal = current->signal; int signr; /* 清理通知类信号标志 */ clear_notify_signal(); /* 处理挂起的task_work(避免在信号处理过程中被中断) */ if (unlikely(task_work_pending(current))) task_work_run(); /* 快速检查是否有待处理信号(无锁优化) */ if (!task_sigpending(current)) return false; /* uprobe相关信号拦截检查 */ if (unlikely(uprobe_deny_signal())) return false; /* 检查进程冻结状态(防止返回用户态时进程被冻结) */ try_to_freeze();relock: /* 获取信号处理锁(禁用本地中断) */ spin_lock_irq(&sighand->siglock); /* * 处理子进程状态变化通知(SIGCONT/SIGSTOP等) * 当线程从停止状态恢复时,需要通知父进程 */ if (unlikely(signal->flags & SIGNAL_CLD_MASK)) { int why; /* 确定通知类型:继续运行或停止 */ if (signal->flags & SIGNAL_CLD_CONTINUED) why = CLD_CONTINUED; else why = CLD_STOPPED; signal->flags &= ~SIGNAL_CLD_MASK; spin_unlock_irq(&sighand->siglock); /* * 通知父进程状态变化: * 1. 对于普通进程,通过wait(2)通知父进程 * 2. 对于被ptrace跟踪的进程,额外通知跟踪者 */ read_lock(&tasklist_lock); do_notify_parent_cldstop(current, false, why); /* 处理被ptrace跟踪的情况 */ if (ptrace_reparented(current->group_leader)) do_notify_parent_cldstop(current->group_leader, true, why); read_unlock(&tasklist_lock); goto relock; // 重新获取锁并继续信号处理 } /* 主信号处理循环 */ for (;;) { struct k_sigaction *ka; enum pid_type type; /* 检查进程是否已被标记为终止状态 */ if ((signal->flags & SIGNAL_GROUP_EXIT) || signal->group_exec_task) { clear_siginfo(&ksig->info); ksig->info.si_signo = signr = SIGKILL; sigdelset(¤t->pending.signal, SIGKILL); trace_signal_deliver(SIGKILL, SEND_SIG_NOINFO, &sighand->action[SIGKILL - 1]); recalc_sigpending(); goto fatal; // 跳转到终止处理 } /* 处理挂起的任务停止请求 */ if (unlikely(current->jobctl & JOBCTL_STOP_PENDING) && do_signal_stop(0)) goto relock; /* 处理调试陷阱和冻结陷阱 */ if (unlikely(current->jobctl & (JOBCTL_TRAP_MASK | JOBCTL_TRAP_FREEZE))) { if (current->jobctl & JOBCTL_TRAP_MASK) { do_jobctl_trap(); spin_unlock_irq(&sighand->siglock); } else if (current->jobctl & JOBCTL_TRAP_FREEZE) do_freezer_trap(); goto relock; } /* 处理进程解冻状态 */ if (unlikely(cgroup_task_frozen(current))) { spin_unlock_irq(&sighand->siglock); cgroup_leave_frozen(false); goto relock; } /* * 信号获取优先级: * 1. 先获取同步信号(如SIGSEGV等由CPU异常触发的信号) * 2. 再从常规队列获取其他信号 */ type = PIDTYPE_PID; signr = dequeue_synchronous_signal(&ksig->info); if (!signr) signr = dequeue_signal(current, ¤t->blocked, &ksig->info, &type); if (!signr) break; /* 没有待处理信号,返回false */ /* 处理被ptrace跟踪的进程信号 */ if (unlikely(current->ptrace) && (signr != SIGKILL) && !(sighand->action[signr -1].sa.sa_flags & SA_IMMUTABLE)) { signr = ptrace_signal(signr, &ksig->info, type); if (!signr) continue; } /* 获取信号处理动作 */ ka = &sighand->action[signr-1]; trace_signal_deliver(signr, &ksig->info, ka); /* 处理忽略信号的情况 */ if (ka->sa.sa_handler == SIG_IGN) continue; /* 处理自定义信号处理函数 */ if (ka->sa.sa_handler != SIG_DFL) { ksig->ka = *ka; /* 处理一次性信号 */ if (ka->sa.sa_flags & SA_ONESHOT) ka->sa.sa_handler = SIG_DFL; break; // 返回给上层处理(do_signal会调用handle_signal) } /* * 以下是内核默认信号处理逻辑 */ /* 处理内核默认忽略的信号(如SIGWINCH) */ if (sig_kernel_ignore(signr)) continue; /* 处理不可杀进程的特殊情况 */ if (unlikely(signal->flags & SIGNAL_UNKILLABLE) && !sig_kernel_only(signr)) continue; /* 处理停止类信号(SIGSTOP/SIGTSTP等) */ if (sig_kernel_stop(signr)) { /* SIGSTOP需要特殊处理(不能被拦截) */ if (signr != SIGSTOP) { spin_unlock_irq(&sighand->siglock); /* 检查进程组是否已成为孤儿 */ if (is_current_pgrp_orphaned()) goto relock; spin_lock_irq(&sighand->siglock); } /* 执行停止操作 */ if (likely(do_signal_stop(ksig->info.si_signo))) { /* do_signal_stop会释放锁,需要重新获取 */ goto relock; } continue; }fatal: /* 处理致命信号 */ spin_unlock_irq(&sighand->siglock); if (unlikely(cgroup_task_frozen(current))) cgroup_leave_frozen(true); /* 标记进程已被信号终止 */ current->flags |= PF_SIGNALED; /* 处理需要coredump的信号(如SIGSEGV) */ if (sig_kernel_coredump(signr)) { if (print_fatal_signals) print_fatal_signal(ksig->info.si_signo); proc_coredump_connector(current); do_coredump(&ksig->info); // 生成核心转储 } /* 用户工作线程特殊处理 */ if (current->flags & PF_USER_WORKER) goto out; /* 执行进程退出(对于SIGKILL等信号) */ do_group_exit(ksig->info.si_signo); } /* 清理并返回 */ spin_unlock_irq(&sighand->siglock);out: ksig->sig = signr; /* 处理地址标签隐藏(安全特性) */ if (!(ksig->ka.sa.sa_flags & SA_EXPOSE_TAGBITS)) hide_si_addr_tag_bits(ksig); return ksig->sig > 0;}
在信号处理流程中检查task中的jobctl标志位是否被设置成JOBCTL_TRAP_FREEZE,条件成立则执行do_freezer_trap开始走进程挂起流程,do_freezer_trap实现如下:
/** * do_freezer_trap - 处理进程冻结陷阱 * * 功能说明: * 1. 将当前进程置于冻结状态(除非进程即将退出) * 2. 清除JOBCTL_TRAP_FREEZE标志位 * 3. 通过cgroup_enter_frozen()进入冻结状态并调度 * * 上下文要求: * - 必须持有current->sighand->siglock自旋锁(函数返回前会释放) * - 必须在进程返回用户空间前调用(通常由信号处理路径触发) * * 设计要点: * - 通过TASK_FREEZABLE状态组合实现安全冻结(避免内核态死锁) * - 与cgroup freezer子系统深度集成,支持进程组粒度的冻结[4,8](@ref) */static void do_freezer_trap(void) __releases(¤t->sighand->siglock){ /* * 检查是否存在除JOBCTL_TRAP_FREEZE外的其他陷阱标志: * - 如果有其他待处理陷阱(如JOBCTL_TRAP_MASK),则放弃本次冻结 * - 确保不会干扰其他更重要的陷阱处理(如ptrace调试陷阱) */ if ((current->jobctl & (JOBCTL_PENDING_MASK | JOBCTL_TRAP_FREEZE)) != JOBCTL_TRAP_FREEZE) { spin_unlock_irq(¤t->sighand->siglock); return; } /* * 安全冻结准备阶段: * 1. 设置TASK_INTERRUPTIBLE|TASK_FREEZABLE状态(可被信号和冻结唤醒) * 2. 清除TIF_SIGPENDING标志避免立即被信号唤醒 * 3. 释放锁后通过cgroup_enter_frozen()进入冻结状态 * * 注:TASK_FREEZABLE是专为冻结设计的特殊状态,允许进程在冻结期间保持可中断[3,7](@ref) */ __set_current_state(TASK_INTERRUPTIBLE|TASK_FREEZABLE); clear_thread_flag(TIF_SIGPENDING); spin_unlock_irq(¤t->sighand->siglock); /* 正式进入冻结状态(关联cgroup freezer子系统) */ cgroup_enter_frozen(); /* * 主动让出CPU,进程将在此处挂起直到被解冻 * 解冻时会从此处恢复执行[5,7](@ref) */ schedule(); /* * 解冻后处理: * 1. 清理任务工作通知标志(TIF_NOTIFY_SIGNAL) * 2. 执行延迟的task_work(如io_uring等异步机制可能设置) * * 注:调用者会负责检查是否需要重新进入冻结状态[1,5](@ref) */ clear_notify_signal(); if (unlikely(task_work_pending(current))) task_work_run();}
调度函数schedule里去,schedule定义在 kernel/sched/core.c 中,在 __schedule() 中接受一个参数,该参数为 bool 型,false 表示非抢占,自愿调度,而 true 则相反。freeze中的调度是主动调度让出CPU。
/** * schedule - 内核主调度函数,负责进程/线程的上下文切换 * * 功能说明: * 1. 这是Linux内核的核心调度入口,通过自愿或强制调度触发(如时间片耗尽、高优先级任务唤醒) * 2. 采用嵌套调度机制:外层处理抢占控制,内层__schedule完成实际切换 * 3. 通过need_resched标志实现惰性调度检查,避免不必要的上下文切换开销 * * 设计要点: * - 禁用内核抢占期间执行关键调度操作,防止并发问题 * - 循环检查need_resched标志,确保及时响应调度请求 * - 与cgroup、workqueue等子系统深度集成(通过sched_submit_work/sched_update_worker) */asmlinkage __visible void __sched schedule(void){ /* 获取当前进程的task_struct指针 */ struct task_struct *tsk = current; /* * 预处理阶段: * 1. 检查死锁风险(如PI锁冲突) * 2. 提交待处理的I/O插队请求(避免I/O死锁) */ sched_submit_work(tsk); /* 主调度循环:可能因need_resched标志被多次触发 */ do { /* 禁用内核抢占,保护关键调度操作 */ preempt_disable(); /* * 核心调度逻辑: * - SM_NONE表示非主动抢占场景(区别于显式调用cond_resched) * - 完成运行队列选择、上下文切换等操作 */ __schedule(SM_NONE); /* 重新启用内核抢占(但不立即检查重调度标志) */ sched_preempt_enable_no_resched(); } while (need_resched()); /* 检查TIF_NEED_RESCHED标志,必要时重新调度 */ /* 后处理阶段:更新工作线程状态(如kworker) */ sched_update_worker(tsk);}EXPORT_SYMBOL(schedule); // 导出符号供内核模块使用/* * 关键机制说明: * 1. 抢占控制: * - preempt_disable/enable 构成调度临界区,防止中断或其它CPU干扰 * - sched_preempt_enable_no_resched 避免立即触发重复调度 * * 2. 调度触发场景: * - 自愿调度:阻塞系统调用、同步原语等待 * - 强制调度:时间片耗尽、高优先级任务唤醒(通过设置TIF_NEED_RESCHED) * * 3. 性能优化: * - __visible 确保函数可被编译器优化内联 * - __sched 宏将函数代码放入专用段,便于调试器过滤调度相关调用栈 * * 4. SMP支持: * - 通过per-CPU运行队列(rq)实现无锁化本地调度 * - __schedule内部处理负载均衡和CPU亲和性 */ [5,6](@ref)
四、最后
确定何时冻结、何时解冻、何时再冻结进程其实是一个复杂的问题,需要维护一个流程图来管理不同的场景。在本文中,我们只是简单介绍了内核冻结功能的实现逻辑。在 Android Framework 层还涉及到低内存时内存整理时解冻、dump进程信息时解冻、发送和接收广播临时解冻、持有文件锁解冻等策略。
参考:
https://juejin.cn/post/7264949719275880482
Control Group v2 --- The Linux Kernel documentation