Linux 进程冻结机制原理详解

56 阅读42分钟

1. 概述

1.1 什么是进程冻结?

进程冻结(Process Freezing)是 Linux 内核在系统挂起(Suspend)时使用的一种机制,用于暂停所有用户空间进程的执行,使系统能够安全地进入低功耗状态。

1.2 核心特点

  1. 状态保存在 RAM 中:不需要写入磁盘
  2. 快速恢复:恢复时间通常在毫秒级
  3. 自动保存:调度器自动处理寄存器保存
  4. 透明性:用户空间进程无需修改代码(但需要正确使用系统调用)

1.3 与休眠(Hibernation)的对比

特性进程冻结(Freeze)休眠(Hibernate)
状态保存位置RAM(内存)磁盘(swap分区)
保存方式自动(调度器)手动(写入磁盘)
恢复时间毫秒级(<100ms)秒级(1-10秒)
功耗低(但仍在运行)极低(几乎为零)
断电支持不支持(需要供电)支持(可断电)
使用场景挂起(Suspend to RAM)休眠(Suspend to Disk)
状态大小整个RAM压缩后的内存镜像

2. 进程状态的数据结构

2.1 task_struct - 进程的核心数据结构

每个进程在内核中都有一个 task_struct 结构,包含了进程的所有信息:

struct task_struct {
    // ========== 进程标识 ==========
    pid_t pid;              // 进程ID
    pid_t tgid;             // 线程组ID
    char comm[TASK_COMM_LEN];  // 进程名称
    
    // ========== 进程状态 ==========
    volatile long state;    // 进程状态:
                            // TASK_RUNNING (0)      - 可运行
                            // TASK_INTERRUPTIBLE (1) - 可中断睡眠
                            // TASK_UNINTERRUPTIBLE (2) - 不可中断睡眠
                            // TASK_STOPPED (4)      - 停止
                            // TASK_TRACED (8)       - 被跟踪
                            // EXIT_DEAD (16)        - 死亡
                            // EXIT_ZOMBIE (32)      - 僵尸
    
    // ========== 进程标志 ==========
    unsigned int flags;     // 进程标志位:
                            // PF_FROZEN (0x00000040)      - 已冻结
                            // PF_FREEZER_SKIP (0x400000)  - 跳过冻结
                            // PF_NOFREEZE (0x800000)      - 不可冻结
                            // PF_KTHREAD (0x00200000)     - 内核线程
    
    // ========== 内存管理 ==========
    struct mm_struct *mm;           // 内存描述符(用户空间)
    struct mm_struct *active_mm;    // 活动内存描述符
    
    // ========== CPU 寄存器状态 ==========
    struct thread_struct thread;    // CPU 寄存器状态
                                    // 这是状态保存的关键!
    
    // ========== 文件系统 ==========
    struct files_struct *files;     // 打开的文件描述符
    
    // ========== 信号处理 ==========
    struct signal_struct *signal;   // 信号处理结构
    struct sigpending pending;      // 待处理的信号
    
    // ========== 定时器 ==========
    struct list_head cpu_timers[3]; // CPU 定时器列表
    
    // ========== 调度相关 ==========
    int prio;               // 优先级
    int static_prio;        // 静态优先级
    struct sched_entity se; // 调度实体
    
    // ========== 其他 ==========
    struct list_head tasks; // 进程链表
    // ... 更多字段
};

2.2 thread_struct - CPU 寄存器状态

这是状态保存的核心!所有 CPU 寄存器都保存在这里:

struct thread_struct {
    // ARM64 架构示例
    unsigned long sp;       // 栈指针(Stack Pointer)
    unsigned long pc;       // 程序计数器(Program Counter)
    unsigned long lr;       // 链接寄存器(Link Register)
    unsigned long sp_el0;   // EL0 栈指针
    unsigned long sp_el1;   // EL1 栈指针
    unsigned long elr_el1;  // EL1 异常链接寄存器
    unsigned long spsr_el1;// EL1 保存的程序状态寄存器
    
    // 通用寄存器
    unsigned long regs[31]; // x0-x30 通用寄存器
    
    // 浮点寄存器
    struct fpsimd_state fpsimd_state; // 浮点和 SIMD 寄存器
    
    // 其他架构特定寄存器
    // ...
};

关键点

  • 当进程被调度出去时,调度器自动将所有寄存器保存到这里
  • 当进程被调度回来时,调度器自动从这里恢复所有寄存器
  • 不需要手动保存或恢复,这是调度器的标准功能

2.3 mm_struct - 内存映射

struct mm_struct {
    struct vm_area_struct *mmap;    // 虚拟内存区域链表
    pgd_t *pgd;                     // 页全局目录(Page Global Directory)
    atomic_t mm_users;               // 使用计数
    atomic_t mm_count;               // 引用计数
    
    // 内存区域
    unsigned long start_code;        // 代码段起始地址
    unsigned long end_code;          // 代码段结束地址
    unsigned long start_data;        // 数据段起始地址
    unsigned long end_data;          // 数据段结束地址
    unsigned long start_brk;         // 堆起始地址
    unsigned long brk;               // 堆当前地址
    unsigned long start_stack;       // 栈起始地址
    
    // 页表
    struct rw_semaphore mmap_sem;    // 内存映射信号量
    // ...
};

关键点

  • 所有内存映射信息都在这个结构中
  • 内存内容本身在 RAM 中,不需要特殊保存
  • 挂起时 RAM 保持供电(或进入低功耗模式),内存内容不丢失

3. 冻结信号的发送机制

3.1 冻结流程的入口

// kernel/power/process.c
int freeze_processes(void)
{
    int error;
    int oom_kills_saved;
    
    // 1. 记录当前的 OOM kill 计数
    oom_kills_saved = oom_kills_count();
    
    // 2. 设置全局冻结标志
    error = __usermodehelper_disable(UMH_FREEZING);
    if (error)
        return error;
    
    // 3. 开始冻结用户空间进程
    if (!pm_freezing)
        atomic_inc(&system_freezing_cnt);
    pm_freezing = true;
    
    // 4. 尝试冻结所有任务
    error = try_to_freeze_tasks(true);
    
    // 5. 检查是否有进程无法冻结
    if (!error && !oom_kills_saved && oom_kills_count() != oom_kills_saved)
        error = -EBUSY;
    
    if (error) {
        // 冻结失败,恢复状态
        thaw_processes();
    } else {
        // 冻结成功
        pr_info("Freezing user space processes ... (elapsed %d.%03d seconds) done.\n",
                elapsed_msecs / 1000, elapsed_msecs % 1000);
    }
    
    return error;
}

3.2 try_to_freeze_tasks - 冻结所有任务

// kernel/power/process.c
static int try_to_freeze_tasks(bool user_only)
{
    struct task_struct *g, *p;
    unsigned long end_time;
    unsigned int todo;
    bool wq_busy = false;
    struct timeval start, end;
    u64 elapsed_msecs64;
    unsigned int elapsed_msecs;
    bool wakeup = false;
    int sleep_usecs = USEC_PER_MSEC;
    
    // 1. 记录开始时间
    do_gettimeofday(&start);
    end_time = jiffies + msecs_to_jiffies(freeze_timeout_msecs);
    
    // 2. 循环尝试冻结所有任务
    while (true) {
        todo = 0;
        read_lock(&tasklist_lock);
        
        // 3. 遍历所有进程
        for_each_process_thread(g, p) {
            // 跳过内核线程(如果只冻结用户空间)
            if (user_only && p->flags & PF_KTHREAD)
                continue;
            
            // 跳过不可冻结的进程
            if (freezer_should_skip(p))
                continue;
            
            // 4. 尝试冻结这个进程
            if (!freeze_task(p))
                todo++;
        }
        
        read_unlock(&tasklist_lock);
        
        // 5. 检查工作队列
        if (!wq_busy) {
            wq_busy = freeze_workqueues_busy();
            todo += wq_busy;
        }
        
        // 6. 检查是否所有进程都已冻结
        if (!todo || time_after(jiffies, end_time))
            break;
        
        // 7. 等待一段时间后重试
        usleep_range(sleep_usecs, sleep_usecs + sleep_usecs / 2);
        sleep_usecs = min_t(unsigned int, sleep_usecs * 2, 100 * USEC_PER_MSEC);
    }
    
    // 8. 检查结果
    do_gettimeofday(&end);
    elapsed_msecs64 = timeval_to_ns(&end) - timeval_to_ns(&start);
    elapsed_msecs = elapsed_msecs64 / NSEC_PER_MSEC;
    
    if (todo) {
        // 有进程无法冻结
        pr_err("Freezing of tasks failed after %d.%03d seconds "
               "(%d tasks refusing to freeze, wq_busy=%d):\n",
               elapsed_msecs / 1000, elapsed_msecs % 1000,
               todo - wq_busy, wq_busy);
        // 打印无法冻结的进程信息
        // ...
        return -EBUSY;
    }
    
    return 0;
}

