Fundamentals 26 min read

Process Freezing Techniques: Deep Dive into the Linux Kernel cgroup Freezer Subsystem

This article explains the background, purpose, and detailed implementation of the Linux kernel cgroup freezer subsystem, including its data structures, command‑line usage, signal‑based freeze workflow, and a practical experiment that demonstrates process suspension and power savings on ARM64.

Linux Code Review Hub
Linux Code Review Hub
Linux Code Review Hub
Process Freezing Techniques: Deep Dive into the Linux Kernel cgroup Freezer Subsystem

Background

The cgroup mechanism was introduced by Google engineers Paul Menage and Rohit Seth in 2006 and merged into the mainline Linux kernel in 2007. Android added CPU freezing for cache‑heavy apps starting with Android 11, moving idle background processes into a dedicated cgroup to reduce CPU usage and save battery. Two freezing approaches exist: the cgroup freezer subsystem and the traditional SIGSTOP / SIGCONT signals; the freezer subsystem is more mature and widely adopted by OEMs.

cgroup Freezer Subsystem Functions

Pause processes: all threads of a frozen task stop execution.

Release resources: CPU and memory occupied by frozen tasks become available to other workloads.

Power saving: frozen tasks do not wake the device, reducing battery drain.

Quick resume: frozen tasks can be thawed and resume execution without a full restart.

cgroup Components

Verify that the cgroup‑v2 filesystem is loaded:

linux:/ # cat /proc/filesystems | grep cgroup2
nodev cgroup2

Mount the cgroup filesystem (usually already mounted at /sys/fs/cgroup): linux:/ # mount -t cgroup2 none d The hierarchy starts with a single root cgroup. New child cgroups are created with mkdir. Controllers such as cpu, memory, and freezer are enabled top‑down; a child can use a controller only if its parent has it enabled. Depth and descendant limits are enforced with cgroup.max.depth and cgroup.max.descendants.

Process‑cgroup Binding

The file cgroup.procs lists PIDs belonging to a cgroup; writing a PID to it binds the process to that cgroup. The relationship is many‑to‑many: a process can belong to multiple cgroups and a cgroup can contain many processes. In the kernel, task_struct holds two relevant members:

struct task_struct {
#ifdef CONFIG_CGROUPS
    struct css_set __rcu *cgroups;
    struct list_head cg_list;
#endif
};

The css_set aggregates pointers to cgroup_subsys_state objects for each controller, reducing per‑task overhead:

struct css_set {
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
    struct list_head tasks;          // tasks sharing this css_set
    struct hlist_node hlist;        // hash table linkage
    struct list_head cgrp_links;    // links to cgroups referenced from this css_set
};
cgroup_subsys_state

stores per‑cgroup controller state:

struct cgroup_subsys_state {
    struct cgroup *cgroup;          // the cgroup this css is attached to
    struct cgroup_subsys *ss;        // the subsystem this css belongs to
    int id;
    unsigned int flags;
    struct work_struct destroy_work;
    struct rcu_work destroy_rwork;
    struct cgroup_subsys_state *parent; // parent css
};

The core cgroup structure contains the freezer state among other fields:

struct cgroup {
    struct cgroup_subsys_state self;
    unsigned long flags;
    int level;
    int max_depth;
    struct cgroup_root *root;
    struct list_head cset_links;
    struct list_head e_csets[CGROUP_SUBSYS_COUNT];
    struct cgroup_freezer_state freezer; // internal freezer state
};

Practical Experiment

Run a CPU‑intensive script ( cpu_load.sh) to generate a process with PID 8855 in state R:

linux:/ # ps -Ae | grep 8855
root          8855  8619   12470528   4160 0      0 R sh

Create a cgroup an_test, move the process into it, and freeze the group:

linux:/sys/fs/cgroup # mkdir an_test
linux:/sys/fs/cgroup # cd an_test
linux:/sys/fs/cgroup/an_test # echo 8855 > cgroup.procs
linux:/sys/fs/cgroup/an_test # echo 1 > cgroup.freeze

After freezing, the process state changes to S and its WCHAN shows it is blocked in do_freezer_trap:

linux:/ # ps -Ae | grep 8855
root          8855  8619   12470528   4160 do_freezer_trap    0 S sh

The CPU load drops, confirming that the frozen task no longer consumes CPU cycles.

Implementation Details

Overall Freeze Flow

The kernel sets the pending‑signal flag TIF_SIGPENDING on the target task without delivering an actual signal, then sends an IPI to wake the task. When the task returns to user space, the pending‑signal check triggers the freeze handling.

