Linux 进程冻结机制原理详解

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 结构,包含了进程的所有信息:

c 复制代码
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 寄存器都保存在这里:

c 复制代码
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 - 内存映射

c 复制代码
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 冻结流程的入口

c 复制代码
// 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 - 冻结所有任务

c 复制代码
// 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 - 向单个进程发送冻结信号

c 复制代码
// 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 - 发送"假信号"

c 复制代码
// 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 系统调用返回时的检查

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

c 复制代码
// 用户空间代码
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 - 检查并冻结

c 复制代码
// 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 - 检查是否需要冻结

c 复制代码
// 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 完整代码分析

c 复制代码
// 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

c 复制代码
set_current_state(TASK_UNINTERRUPTIBLE);

作用

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

状态转换

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

步骤 2:设置 PF_FROZEN 标志

c 复制代码
spin_lock_irq(&freezer_lock);
current->flags |= PF_FROZEN;

作用

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

标志位

c 复制代码
#define PF_FROZEN 0x00000040  // 第 6 位

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

c 复制代码
schedule();  // 进程在这里被调度出去

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

schedule() 被调用时:

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

详细过程

c 复制代码
// 调度器代码(简化版)
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:进程被唤醒

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

c 复制代码
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 架构示例

c 复制代码
// 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 寄存器
    // ...
};

保存过程(汇编代码)

assembly 复制代码
// 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

恢复过程(汇编代码)

assembly 复制代码
// 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 浮点寄存器保存

c 复制代码
// 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 内存布局

进程的内存空间包括:

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

7.2 内存映射信息

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

c 复制代码
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 中!

css 复制代码
┌─────────────────────────────────────┐
│          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

c 复制代码
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 文件状态

每个打开的文件都有:

c 复制代码
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 信号结构

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

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

9.2 待处理的信号

c 复制代码
struct sigqueue {
    struct list_head list;      // 链表节点
    siginfo_t info;             // 信号信息
    // ...
};

关键点

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

9.3 定时器

c 复制代码
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 - 解冻所有进程

c 复制代码
// 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 - 解冻单个进程

c 复制代码
// 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 场景设置

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

c 复制代码
// 用户空间代码
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 冻结过程的时间线

ini 复制代码
T=0s:   进程调用 epoll_wait(epfd, events, 10, 30000)
        ↓
T=0s:   进入内核,开始等待事件或超时
        ↓
T=0s:   进程进入 TASK_INTERRUPTIBLE 状态
        ↓
T=5s:   系统尝试挂起,调用 freeze_processes()
        ↓
T=5s:   设置 pm_freezing = true
        ↓
T=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) == true
        ↓
T=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 修复后的代码

c 复制代码
// 修复后的代码
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 修复后的冻结过程

ini 复制代码
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 = true
        ↓
T=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) == true
        ↓
T=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(部分)

c 复制代码
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)

c 复制代码
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(部分)

c 复制代码
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;
    // ...
};

文档结束

相关推荐
Shawn_CH6 小时前
epoll_wait 及相关函数原理详解
嵌入式
黑客思维者2 天前
XGW-9000系列高端新能源电站边缘网关硬件架构设计
网络·架构·硬件架构·嵌入式·新能源·计算机硬件·电站
神圣的大喵2 天前
平台无关的嵌入式通用按键管理器
c语言·单片机·嵌入式硬件·嵌入式·按键库
网易独家音乐人Mike Zhou2 天前
【嵌入式模块芯片开发】LP87524电源PMIC芯片配置流程,给雷达供电的延时上电时序及API函数
c语言·stm32·单片机·51单片机·嵌入式·电源·毫米波雷达
Nerd Nirvana2 天前
WSL——Windows Subsystem for Linux流程一览
linux·运维·服务器·windows·嵌入式·wsl·wsl2
rechol2 天前
mcu启动流程
stm32·单片机·mcu·嵌入式
MounRiver_Studio3 天前
RISC-V IDE MRS2使用笔记(七):书签与笔记功能
ide·嵌入式·risc-v
切糕师学AI3 天前
ARM 架构中的 PRIMASK、FAULTMAST、BASEPRI 寄存器
arm开发·架构·嵌入式·寄存器
迷人的星空4 天前
嵌入式软件调试指南:一看就懂,上手就用
嵌入式