3.3 freeze_task - 向单个进程发送冻结信号

// kernel/freezer.c
bool freeze_task(struct task_struct *p)
{
    unsigned long flags;
    
    // 1. 检查是否应该跳过这个进程
    if (freezer_should_skip(p))
        return false;
    
    // 2. 获取 freezer_lock
    spin_lock_irqsave(&freezer_lock, flags);
    
    // 3. 检查进程是否已经在冻结或已冻结
    if (!freezing(p) || frozen(p)) {
        spin_unlock_irqrestore(&freezer_lock, flags);
        return false;
    }
    
    // 4. 发送冻结信号
    if (!(p->flags & PF_KTHREAD)) {
        // 用户空间进程:发送"假信号"
        fake_signal_wake_up(p);
    } else {
        // 内核线程:直接唤醒
        wake_up_state(p, TASK_INTERRUPTIBLE);
    }
    
    spin_unlock_irqrestore(&freezer_lock, flags);
    return true;
}

3.4 fake_signal_wake_up - 发送"假信号"

// kernel/freezer.c
static void fake_signal_wake_up(struct task_struct *p)
{
    unsigned long flags;
    
    // 1. 获取信号处理锁
    if (lock_task_sighand(p, &flags)) {
        // 2. 唤醒进程(如果进程在睡眠)
        signal_wake_up(p, 0);
        // 注意:这里没有真正发送信号,只是:
        // - 设置 TIF_SIGPENDING 标志
        // - 唤醒进程(如果进程在 TASK_INTERRUPTIBLE 状态)
        unlock_task_sighand(p, &flags);
    }
}

关键点

  • fake_signal_wake_up() 不发送真正的信号
  • 它只是:
    1. 设置 TIF_SIGPENDING 标志(表示有信号待处理)
    2. 唤醒进程(如果进程在睡眠)
  • 进程被唤醒后,会在系统调用返回时检查这个标志
  • 如果发现需要冻结,进程会调用 try_to_freeze()

4. 进程如何响应冻结信号

4.1 检查时机

进程在以下时机检查是否需要冻结:

  1. 系统调用返回时
  2. 从睡眠状态唤醒时
  3. 显式调用 try_to_freeze()

4.2 系统调用返回时的检查

当用户空间进程执行系统调用时:

// 用户空间代码
int nfds = epoll_wait(epfd, events, maxevents, timeout);
// ↑ 系统调用

// 内核中的系统调用实现(简化版)
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
                int, maxevents, int, timeout)
{
    // ... 等待事件或超时 ...
    
    // 系统调用返回前
    if (freezing(current)) {  // ← 检查是否需要冻结
        __refrigerator(false);  // ← 进入冻结状态
    }
    
    return nfds;
}

关键点

  • 进程只有在系统调用返回时才能检查冻结条件
  • 如果系统调用长时间阻塞(如 epoll_wait 使用 30 秒超时),进程就无法及时响应冻结请求

4.3 try_to_freeze - 检查并冻结

// include/linux/freezer.h
static inline bool try_to_freeze(void)
{
    // 1. 检查是否有 PF_NOFREEZE 标志
    if (!(current->flags & PF_NOFREEZE))
        debug_check_no_locks_held();
    
    // 2. 调用实际的冻结函数
    return try_to_freeze_unsafe();
}

static inline bool try_to_freeze_unsafe(void)
{
    might_sleep();
    
    // 3. 检查是否需要冻结
    if (likely(!freezing(current)))
        return false;
    
    // 4. 进入冻结状态
    return __refrigerator(false);
}

4.4 freezing - 检查是否需要冻结

// include/linux/freezer.h
static inline bool freezing(struct task_struct *p)
{
    // 1. 快速路径:如果没有冻结条件,直接返回 false
    if (likely(!atomic_read(&system_freezing_cnt)))
        return false;
    
    // 2. 慢速路径:详细检查
    return freezing_slow_path(p);
}

// kernel/freezer.c
bool freezing_slow_path(struct task_struct *p)
{
    // 1. 检查是否有 PF_NOFREEZE 或 PF_SUSPEND_TASK 标志
    if (p->flags & (PF_NOFREEZE | PF_SUSPEND_TASK))
        return false;
    
    // 2. 检查是否有 TIF_MEMDIE 标志(内存不足时杀死)
    if (test_tsk_thread_flag(p, TIF_MEMDIE))
        return false;
    
    // 3. 检查 cgroup 冻结
    if (pm_nosig_freezing || cgroup_freezing(p))
        return true;
    
    // 4. 检查 PM 冻结(用户空间进程)
    if (pm_freezing && !(p->flags & PF_KTHREAD))
        return true;
    
    return false;
}

5. __refrigerator 的详细工作流程

5.1 完整代码分析

// kernel/freezer.c
bool __refrigerator(bool check_kthr_stop)
{
    bool was_frozen = false;
    unsigned int save = get_current_state();  // 保存当前状态
    
    pr_debug("%s entered refrigerator\n", current->comm);
    
    // ========== 进入无限循环 ==========
    for (;;) {
        // ========== 步骤 1:设置为不可中断状态 ==========
        set_current_state(TASK_UNINTERRUPTIBLE);
        // 这确保进程不会被信号唤醒(除了解冻信号)
        
        // ========== 步骤 2:设置冻结标志 ==========
        spin_lock_irq(&freezer_lock);
        current->flags |= PF_FROZEN;  // ← 标记为已冻结
        
        // 检查是否还需要冻结
        if (!freezing(current) ||
            (check_kthr_stop && kthread_should_stop()))
            current->flags &= ~PF_FROZEN;  // 如果不需要冻结了,清除标志
        spin_unlock_irq(&freezer_lock);
        
        // ========== 步骤 3:检查是否需要退出 ==========
        if (!(current->flags & PF_FROZEN)) {
            // 不需要冻结了,退出循环
            break;
        }
        
        // ========== 步骤 4:标记为已冻结 ==========
        was_frozen = true;
        
        // ========== 步骤 5:进入睡眠(关键!)==========
        schedule();  // ← 进程在这里被调度出去
        
        // ========== 步骤 6:被唤醒后继续循环 ==========
        // 当进程被解冻时,会从这里继续执行
        // 继续循环检查是否还需要冻结
    }
    
    pr_debug("%s left refrigerator\n", current->comm);
    
    // ========== 步骤 7:恢复之前的状态 ==========
    set_current_state(save);
    
    return was_frozen;
}

5.2 详细步骤说明

步骤 1:设置进程状态为 TASK_UNINTERRUPTIBLE

set_current_state(TASK_UNINTERRUPTIBLE);

作用

  • 将进程状态设置为不可中断睡眠
  • 这确保进程不会被普通信号唤醒
  • 只有解冻信号才能唤醒进程

状态转换

TASK_RUNNING (0) 
    ↓
TASK_INTERRUPTIBLE (1)  // 如果进程之前在等待
    ↓
TASK_UNINTERRUPTIBLE (2)  // 现在设置为不可中断

步骤 2:设置 PF_FROZEN 标志

spin_lock_irq(&freezer_lock);
current->flags |= PF_FROZEN;

作用

  • 标记进程为已冻结状态
  • 其他代码可以通过 frozen(p) 检查进程是否已冻结

标志位

#define PF_FROZEN 0x00000040  // 第 6 位

步骤 3:调用 schedule() - 关键步骤

schedule();  // 进程在这里被调度出去

这是状态保存的关键时刻!

schedule() 被调用时:

  1. 调度器选择下一个进程运行
  2. 自动保存当前进程的所有寄存器到 thread_struct
  3. 进程被移出运行队列
  4. 进程进入 TASK_UNINTERRUPTIBLE 状态
  5. 进程的所有状态都保留在 RAM 中

详细过程

// 调度器代码(简化版)
void schedule(void)
{
    struct task_struct *prev = current;
    struct task_struct *next;
    
    // 1. 选择下一个要运行的进程
    next = pick_next_task();
    
    // 2. 如果切换进程
    if (prev != next) {
        // 3. 保存当前进程的寄存器
        context_switch(prev, next);
    }
}

void context_switch(struct task_struct *prev, struct task_struct *next)
{
    // 1. 保存当前进程的寄存器到 thread_struct
    switch_to(prev, next, prev);
    // ↑ 这里会调用架构特定的代码保存所有寄存器
}