Setting Freeze/Unfreeze

Writing to cgroup.freeze invokes cgroup_freeze_write, which calls cgroup_freeze_task to set or clear the JOBCTL_TRAP_FREEZE bit and then wakes the task:

static struct cftype cgroup_base_files[] = {
    { .name = "cgroup.freeze", .flags = CFTYPE_NOT_ON_ROOT,
      .seq_show = cgroup_freeze_show, .write = cgroup_freeze_write },
    ...
};

Write path:

vfs_write → kernfs_fop_write_iter → cgroup_file_write → cgroup_freeze_write →
    cgroup_kn_lock_live → cgroup_freeze → css_for_each_descendant_pre →
    cgroup_do_freeze → cgroup_freeze_task
cgroup_do_freeze

iterates over all tasks in the cgroup, skipping kernel threads:

static void cgroup_do_freeze(struct cgroup *cgrp, bool freeze) {
    css_task_iter_start(&cgrp->self, 0, &it);
    while ((task = css_task_iter_next(&it))) {
        if (task->flags & PF_KTHREAD)
            continue;
        cgroup_freeze_task(task, freeze);
    }
    css_task_iter_end(&it);
}
cgroup_freeze_task

sets or clears JOBCTL_TRAP_FREEZE and wakes the task:

static void cgroup_freeze_task(struct task_struct *task, bool freeze) {
    if (freeze) {
        task->jobctl |= JOBCTL_TRAP_FREEZE;
        signal_wake_up(task, false);
    } else {
        task->jobctl &= ~JOBCTL_TRAP_FREEZE;
        wake_up_process(task);
    }
    unlock_task_sighand(task, &flags);
}
signal_wake_up_state

sets TIF_SIGPENDING, calls wake_up_state, and if the task is on another CPU, invokes kick_process to send an IPI:

void signal_wake_up_state(struct task_struct *t, unsigned int state) {
    set_tsk_thread_flag(t, TIF_SIGPENDING);
    if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
        kick_process(t);
}
kick_process

sends a reschedule IPI to the target CPU:

void kick_process(struct task_struct *p) {
    int cpu;
    preempt_disable();
    cpu = task_cpu(p);
    if ((cpu != smp_processor_id()) && task_curr(p))
        smp_send_reschedule(cpu);
    preempt_enable();
}

Signal Handling Chain

When the task returns to user mode, do_notify_resume checks TIF_SIGPENDING and calls do_signal. do_signal eventually reaches get_signal, which discovers the pending freeze trap and invokes do_freezer_trap:

void do_notify_resume(struct pt_regs *regs, unsigned long thread_flags) {
    if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
        do_signal(regs);
}

Freeze path inside do_signal:

if (current->jobctl & JOBCTL_TRAP_FREEZE) {
    do_freezer_trap();
}
do_freezer_trap

clears TIF_SIGPENDING, sets the task state to TASK_INTERRUPTIBLE, calls cgroup_enter_frozen, and finally invokes freezable_schedule to voluntarily yield the CPU:

static void do_freezer_trap(void) {
    if ((current->jobctl & (JOBCTL_PENDING_MASK | JOBCTL_TRAP_FREEZE)) != JOBCTL_TRAP_FREEZE)
        return;
    __set_current_state(TASK_INTERRUPTIBLE);
    clear_thread_flag(TIF_SIGPENDING);
    cgroup_enter_frozen();
    freezable_schedule();
}
freezable_schedule

calls the regular scheduler while preventing the freezer from being counted as a blocker:

static inline void freezable_schedule(void) {
    freezer_do_not_count();
    schedule();
    freezer_count();
}

Conclusion

The kernel‑side mechanics of the cgroup freezer involve a write to cgroup.freeze, propagation of the freeze flag down the hierarchy, per‑task flag manipulation ( JOBCTL_TRAP_FREEZE, TIF_SIGPENDING), an IPI‑based wake‑up, and a signal‑handling path that ultimately calls do_freezer_trap and freezable_schedule to suspend the task. This foundation enables Android’s higher‑level background‑process management policies (e.g., low‑memory handling, broadcast‑driven temporary unfreeze, binder‑based freeze) to control when processes are frozen, unfrozen, or refrozen.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Process ManagementLinux kernelARM64cgroupfreezer
Linux Code Review Hub
Written by

Linux Code Review Hub

A professional Linux technology community and learning platform covering the kernel, memory management, process management, file system and I/O, performance tuning, device drivers, virtualization, and cloud computing.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.