// ARM64 架构的寄存器保存(简化版)
.macro switch_to prev, next, last
    // 保存所有通用寄存器
    stp x19, x20, [\prev, #THREAD_CPU_CONTEXT + 16 * 0]
    stp x21, x22, [\prev, #THREAD_CPU_CONTEXT + 16 * 1]
    // ... 保存所有寄存器
    
    // 保存栈指针
    mov x10, sp
    str x10, [\prev, #THREAD_CPU_CONTEXT + 16 * 13]
    
    // 恢复下一个进程的寄存器
    ldp x19, x20, [\next, #THREAD_CPU_CONTEXT + 16 * 0]
    ldp x21, x22, [\next, #THREAD_CPU_CONTEXT + 16 * 1]
    // ... 恢复所有寄存器
    
    // 恢复栈指针
    ldr x10, [\next, #THREAD_CPU_CONTEXT + 16 * 13]
    mov sp, x10
.endm

关键点

  • 所有寄存器(包括 SP、PC、LR、通用寄存器、浮点寄存器)都被保存到 thread_struct
  • 这是自动的,不需要手动保存
  • 保存的位置是 task_struct.thread,在 RAM 中

步骤 4:进程被唤醒

当系统解冻时,进程会被唤醒:

void __thaw_task(struct task_struct *p)
{
    if (frozen(p)) {
        wake_up_process(p);  // 唤醒进程
    }
}

唤醒过程

  1. 清除 PF_FROZEN 标志
  2. 将进程状态改为 TASK_RUNNING
  3. 将进程加入运行队列
  4. 调度器选择进程运行
  5. thread_struct 恢复所有寄存器
  6. schedule() 返回,继续执行

6. 调度器如何保存和恢复状态

6.1 寄存器保存的详细过程

ARM64 架构示例

// arch/arm64/include/asm/processor.h
struct cpu_context {
    unsigned long x19;
    unsigned long x20;
    unsigned long x21;
    unsigned long x22;
    unsigned long x23;
    unsigned long x24;
    unsigned long x25;
    unsigned long x26;
    unsigned long x27;
    unsigned long x28;
    unsigned long fp;      // x29 - 帧指针
    unsigned long sp;      // x30 - 栈指针(实际上是 x31)
    unsigned long pc;      // 程序计数器
};

struct thread_struct {
    struct cpu_context cpu_context;  // CPU 寄存器上下文
    unsigned long tp_value;          // 线程指针
    struct fpsimd_state fpsimd_state; // 浮点和 SIMD 寄存器
    // ...
};

保存过程(汇编代码)

// arch/arm64/kernel/entry.S
.macro save_all_regs
    // 保存通用寄存器 x19-x28
    stp x19, x20, [sp, #-16]!
    stp x21, x22, [sp, #-16]!
    stp x23, x24, [sp, #-16]!
    stp x25, x26, [sp, #-16]!
    stp x27, x28, [sp, #-16]!
    
    // 保存帧指针和链接寄存器
    stp x29, x30, [sp, #-16]!
    
    // 保存程序计数器(PC)
    mrs x0, elr_el1
    str x0, [sp, #-8]
    
    // 保存栈指针
    mov x0, sp
    str x0, [sp, #-8]
.endm

恢复过程(汇编代码)

// arch/arm64/kernel/entry.S
.macro restore_all_regs
    // 恢复栈指针
    ldr x0, [sp, #-8]
    mov sp, x0
    
    // 恢复程序计数器
    ldr x0, [sp, #-8]
    msr elr_el1, x0
    
    // 恢复帧指针和链接寄存器
    ldp x29, x30, [sp], #16
    
    // 恢复通用寄存器 x19-x28
    ldp x27, x28, [sp], #16
    ldp x25, x26, [sp], #16
    ldp x23, x24, [sp], #16
    ldp x21, x22, [sp], #16
    ldp x19, x20, [sp], #16
.endm

6.2 浮点寄存器保存

// arch/arm64/include/asm/fpsimd.h
struct fpsimd_state {
    __uint128_t vregs[32];  // 128位 SIMD 寄存器
    u32 fpsr;               // 浮点状态寄存器
    u32 fpcr;               // 浮点控制寄存器
};

保存时机

  • 当进程被调度出去时,如果使用了浮点/SIMD 指令
  • 保存到 thread_struct.fpsimd_state

7. 内存状态的保存

7.1 内存布局

进程的内存空间包括:

高地址
┌─────────────────┐
│   栈 (Stack)    │ ← 向下增长
│                 │
├─────────────────┤
│                 │
│   堆 (Heap)     │ ← 向上增长
│                 │
├─────────────────┤
│   数据段 (BSS)  │
├─────────────────┤
│   数据段 (Data) │
├─────────────────┤
│   代码段 (Text) │
└─────────────────┘
低地址

7.2 内存映射信息

所有内存映射信息都在 mm_struct 中:

struct mm_struct {
    // 虚拟内存区域链表
    struct vm_area_struct *mmap;
    
    // 页表
    pgd_t *pgd;  // 页全局目录
    
    // 内存区域
    unsigned long start_code;   // 代码段起始
    unsigned long end_code;      // 代码段结束
    unsigned long start_data;    // 数据段起始
    unsigned long end_data;      // 数据段结束
    unsigned long start_brk;     // 堆起始
    unsigned long brk;           // 堆当前
    unsigned long start_stack;   // 栈起始
    // ...
};

7.3 内存内容的位置

关键点:内存内容本身在 RAM 中!

┌─────────────────────────────────────┐
│          RAM (物理内存)              │
├─────────────────────────────────────┤
│  进程 A 的代码段                    │ ← 在 RAM 中
│  进程 A 的数据段                    │ ← 在 RAM 中
│  进程 A 的堆                        │ ← 在 RAM 中
│  进程 A 的栈                        │ ← 在 RAM 中
├─────────────────────────────────────┤
│  进程 B 的代码段                    │ ← 在 RAM 中
│  进程 B 的数据段                    │ ← 在 RAM 中
│  ...                                │
└─────────────────────────────────────┘

挂起时的行为

  • RAM 保持供电(或进入低功耗模式)
  • 内存内容不会丢失
  • 不需要写入磁盘
  • 恢复时直接从 RAM 读取

7.4 页表

页表定义了虚拟地址到物理地址的映射:

虚拟地址空间         页表          物理地址空间
┌──────────┐        ┌──────┐       ┌──────────┐
│ 0x400000 │ ────→  │ PTE  │ ────→ │ 0x100000 │
│          │        └──────┘       │          │
│ 0x500000 │ ────→  │ PTE  │ ────→ │ 0x200000 │
│          │        └──────┘       │          │
└──────────┘                       └──────────┘

关键点

  • 页表本身也在 RAM 中
  • 挂起时页表保持不变
  • 恢复时页表仍然有效

8. 文件描述符和打开文件的保存

8.1 files_struct

struct files_struct {
    atomic_t count;              // 引用计数
    struct fdtable *fdt;        // 文件描述符表
    struct file *fd_array[NR_OPEN_DEFAULT];  // 打开的文件数组
    // ...
};

struct fdtable {
    unsigned int max_fds;       // 最大文件描述符数
    struct file **fd;           // 文件指针数组
    // ...
};

8.2 文件状态

每个打开的文件都有:

struct file {
    struct path f_path;         // 文件路径
    struct inode *f_inode;      // inode
    struct file_operations *f_op; // 文件操作
    loff_t f_pos;               // 文件位置(当前读写位置)
    unsigned int f_flags;       // 文件标志
    // ...
};

关键点

  • 所有打开的文件信息都在 task_struct.files
  • 文件位置(f_pos)等信息都保存在内存中
  • 挂起时这些信息保持不变
  • 恢复时文件描述符仍然有效

8.3 文件内容

文件内容本身不在进程的内存空间中,而是在:

  • 磁盘上的文件系统
  • 或者内核的页缓存中

关键点

  • 文件内容不需要特殊保存
  • 挂起时文件系统保持不变
  • 恢复时可以直接访问文件

9. 信号和定时器的保存

9.1 信号结构

struct signal_struct {
    atomic_t count;              // 引用计数
    struct sigpending pending;  // 待处理的信号
    // ...
};

struct sigpending {
    struct list_head list;      // 信号链表
    sigset_t signal;            // 信号集合
};

9.2 待处理的信号

struct sigqueue {
    struct list_head list;      // 链表节点
    siginfo_t info;             // 信号信息
    // ...
};

关键点

  • 所有待处理的信号都在 task_struct.signal.pending
  • 挂起时信号队列保持不变
  • 恢复时信号仍然有效

9.3 定时器

struct task_struct {
    struct list_head cpu_timers[3];  // CPU 定时器列表
    // ...
};

struct cpu_timer {
    struct list_head entry;     // 链表节点
    u64 expires;               // 到期时间
    void (*function)(unsigned long);  // 回调函数
    unsigned long data;         // 回调数据
    // ...
};

关键点

  • 所有定时器都在 task_struct.cpu_timers
  • 挂起时定时器暂停(或调整到期时间)
  • 恢复时定时器继续工作

10. 解冻过程详解

10.1 thaw_processes - 解冻所有进程

// kernel/power/process.c
void thaw_processes(void)
{
    struct task_struct *g, *p;
    
    pr_info("Restarting tasks ... ");
    
    // 1. 清除全局冻结标志
    __usermodehelper_set_disable_depth(UMH_FREEZING);
    atomic_dec(&system_freezing_cnt);
    pm_freezing = false;
    pm_nosig_freezing = false;
    
    // 2. 唤醒所有冻结的进程
    read_lock(&tasklist_lock);
    for_each_process_thread(g, p) {
        if (frozen(p)) {
            __thaw_task(p);
        }
    }
    read_unlock(&tasklist_lock);
    
    // 3. 恢复用户空间辅助程序
    __usermodehelper_set_disable_depth(UMH_DISABLED);
    
    schedule();
    pr_cont("done.\n");
}

10.2 __thaw_task - 解冻单个进程

// kernel/freezer.c
void __thaw_task(struct task_struct *p)
{
    unsigned long flags;
    
    spin_lock_irqsave(&freezer_lock, flags);
    
    if (frozen(p)) {
        // 清除冻结标志
        p->flags &= ~PF_FROZEN;
        
        // 唤醒进程
        wake_up_process(p);
    }
    
    spin_unlock_irqrestore(&freezer_lock, flags);
}

10.3 进程恢复执行

当进程被唤醒后:

  1. 调度器选择进程运行
  2. thread_struct 恢复所有寄存器
  3. schedule() 返回
  4. 继续执行 __refrigerator() 中的循环
  5. 检查 PF_FROZEN 标志,发现已清除
  6. 退出 __refrigerator()
  7. 恢复之前的状态
  8. 继续正常执行

11. 完整示例:一个进程的冻结过程

11.1 场景设置

假设有一个用户空间进程,正在执行:

// 用户空间代码
int main() {
    int epfd = epoll_create1(0);
    struct epoll_event events[10];
    
    while (1) {
        // 使用 30 秒超时(问题代码)
        int nfds = epoll_wait(epfd, events, 10, 30000);
        
        if (nfds > 0) {
            // 处理事件
            handle_events(events, nfds);
        }
    }
}

11.2 冻结过程的时间线

T=0s:   进程调用 epoll_wait(epfd, events, 10, 30000)
        ↓
T=0s:   进入内核,开始等待事件或超时
        ↓
T=0s:   进程进入 TASK_INTERRUPTIBLE 状态
        ↓
T=5s:   系统尝试挂起,调用 freeze_processes()
        ↓
T=5s:   设置 pm_freezing = trueT=5s:   调用 freeze_task(进程)
        ↓
T=5s:   调用 fake_signal_wake_up(进程)
        ↓
T=5s:   设置 TIF_SIGPENDING 标志
        ↓
T=5s:   唤醒进程(从 TASK_INTERRUPTIBLE 唤醒)
        ↓
T=5s:   进程被唤醒,但 epoll_wait 还没超时
        ↓
T=5s:   进程检查:有事件吗?没有
        ↓
T=5s:   进程检查:超时了吗?没有(还有25秒)
        ↓
T=5s:   进程继续等待 ← 问题:没有返回!
        ↓
T=30s:  epoll_wait 超时返回
        ↓
T=30s:  系统调用返回,检查冻结条件
        ↓
T=30s:  发现 freezing(current) == trueT=30s:  调用 try_to_freeze()
        ↓
T=30s:  调用 __refrigerator(false)
        ↓
T=30s:  设置 PF_FROZEN 标志
        ↓
T=30s:  调用 schedule() ← 进程被调度出去
        ↓
T=30s:  【调度器自动保存所有寄存器到 thread_struct】
        ↓
T=30s:  进程进入 TASK_UNINTERRUPTIBLE 状态
        ↓
T=30s:  进程的所有状态都保留在 RAM 中:
        - 寄存器 → thread_struct(在 RAM 中)
        - 内存(代码/数据/栈/堆)→ RAM
        - 文件描述符 → files_struct(在 RAM 中)
        - 信号/定时器 → signal_struct(在 RAM 中)
        ↓
T=30s:  进程成功冻结!

11.3 修复后的代码

// 修复后的代码
int main() {
    int epfd = epoll_create1(0);
    struct epoll_event events[10];
    
    while (1) {
        // 使用 1 秒超时(修复后)
        int nfds = epoll_wait(epfd, events, 10, 1000);
        
        if (nfds > 0) {
            // 处理事件
            handle_events(events, nfds);
        } else if (nfds == 0) {
            // 超时,检查是否需要退出
            if (should_exit()) {
                break;
            }
        } else {
            // 错误处理
            if (errno == EINTR) {
                // 被信号中断,继续循环
                continue;
            }
            // 其他错误处理
        }
    }
}

11.4 修复后的冻结过程

T=0s:   进程调用 epoll_wait(epfd, events, 10, 1000)
        ↓
T=0s:   进入内核,开始等待事件或超时
        ↓
T=1s:   epoll_wait 超时返回(正常情况)
        ↓
T=1s:   系统调用返回,检查冻结条件
        ↓
T=1s:   发现 freezing(current) == false(还没有冻结请求)
        ↓
T=1s:   继续循环
        ↓
T=5s:   系统尝试挂起,调用 freeze_processes()
        ↓
T=5s:   设置 pm_freezing = trueT=5s:   调用 freeze_task(进程)
        ↓
T=5s:   调用 fake_signal_wake_up(进程)
        ↓
T=5s:   设置 TIF_SIGPENDING 标志
        ↓
T=5s:   进程正在执行循环,准备调用 epoll_wait
        ↓
T=5s:   进程调用 epoll_wait(epfd, events, 10, 1000)
        ↓
T=5s:   进入内核,检查 TIF_SIGPENDING 标志
        ↓
T=5s:   发现需要冻结,epoll_wait 提前返回(或快速超时)
        ↓
T=5s:   系统调用返回,检查冻结条件
        ↓
T=5s:   发现 freezing(current) == trueT=5s:   调用 try_to_freeze()
        ↓
T=5s:   调用 __refrigerator(false)
        ↓
T=5s:   设置 PF_FROZEN 标志
        ↓
T=5s:   调用 schedule() ← 进程被调度出去
        ↓
T=5s:  【调度器自动保存所有寄存器到 thread_struct】
        ↓
T=5s:  进程成功冻结!← 只用了 5 秒,而不是 30 秒!

12. 总结

12.1 状态保存的位置

状态类型保存位置保存方式是否需要特殊处理
CPU 寄存器task_struct.thread调度器自动保存❌ 不需要
内存(代码/数据)RAM已在内存中❌ 不需要
内存映射信息task_struct.mm已在内存中❌ 不需要
文件描述符task_struct.files已在内存中❌ 不需要
信号队列task_struct.signal已在内存中❌ 不需要
定时器task_struct.cpu_timers已在内存中❌ 不需要
进程状态task_struct.state调度器自动设置❌ 不需要

12.2 关键点

  1. 所有状态都在 RAM 中:不需要写入磁盘
  2. 自动保存:调度器在进程切换时自动保存寄存器
  3. 内存保留:挂起时 RAM 保持供电,内存内容不丢失
  4. 快速恢复:恢复时只需恢复寄存器,从 RAM 继续执行
  5. 透明性:用户空间进程无需修改代码(但需要正确使用系统调用)

12.3 为什么需要短超时?

因为进程只有在系统调用返回时才能检查冻结条件。如果系统调用长时间阻塞,进程就无法及时响应冻结请求。

12.4 与休眠的区别

  • 冻结(Freeze):状态在 RAM 中,快速恢复,用于挂起
  • 休眠(Hibernate):状态写入磁盘,慢速恢复,用于长时间断电

附录:相关数据结构完整定义

A.1 task_struct(部分)

struct task_struct {
    // 进程标识
    pid_t pid;
    pid_t tgid;
    char comm[TASK_COMM_LEN];
    
    // 进程状态
    volatile long state;
    unsigned int flags;
    
    // 内存管理
    struct mm_struct *mm;
    struct mm_struct *active_mm;
    
    // CPU 寄存器
    struct thread_struct thread;
    
    // 文件系统
    struct files_struct *files;
    
    // 信号处理
    struct signal_struct *signal;
    struct sigpending pending;
    
    // 定时器
    struct list_head cpu_timers[3];
    
    // 调度
    int prio;
    struct sched_entity se;
    
    // 链表
    struct list_head tasks;
    // ...
};

A.2 thread_struct(ARM64)

struct thread_struct {
    struct cpu_context cpu_context;
    unsigned long tp_value;
    struct fpsimd_state fpsimd_state;
    unsigned long fault_address;
    unsigned long fault_code;
    // ...
};

A.3 mm_struct(部分)

struct mm_struct {
    struct vm_area_struct *mmap;
    struct rb_root mm_rb;
    pgd_t *pgd;
    atomic_t mm_users;
    atomic_t mm_count;
    unsigned long start_code;
    unsigned long end_code;
    unsigned long start_data;
    unsigned long end_data;
    unsigned long start_brk;
    unsigned long brk;
    unsigned long start_stack;
    // ...
};

13. 系统调用返回路径的详细分析

13.1 系统调用返回的完整流程

当用户空间进程执行系统调用时,内核需要处理返回路径,这是检查冻结条件的关键时机。

ARM64 架构的系统调用返回流程

// arch/arm64/kernel/entry.S
/*
 * 系统调用返回路径:
 * 1. 从内核空间返回到用户空间
 * 2. 检查是否有待处理的信号
 * 3. 检查是否需要冻结
 * 4. 恢复用户空间上下文
 */
ret_to_user:
    // 1. 禁用中断
    disable_daif
    
    // 2. 检查是否需要重新调度
    ldr x1, [tsk, #TSK_TI_FLAGS]
    tbnz x1, #TIF_NEED_RESCHED, work_pending
    
    // 3. 检查是否有待处理的信号(包括冻结信号)
    tbnz x1, #TIF_SIGPENDING, do_notify_resume
    
    // 4. 检查是否需要冻结
    tbnz x1, #TIF_FREEZE, do_freeze
    
    // 5. 恢复用户空间上下文
    kernel_exit 0

do_notify_resume:
    // 处理信号
    mov x0, sp
    mov x1, #0
    bl do_notify_resume
    b ret_to_user

do_freeze:
    // 处理冻结
    bl try_to_freeze
    b ret_to_user

关键点

  • TIF_SIGPENDING 标志在 fake_signal_wake_up() 中设置
  • 系统调用返回时检查这个标志
  • 如果设置了,会调用 do_notify_resume() 处理信号
  • 在信号处理过程中,会检查是否需要冻结

13.2 TIF_SIGPENDING 标志的检查机制

TIF_SIGPENDING 的定义

// arch/arm64/include/asm/thread_info.h
#define TIF_SIGPENDING       0   // 第 0 位:有待处理的信号
#define TIF_NEED_RESCHED      1   // 第 1 位:需要重新调度
#define TIF_FREEZE            2   // 第 2 位:需要冻结(某些架构)

标志的存储位置

// include/linux/thread_info.h
struct thread_info {
    unsigned long flags;  // 线程标志位
    // ...
};

// 每个进程的 thread_info 存储在 task_struct 中
// 通过 current_thread_info() 宏访问
#define current_thread_info() \
    ((struct thread_info *)(current_stack_pointer & ~(THREAD_SIZE - 1)))

标志的检查时机

// kernel/signal.c
/*
 * 系统调用返回时检查 TIF_SIGPENDING 标志
 * 这个检查发生在系统调用返回路径中
 */
static void do_signal(struct pt_regs *regs)
{
    struct ksignal ksig;
    
    // 1. 检查是否有待处理的信号
    if (get_signal(&ksig)) {
        // 2. 处理信号
        handle_signal(&ksig, regs);
    }
    
    // 3. 在信号处理过程中,检查是否需要冻结
    if (freezing(current)) {
        try_to_freeze();
    }
}

13.3 系统调用返回路径的详细时序

完整的系统调用返回时序

用户空间系统调用返回
    ↓
内核空间系统调用处理函数返回
    ↓
ret_to_user(汇编代码)
    ├─ 1. 禁用中断
    ├─ 2. 检查 TIF_NEED_RESCHED
    │   └─ 如果需要,调用 schedule()
    ├─ 3. 检查 TIF_SIGPENDING
    │   └─ 如果设置,调用 do_notify_resume()
    │       ├─ 调用 do_signal()
    │       │   ├─ 检查冻结条件
    │       │   └─ 如果需要,调用 try_to_freeze()
    │       └─ 返回
    ├─ 4. 检查 TIF_FREEZE(某些架构)
    │   └─ 如果设置,调用 try_to_freeze()
    └─ 5. kernel_exit(恢复用户空间上下文)
        ├─ 恢复通用寄存器
        ├─ 恢复浮点寄存器
        ├─ 恢复程序计数器
        └─ 返回到用户空间

关键点

  • 系统调用返回路径是检查冻结条件的唯一时机
  • 如果系统调用长时间阻塞,进程无法进入返回路径
  • 这就是为什么需要短超时的原因

14. CPU 寄存器保存/恢复的汇编级别细节

14.1 ARM64 架构的寄存器保存机制

ARM64 的寄存器分类

ARM64 寄存器:
├─ 通用寄存器(x0-x30)
   ├─ x0-x7: 参数/返回值寄存器
   ├─ x8: 间接结果位置寄存器
   ├─ x9-x15: 临时寄存器
   ├─ x16-x17: IP0/IP1(内部过程调用)
   ├─ x18: 平台寄存器
   ├─ x19-x28: 被调用者保存寄存器
   ├─ x29: 帧指针(FP)
   └─ x30: 链接寄存器(LR)
├─ 栈指针寄存器(SP)
├─ 程序计数器(PC)
├─ 程序状态寄存器(PSTATE)
└─ 浮点/SIMD 寄存器(v0-v31)

cpu_context 结构

// arch/arm64/include/asm/processor.h
struct cpu_context {
    unsigned long x19;   // 被调用者保存寄存器
    unsigned long x20;
    unsigned long x21;
    unsigned long x22;
    unsigned long x23;
    unsigned long x24;
    unsigned long x25;
    unsigned long x26;
    unsigned long x27;
    unsigned long x28;
    unsigned long fp;    // x29 - 帧指针
    unsigned long sp;    // 栈指针
    unsigned long pc;    // 程序计数器
};

14.2 寄存器保存的汇编实现

cpu_switch_to 的完整实现

// arch/arm64/kernel/entry.S
/*
 * cpu_switch_to(prev, next)
 * 
 * 参数:
 *   x0: prev (前一个进程的 task_struct)
 *   x1: next (下一个进程的 task_struct)
 * 
 * 功能:
 *   1. 保存前一个进程的寄存器
 *   2. 恢复下一个进程的寄存器
 *   3. 切换栈指针
 */
SYM_FUNC_START(cpu_switch_to)
    // ========== 保存前一个进程的寄存器 ==========
    
    // 1. 计算 cpu_context 的偏移
    mov x10, #THREAD_CPU_CONTEXT
    add x8, x0, x10  // x8 = prev->thread.cpu_context
    
    // 2. 保存栈指针
    mov x9, sp
    stp x19, x20, [x8], #16    // 保存 x19, x20
    stp x21, x22, [x8], #16    // 保存 x21, x22
    stp x23, x24, [x8], #16    // 保存 x23, x24
    stp x25, x26, [x8], #16    // 保存 x25, x26
    stp x27, x28, [x8], #16    // 保存 x27, x28
    stp x29, x9, [x8], #16      // 保存 x29 (FP), x9 (SP)
    str lr, [x8]                // 保存 x30 (LR)
    
    // ========== 恢复下一个进程的寄存器 ==========
    
    // 3. 计算下一个进程的 cpu_context 偏移
    add x8, x1, x10  // x8 = next->thread.cpu_context
    
    // 4. 恢复寄存器
    ldp x19, x20, [x8], #16    // 恢复 x19, x20
    ldp x21, x22, [x8], #16    // 恢复 x21, x22
    ldp x23, x24, [x8], #16    // 恢复 x23, x24
    ldp x25, x26, [x8], #16    // 恢复 x25, x26
    ldp x27, x28, [x8], #16    // 恢复 x27, x28
    ldp x29, x9, [x8], #16     // 恢复 x29 (FP), x9 (SP)
    ldr lr, [x8]               // 恢复 x30 (LR)
    
    // 5. 切换栈指针
    mov sp, x9
    
    // 6. 切换线程指针(Thread Pointer)
    msr sp_el0, x1  // 设置 EL0 栈指针为 next->task_struct
    
    // 7. 指针认证密钥安装(如果启用)
    ptrauth_keys_install_kernel x1, x8, x9, x10
    
    // 8. 影子调用栈(如果启用)
    scs_save x0     // 保存前一个进程的影子调用栈
    scs_load_current // 加载当前进程的影子调用栈
    
    // 9. 返回
    ret
SYM_FUNC_END(cpu_switch_to)

关键点

  • 只保存被调用者保存的寄存器(x19-x30)
  • 调用者保存的寄存器(x0-x18)不需要保存,因为调用者会负责保存
  • 栈指针(SP)和链接寄存器(LR)必须保存
  • 程序计数器(PC)通过 LR 间接保存

14.3 浮点/SIMD 寄存器的保存

fpsimd_state 结构

// arch/arm64/include/asm/fpsimd.h
struct fpsimd_state {
    __uint128_t vregs[32];  // 128位 SIMD 寄存器 v0-v31
    u32 fpsr;               // 浮点状态寄存器
    u32 fpcr;               // 浮点控制寄存器
};

浮点寄存器保存的时机

// arch/arm64/kernel/fpsimd.c
/*
 * 浮点寄存器保存的时机:
 * 1. 进程被调度出去时(如果使用了浮点指令)
 * 2. 进程进入内核空间时(如果使用了浮点指令)
 * 3. 信号处理时(如果使用了浮点指令)
 */
void fpsimd_save_state(struct fpsimd_state *st)
{
    int i;
    
    // 1. 保存所有 SIMD 寄存器
    for (i = 0; i < 32; i++) {
        asm volatile("str q%0, [%1, #%2]\n"
                     : "=w" (st->vregs[i])
                     : "r" (st), "i" (i * 16)
                     : "memory");
    }
    
    // 2. 保存浮点状态寄存器
    asm volatile("mrs %0, fpsr\n" : "=r" (st->fpsr));
    
    // 3. 保存浮点控制寄存器
    asm volatile("mrs %0, fpcr\n" : "=r" (st->fpcr));
}

浮点寄存器恢复

// arch/arm64/kernel/fpsimd.c
void fpsimd_restore_state(struct fpsimd_state *st)
{
    int i;
    
    // 1. 恢复所有 SIMD 寄存器
    for (i = 0; i < 32; i++) {
        asm volatile("ldr q%0, [%1, #%2]\n"
                     : "=w" (st->vregs[i])
                     : "r" (st), "i" (i * 16)
                     : "memory");
    }
    
    // 2. 恢复浮点状态寄存器
    asm volatile("msr fpsr, %0\n" : : "r" (st->fpsr));
    
    // 3. 恢复浮点控制寄存器
    asm volatile("msr fpcr, %0\n" : : "r" (st->fpcr));
}

14.4 寄存器保存的内存布局

thread_struct 在内存中的布局

task_struct (在 RAM 中)
├─ thread (thread_struct)
│  ├─ cpu_context (cpu_context)
│  │  ├─ x19 (8 字节)
│  │  ├─ x20 (8 字节)
│  │  ├─ ...
│  │  ├─ x28 (8 字节)
│  │  ├─ fp (8 字节)
│  │  ├─ sp (8 字节)
│  │  └─ pc (8 字节)
│  ├─ fpsimd_state (fpsimd_state)
│  │  ├─ vregs[0-31] (每个 16 字节,共 512 字节)
│  │  ├─ fpsr (4 字节)
│  │  └─ fpcr (4 字节)
│  └─ 其他字段
└─ 其他字段

关键点

  • 所有寄存器都保存在 task_struct.thread
  • 这个结构在 RAM 中,不需要写入磁盘
  • 挂起时 RAM 保持供电,寄存器状态不丢失

15. 进程状态转换的详细机制

15.1 进程状态的完整定义

Linux 进程状态的完整列表

// include/linux/sched.h
#define TASK_RUNNING            0x0000  // 可运行
#define TASK_INTERRUPTIBLE      0x0001  // 可中断睡眠
#define TASK_UNINTERRUPTIBLE    0x0002  // 不可中断睡眠
#define TASK_STOPPED            0x0004  // 停止
#define TASK_TRACED             0x0008  // 被跟踪
#define EXIT_DEAD               0x0010  // 死亡
#define EXIT_ZOMBIE             0x0020  // 僵尸
#define TASK_PARKED             0x0040  // 停放
#define TASK_DEAD               0x0080  // 死亡
#define TASK_WAKEKILL           0x0100  // 可被杀死唤醒
#define TASK_WAKING             0x0200  // 正在唤醒
#define TASK_NOLOAD             0x0400  // 无负载
#define TASK_NEW                0x0800  // 新建
#define TASK_STATE_MAX          0x1000  // 状态最大值

进程状态在冻结中的转换

正常状态:
TASK_RUNNING (0)
    ↓
进入系统调用
    ↓
TASK_INTERRUPTIBLE (1)  // 等待事件或超时
    ↓
收到冻结信号
    ↓
TASK_UNINTERRUPTIBLE (2)  // 在 __refrigerator() 中
    ↓
调用 schedule()
    ↓
进程被调度出去
    ↓
状态保持为 TASK_UNINTERRUPTIBLE (2)
    ↓
解冻时
    ↓
TASK_RUNNING (0)  // 恢复运行

15.2 set_current_state 的详细实现

set_current_state 宏

// include/linux/sched.h
#define set_current_state(state_value) \
    do { \
        WARN_ON_ONCE(is_special_task_state(state_value)); \
        smp_store_mb(current->__state, (state_value)); \
    } while (0)

关键点

  • smp_store_mb() 是内存屏障,确保状态写入对其他 CPU 可见
  • current->__state 是进程的实际状态字段
  • 状态改变会触发调度器的重新评估

内存屏障的作用

// include/linux/compiler.h
#define smp_store_mb(var, value) \
    do { \
        WRITE_ONCE(var, value); \
        smp_mb(); \
    } while (0)

// 确保:
// 1. 状态写入完成
// 2. 其他 CPU 可以看到状态改变
// 3. 后续的内存操作不会重排序到状态写入之前

15.3 进程状态检查的详细机制

进程状态检查的时机

// kernel/sched/core.c
/*
 * 调度器在以下时机检查进程状态:
 * 1. 时钟中断时(scheduler_tick)
 * 2. 进程唤醒时(try_to_wake_up)
 * 3. 进程阻塞时(__schedule)
 * 4. 进程解冻时(__thaw_task)
 */
static void __sched notrace __schedule(bool preempt)
{
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct rq_flags rf;
    struct rq *rq;
    int cpu;
    
    cpu = smp_processor_id();
    rq = cpu_rq(cpu);
    prev = rq->curr;
    
    // 1. 检查前一个进程的状态
    if (prev->__state != TASK_RUNNING) {
        // 2. 如果不在运行状态,从运行队列移除
        deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
    }
    
    // 3. 选择下一个进程
    next = pick_next_task(rq, prev, &rf);
    
    // 4. 如果切换进程
    if (prev != next) {
        // 5. 执行上下文切换
        rq = context_switch(rq, prev, next, &rf);
    }
}

关键点

  • 调度器只调度 TASK_RUNNING 状态的进程
  • TASK_UNINTERRUPTIBLE 状态的进程不会被调度
  • 冻结的进程处于 TASK_UNINTERRUPTIBLE 状态,不会被调度

16. 信号处理的底层机制

16.1 信号发送的完整流程

fake_signal_wake_up 的详细实现

// kernel/freezer.c
static void fake_signal_wake_up(struct task_struct *p)
{
    unsigned long flags;
    
    // 1. 获取信号处理锁
    if (lock_task_sighand(p, &flags)) {
        // 2. 唤醒进程
        signal_wake_up(p, 0);
        // 注意:第二个参数是 0,表示不发送真正的信号
        // 只是设置 TIF_SIGPENDING 标志
        
        // 3. 释放信号处理锁
        unlock_task_sighand(p, &flags);
    }
}

signal_wake_up 的详细实现

// kernel/signal.c
void signal_wake_up(struct task_struct *t, bool resume)
{
    unsigned int mask;
    
    // 1. 设置 TIF_SIGPENDING 标志
    set_tsk_thread_flag(t, TIF_SIGPENDING);
    
    // 2. 如果进程在睡眠,唤醒它
    mask = TASK_INTERRUPTIBLE;
    if (resume)
        mask |= TASK_WAKEKILL;
    
    // 3. 唤醒进程(如果进程在 TASK_INTERRUPTIBLE 状态)
    if (wake_up_state(t, mask))
        kick_process(t);
}

关键点

  • fake_signal_wake_up() 不发送真正的信号
  • 它只设置 TIF_SIGPENDING 标志
  • 这个标志会在系统调用返回时被检查
  • 如果进程在 TASK_INTERRUPTIBLE 状态,会被唤醒

16.2 TIF_SIGPENDING 标志的设置和检查

标志设置的汇编实现

// arch/arm64/include/asm/thread_info.h
.macro set_tsk_thread_flag, flag
    // 1. 获取当前进程的 thread_info
    mrs x0, sp_el0
    mov x1, #THREAD_SIZE
    sub x0, x0, x1
    add x0, x0, #THREAD_INFO_FLAGS
    
    // 2. 设置标志位
    ldr x1, [x0]
    orr x1, x1, #(1 << \flag)
    str x1, [x0]
    
    // 3. 内存屏障,确保标志写入完成
    dmb ish
.endm

标志检查的汇编实现

// arch/arm64/kernel/entry.S
.macro test_thread_flag, flag
    // 1. 获取当前进程的 thread_info
    mrs x0, sp_el0
    mov x1, #THREAD_SIZE
    sub x0, x0, x1
    add x0, x0, #THREAD_INFO_FLAGS
    
    // 2. 读取标志位
    ldr x1, [x0]
    tst x1, #(1 << \flag)
    // 如果标志设置,Z 标志位为 0
    // 如果标志未设置,Z 标志位为 1
.endm

16.3 信号处理的完整时序

信号处理的详细时序

fake_signal_wake_up(进程)
    ↓
lock_task_sighand(进程)
    ↓
signal_wake_up(进程, 0)
    ├─ set_tsk_thread_flag(进程, TIF_SIGPENDING)
    │   └─ 设置 thread_info->flags 的第 0 位
    └─ wake_up_state(进程, TASK_INTERRUPTIBLE)
        ├─ 如果进程在 TASK_INTERRUPTIBLE 状态
        │   ├─ 将进程状态改为 TASK_RUNNING
        │   ├─ 将进程加入运行队列
        │   └─ 触发调度
        └─ 如果进程不在 TASK_INTERRUPTIBLE 状态
            └─ 不做任何操作
    ↓
unlock_task_sighand(进程)
    ↓
进程被唤醒(如果之前在睡眠)
    ↓
进程继续执行或从系统调用返回
    ↓
ret_to_user(系统调用返回路径)
    ↓
检查 TIF_SIGPENDING 标志
    ↓
如果设置,调用 do_notify_resume()
    ↓
do_signal()
    ↓
检查冻结条件
    ↓
如果需要,调用 try_to_freeze()

17. 调度器选择下一个进程的详细机制

17.1 pick_next_task 的详细实现

CFS 调度器的进程选择

// kernel/sched/fair.c
static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;
    struct task_struct *p;
    
    // 1. 如果 CFS 运行队列为空,返回 NULL
    if (!cfs_rq->nr_running)
        return NULL;
    
    // 2. 选择 vruntime 最小的进程
    // vruntime 是虚拟运行时间,用于公平调度
    se = pick_next_entity(cfs_rq);
    
    // 3. 获取对应的 task_struct
    p = task_of(se);
    
    // 4. 更新统计信息
    update_stats_wait_end_fair(cfs_rq, se);
    
    return p;
}

pick_next_entity 的详细实现

// kernel/sched/fair.c
static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq)
{
    struct sched_entity *se = __pick_first_entity(cfs_rq);
    
    // 1. 从红黑树中选择 vruntime 最小的实体
    // CFS 使用红黑树维护进程的 vruntime 顺序
    // 最左边的节点是 vruntime 最小的进程
    
    // 2. 如果实体不在运行队列上,跳过
    if (!se || entity_is_task(se))
        return se;
    
    // 3. 如果是组调度,递归选择
    return pick_next_entity(cfs_rq->my_q, se);
}

关键点

  • 调度器只选择 TASK_RUNNING 状态的进程
  • 冻结的进程处于 TASK_UNINTERRUPTIBLE 状态,不会被选择
  • 进程的 vruntime 用于公平调度

17.2 进程从运行队列移除的机制

deactivate_task 的详细实现

// kernel/sched/core.c
static void deactivate_task(struct rq *rq, struct task_struct *p, int flags)
{
    // 1. 更新统计信息
    update_rq_clock(rq);
    
    // 2. 从运行队列移除
    dequeue_task(rq, p, flags);
    
    // 3. 更新负载
    update_load_avg(rq, p, 0);
    
    // 4. 如果进程不在任何运行队列上,更新状态
    if (!task_on_rq_queued(p)) {
        p->on_rq = 0;
    }
}

dequeue_task 的详细实现

// kernel/sched/core.c
void dequeue_task(struct rq *rq, struct task_struct *p, int flags)
{
    // 1. 更新统计信息
    update_rq_clock(rq);
    
    // 2. 调用调度类的 dequeue_task 方法
    p->sched_class->dequeue_task(rq, p, flags);
    
    // 3. 更新运行队列的进程计数
    rq->nr_running--;
}

CFS 调度器的 dequeue_task

// kernel/sched/fair.c
static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
    struct sched_entity *se = &p->se;
    struct cfs_rq *cfs_rq;
    
    // 1. 从 CFS 运行队列移除
    for_each_sched_entity(se) {
        cfs_rq = cfs_rq_of(se);
        dequeue_entity(cfs_rq, se, flags);
    }
}

dequeue_entity 的详细实现

// kernel/sched/fair.c
static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    // 1. 更新 vruntime
    update_curr(cfs_rq);
    
    // 2. 从红黑树移除
    __dequeue_entity(cfs_rq, se);
    
    // 3. 更新统计信息
    update_load_avg(cfs_rq, se, UPDATE_TG);
    
    // 4. 更新运行队列的进程计数
    cfs_rq->nr_running--;
}

关键点

  • 冻结的进程会被从运行队列移除
  • 进程的调度实体从红黑树中移除
  • 进程不再参与调度

18. 多核情况下的进程冻结

18.1 多核进程冻结的挑战

多核系统的特点

  • 每个 CPU 核心有独立的运行队列
  • 进程可以在不同 CPU 核心上运行
  • 需要协调所有 CPU 核心的冻结操作

冻结进程可能在不同 CPU 上

CPU 0:
├─ 进程 A (运行中)
├─ 进程 B (运行中)
└─ 进程 C (冻结中)

CPU 1:
├─ 进程 D (运行中)
├─ 进程 E (运行中)
└─ 进程 F (冻结中)

18.2 多核冻结的协调机制

freeze_task 的多核安全

// kernel/freezer.c
bool freeze_task(struct task_struct *p)
{
    unsigned long flags;
    bool ret = false;
    
    // 1. 获取 freezer_lock
    // 这个锁是全局的,保护所有 CPU 的冻结操作
    spin_lock_irqsave(&freezer_lock, flags);
    
    // 2. 检查是否应该跳过这个进程
    if (freezer_should_skip(p)) {
        spin_unlock_irqrestore(&freezer_lock, flags);
        return false;
    }
    
    // 3. 检查进程是否已经在冻结或已冻结
    if (!freezing(p) || frozen(p)) {
        spin_unlock_irqrestore(&freezer_lock, flags);
        return false;
    }
    
    // 4. 发送冻结信号
    if (!(p->flags & PF_KTHREAD)) {
        // 用户空间进程:发送"假信号"
        fake_signal_wake_up(p);
    } else {
        // 内核线程:直接唤醒
        wake_up_state(p, TASK_INTERRUPTIBLE);
    }
    
    spin_unlock_irqrestore(&freezer_lock, flags);
    return true;
}

关键点

  • freezer_lock 是全局锁,保护所有 CPU 的冻结操作
  • 使用 spin_lock_irqsave() 禁用中断,防止死锁
  • 进程可能在任何 CPU 上,但冻结操作是全局的

18.3 进程迁移对冻结的影响

进程可能在冻结过程中迁移

T=0s:  进程在 CPU 0 上运行
    ↓
T=1s:  调用 freeze_task(进程)
    ↓
T=1s:  设置 TIF_SIGPENDING 标志
    ↓
T=1s:  进程从 CPU 0 迁移到 CPU 1T=2s:  进程在 CPU 1 上检查 TIF_SIGPENDING 标志
    ↓
T=2s:  进程进入 __refrigerator()
    ↓
T=2s:  进程在 CPU 1 上被冻结

关键点

  • TIF_SIGPENDING 标志存储在 thread_info 中,跟随进程迁移
  • 进程可以在任何 CPU 上检查冻结条件
  • 冻结操作是进程级别的,不依赖于 CPU

19. 竞态条件的详细分析

19.1 冻结和解冻的竞态条件

可能的竞态条件

时间线:
T=0s:  进程 A 调用 freeze_task(进程 B)
    ↓
T=0s:  设置 TIF_SIGPENDING 标志
    ↓
T=1s:  进程 B 检查 TIF_SIGPENDING 标志
    ↓
T=1s:  进程 B 进入 __refrigerator()
    ↓
T=1s:  进程 B 设置 PF_FROZEN 标志
    ↓
T=2s:  进程 A 调用 __thaw_task(进程 B)
    ↓
T=2s:  清除 PF_FROZEN 标志
    ↓
T=2s:  唤醒进程 B
    ↓
T=2s:  进程 B 从 __refrigerator() 返回

保护机制

// kernel/freezer.c
bool __refrigerator(bool check_kthr_stop)
{
    bool was_frozen = false;
    unsigned int save = get_current_state();
    
    for (;;) {
        set_current_state(TASK_UNINTERRUPTIBLE);
        
        // 使用 freezer_lock 保护标志操作
        spin_lock_irq(&freezer_lock);
        current->flags |= PF_FROZEN;
        
        // 检查是否还需要冻结
        if (!freezing(current) ||
            (check_kthr_stop && kthread_should_stop()))
            current->flags &= ~PF_FROZEN;
        spin_unlock_irq(&freezer_lock);
        
        if (!(current->flags & PF_FROZEN)) {
            break;
        }
        
        was_frozen = true;
        schedule();
    }
    
    set_current_state(save);
    return was_frozen;
}

关键点

  • freezer_lock 保护 PF_FROZEN 标志的读写
  • 使用 spin_lock_irq() 禁用中断,防止死锁
  • 检查和解冻操作是原子的

19.2 系统调用返回的竞态条件

可能的竞态条件

时间线:
T=0s:  进程调用 epoll_wait(epfd, events, 10, 30000)
    ↓
T=0s:  进入内核,开始等待
    ↓
T=5s:  系统尝试冻结,调用 freeze_task(进程)
    ↓
T=5s:  设置 TIF_SIGPENDING 标志
    ↓
T=5s:  唤醒进程(如果进程在睡眠)
    ↓
T=5s:  进程继续等待(因为 epoll_wait 还没超时)
    ↓
T=30s: epoll_wait 超时返回
    ↓
T=30s: 检查 TIF_SIGPENDING 标志
    ↓
T=30s: 进入 __refrigerator()

问题

  • 进程在系统调用中长时间阻塞,无法及时响应冻结请求
  • 这是为什么需要短超时的原因

解决方案

// 修复后的代码
int main() {
    int epfd = epoll_create1(0);
    struct epoll_event events[10];
    
    while (1) {
        // 使用短超时(1 秒)
        int nfds = epoll_wait(epfd, events, 10, 1000);
        
        if (nfds > 0) {
            // 处理事件
            handle_events(events, nfds);
        } else if (nfds == 0) {
            // 超时,检查是否需要退出
            if (should_exit()) {
                break;
            }
        } else {
            // 错误处理
            if (errno == EINTR) {
                // 被信号中断,继续循环
                continue;
            }
        }
    }
}

20. 错误处理和恢复机制

20.1 冻结失败的检测

try_to_freeze_tasks 的超时检测

// kernel/power/process.c
static int try_to_freeze_tasks(bool user_only)
{
    struct task_struct *g, *p;
    unsigned long end_time;
    unsigned int todo;
    bool wq_busy = false;
    struct timeval start, end;
    u64 elapsed_msecs64;
    unsigned int elapsed_msecs;
    bool wakeup = false;
    int sleep_usecs = USEC_PER_MSEC;
    
    // 1. 记录开始时间
    do_gettimeofday(&start);
    end_time = jiffies + msecs_to_jiffies(freeze_timeout_msecs);
    // freeze_timeout_msecs 默认是 20000 毫秒(20 秒)
    
    // 2. 循环尝试冻结所有任务
    while (true) {
        todo = 0;
        read_lock(&tasklist_lock);
        
        // 3. 遍历所有进程
        for_each_process_thread(g, p) {
            // 跳过不可冻结的进程
            if (freezer_should_skip(p))
                continue;
            
            // 4. 尝试冻结这个进程
            if (!freeze_task(p))
                todo++;
        }
        
        read_unlock(&tasklist_lock);
        
        // 5. 检查工作队列
        if (!wq_busy) {
            wq_busy = freeze_workqueues_busy();
            todo += wq_busy;
        }
        
        // 6. 检查是否所有进程都已冻结
        if (!todo || time_after(jiffies, end_time))
            break;
        
        // 7. 等待一段时间后重试
        usleep_range(sleep_usecs, sleep_usecs + sleep_usecs / 2);
        sleep_usecs = min_t(unsigned int, sleep_usecs * 2, 100 * USEC_PER_MSEC);
    }
    
    // 8. 检查结果
    do_gettimeofday(&end);
    elapsed_msecs64 = timeval_to_ns(&end) - timeval_to_ns(&start);
    elapsed_msecs = elapsed_msecs64 / NSEC_PER_MSEC;
    
    if (todo) {
        // 有进程无法冻结
        pr_err("Freezing of tasks failed after %d.%03d seconds "
               "(%d tasks refusing to freeze, wq_busy=%d):\n",
               elapsed_msecs / 1000, elapsed_msecs % 1000,
               todo - wq_busy, wq_busy);
        
        // 打印无法冻结的进程信息
        show_freezing_tasks();
        
        return -EBUSY;
    }
    
    return 0;
}

20.2 冻结失败后的恢复

freeze_processes 的错误处理

// kernel/power/process.c
int freeze_processes(void)
{
    int error;
    int oom_kills_saved;
    
    // 1. 记录当前的 OOM kill 计数
    oom_kills_saved = oom_kills_count();
    
    // 2. 设置全局冻结标志
    error = __usermodehelper_disable(UMH_FREEZING);
    if (error)
        return error;
    
    // 3. 开始冻结用户空间进程
    if (!pm_freezing)
        atomic_inc(&system_freezing_cnt);
    pm_freezing = true;
    
    // 4. 尝试冻结所有任务
    error = try_to_freeze_tasks(true);
    
    // 5. 检查是否有进程无法冻结
    if (!error && !oom_kills_saved && oom_kills_count() != oom_kills_saved)
        error = -EBUSY;
    
    if (error) {
        // 冻结失败,恢复状态
        thaw_processes();
    } else {
        // 冻结成功
        pr_info("Freezing user space processes ... (elapsed %d.%03d seconds) done.\n",
                elapsed_msecs / 1000, elapsed_msecs % 1000);
    }
    
    return error;
}

thaw_processes 的恢复机制

// kernel/power/process.c
void thaw_processes(void)
{
    struct task_struct *g, *p;
    
    pr_info("Restarting tasks ... ");
    
    // 1. 清除全局冻结标志
    __usermodehelper_set_disable_depth(UMH_FREEZING);
    atomic_dec(&system_freezing_cnt);
    pm_freezing = false;
    pm_nosig_freezing = false;
    
    // 2. 唤醒所有冻结的进程
    read_lock(&tasklist_lock);
    for_each_process_thread(g, p) {
        if (frozen(p)) {
            __thaw_task(p);
        }
    }
    read_unlock(&tasklist_lock);
    
    // 3. 恢复用户空间辅助程序
    __usermodehelper_set_disable_depth(UMH_DISABLED);
    
    // 4. 触发调度,让进程恢复运行
    schedule();
    
    pr_cont("done.\n");
}

21. 总结和最佳实践

21.1 进程冻结的完整流程总结

冻结流程的完整时序

1. 用户空间触发挂起
   echo mem > /sys/power/state
    ↓
2. 内核调用 freeze_processes()
    ↓
3. 设置全局冻结标志
   pm_freezing = true
   atomic_inc(&system_freezing_cnt)
    ↓
4. 遍历所有进程
   for_each_process_thread()
    ↓
5. 对每个进程调用 freeze_task()
    ├─ 检查是否应该跳过
    ├─ 设置 TIF_SIGPENDING 标志
    └─ 唤醒进程(如果进程在睡眠)
    ↓
6. 进程检查 TIF_SIGPENDING 标志
   (在系统调用返回时)
    ↓
7. 进程调用 try_to_freeze()
    ↓
8. 进程进入 __refrigerator()
    ├─ 设置进程状态为 TASK_UNINTERRUPTIBLE
    ├─ 设置 PF_FROZEN 标志
    └─ 调用 schedule()
        ↓
9. 调度器保存进程状态
    ├─ 保存 CPU 寄存器到 thread_struct
    ├─ 保存浮点寄存器到 fpsimd_state
    └─ 进程从运行队列移除
    ↓
10. 进程成功冻结
    所有状态都保留在 RAM 中

解冻流程的完整时序

1. 系统从挂起状态恢复
    ↓
2. 内核调用 thaw_processes()
    ↓
3. 清除全局冻结标志
   pm_freezing = false
   atomic_dec(&system_freezing_cnt)
    ↓
4. 遍历所有冻结的进程
   for_each_process_thread()
    ↓
5. 对每个冻结的进程调用 __thaw_task()
    ├─ 清除 PF_FROZEN 标志
    └─ 唤醒进程
    ↓
6. 调度器选择进程运行
    ↓
7. 调度器恢复进程状态
    ├─ 从 thread_struct 恢复 CPU 寄存器
    ├─ 从 fpsimd_state 恢复浮点寄存器
    └─ 进程加入运行队列
    ↓
8. 进程从 schedule() 返回
    ↓
9. 进程退出 __refrigerator()
    ↓
10. 进程继续正常执行

21.2 关键点总结

  1. 状态保存的位置

    • CPU 寄存器 → task_struct.thread.cpu_context(在 RAM 中)
    • 浮点寄存器 → task_struct.thread.fpsimd_state(在 RAM 中)
    • 内存内容 → RAM(保持供电)
    • 文件描述符 → task_struct.files(在 RAM 中)
    • 信号队列 → task_struct.signal(在 RAM 中)
  2. 状态保存的时机

    • 进程调用 schedule()
    • 调度器自动保存所有寄存器
    • 不需要手动保存或恢复
  3. 冻结检查的时机

    • 系统调用返回时
    • 从睡眠状态唤醒时
    • 显式调用 try_to_freeze()
  4. 为什么需要短超时

    • 进程只有在系统调用返回时才能检查冻结条件
    • 如果系统调用长时间阻塞,进程无法及时响应冻结请求
    • 使用短超时(1-5 秒)可以确保进程及时响应

21.3 最佳实践

  1. 用户空间代码

    • 使用短超时(1-5 秒)进行阻塞系统调用
    • 正确处理 EINTR 错误
    • 在循环中检查退出条件
  2. 内核代码

    • 正确实现 PM 回调
    • 正确管理唤醒源
    • 优化 suspend/resume 时间
  3. 调试和排查

    • 使用调试工具查看日志
    • 检查无法冻结的进程
    • 使用 ftrace 跟踪冻结流程