04-进程调度与中断管理.md 30 KB


title: 进程调度与中断管理 tags: [Linux内核, 进程调度, 中断管理, 嵌入式, CFS, tasklet] created: 2026-09-16

updated: 2026-09-17

进程调度与中断管理

关联知识: [[01-Cortex-A7架构详解|Cortex-A7架构详解]] · [[freertos-task-scheduling|FreeRTOS任务调度]] · [[freertos-interrupt-time|FreeRTOS中断管理]]


一、进程管理概述

1.1 进程 vs 线程

Linux 中进程和线程本质上都由 task_struct 描述,区别在于共享资源的程度:

特性 进程 (Process) 线程 (Thread)
地址空间 独立的 mm_struct 共享所属进程的地址空间
资源开销 fork 时需复制页表,代价较高 clone 时仅分配栈和 TLS,轻量
通信方式 管道、共享内存、信号等 直接读写共享变量
内核视角 都是 task_struct,通过 CLONE 标志区分 同左,线程本质是轻量级进程 (LWP)

嵌入式场景中,多线程比多进程更常见——内存受限、切换开销敏感。

1.2 task_struct 结构体

task_struct 是 Linux 内核中最大的数据结构之一(约 6KB,视配置而定),定义在 include/linux/sched.h。核心字段分组:

struct task_struct {
    /* --- 标识 --- */
    pid_t pid;                  // 进程 ID (全局唯一)
    pid_t tgid;                 // 线程组 ID (= 主线程 PID)
    char comm[TASK_COMM_LEN];   // 进程名称 (16 字节)

    /* --- 状态 --- */
    volatile long state;        // 进程状态 (TASK_RUNNING 等)
    int exit_state;             // 退出状态 (EXIT_ZOMBIE / EXIT_DEAD)
    unsigned int flags;         // 进程标志 (PF_KTHREAD, PF_USED_MATH 等)

    /* --- 调度 --- */
    int prio;                   // 动态优先级 (0~139)
    int static_prio;            // 静态优先级 (由 nice 值映射)
    int normal_prio;            // 普通优先级
    unsigned int rt_priority;   // 实时优先级 (0~99)
    unsigned int policy;        // 调度策略 (SCHED_NORMAL / SCHED_FIFO 等)
    u64 sum_exec_runtime;       // 总执行时间 (纳秒)
    u64 vruntime;               // 虚拟运行时间 (CFS 核心)

    /* --- 内存 --- */
    struct mm_struct *mm;       // 用户空间内存描述符
    struct mm_struct *active_mm; // 当前活跃的内存描述符

    /* --- 栈 --- */
    void *stack;                // 内核栈指针 (通常 8KB / 16KB)

    /* --- 家族 --- */
    struct task_struct *parent; // 父进程
    struct list_head children;  // 子进程链表
    struct task_struct *group_leader; // 线程组领头进程

    /* --- 文件与信号 --- */
    struct fs_struct *fs;       // 当前工作目录
    struct files_struct *files;// 打开的文件表
    struct signal_struct *sig; // 信号信息
};

1.3 进程状态

                fork()
    ┌──────────────────────────┐
    │        TASK_RUNNING      │◄─────── 被调度器选中
    │    (就绪 / 正在运行)      │
    └────┬──────────────┬──────┘
         │              │
    schedule()     被唤醒 / 时间片到
         │              │
         ▼              │
  ┌─────────────┐       │
  │ 可中断睡眠    │───────┘
  │ TASK_INTERRUPTIBLE │  (等待事件,可被信号唤醒)
  └──────┬──────┘
         │ 不可被信号唤醒
         ▼
  ┌──────────────────┐
  │ 不可中断睡眠       │  (等待 I/O,不可被任何信号唤醒)
  │ TASK_UNINTERRUPTIBLE│
  └──────┬───────────┘
         │
         ▼
  ┌─────────────┐    wait()    ┌─────────────┐
  │  EXIT_ZOMBIE │ ──────────► │  EXIT_DEAD   │
  │  (僵尸进程)   │   释放资源    │  (彻底消失)   │
  └─────────────┘              └─────────────┘

  TASK_STOPPED  — 被 SIGSTOP 暂停 (调试 / Ctrl+Z)
  TASK_TRACED   — 被 ptrace 跟踪

嵌入式注意: 进程陷入 TASK_UNINTERRUPTIBLE 不响应任何信号,若出现在驱动 I/O 路径中可能导致系统"假死"。排查时检查 D 状态进程。


二、进程调度

2.1 调度策略总览

策略 常量 适用场景 调度类
普通分时 SCHED_NORMAL 一般用户进程 CFS
批处理 SCHED_BATCH 吞吐量敏感的后台任务 CFS
空闲 SCHED_IDLE 极低优先级,仅系统空闲时运行 CFS
先来先服务 SCHED_FIFO 硬实时任务(无时间片,主动让出) RT
时间片轮转 SCHED_RR 硬实时任务(同优先级间轮转) RT

调度类继承链: stop_sched_classdl_sched_classrt_sched_classfair_sched_classidle_sched_class。高优先级类的进程始终优先于低优先级类。

2.2 CFS 完全公平调度器

CFS 是 Linux 默认的普通进程调度器(SCHED_NORMAL / SCHED_BATCH),核心思想:用虚拟运行时间 (vruntime) 衡量每个进程的"公平份额",始终调度 vruntime 最小的进程

2.2.1 虚拟运行时间

实际运行时间 × (NICE_0_WEIGHT / 该进程权重) = vruntime 增量
  • nice 值越低 → 权重越高 → vruntime 增长越慢 → 获得更多 CPU 时间
  • nice -20 的进程权重是 nice 19 的约 88 倍
  • 权重通过 sched_prio_to_weight[] 数组映射,共 40 级(对应 nice -20 ~ 19)

权重表(部分):

nice 值 权重 nice 值 权重
-20 88761 0 1024
-10 33554 10 256
-5 52429 19 15

2.2.2 红黑树组织

CFS 使用红黑树按 vruntime 排序所有可运行进程:

              [vruntime=500]
             /             \
     [vruntime=300]    [vruntime=700]
       /       \           /       \
  [200]     [400]     [600]     [800]
  • 最左节点始终是 vruntime 最小的进程,调度器直接取左叶子
  • 插入/删除/查找最左节点均为 O(log n),支持数千进程不退化
  • vruntime 使用 u64 存储,溢出时通过 calc_delta_mine() 重新计算

2.2.3 CFS 调度周期

调度周期 (sysctl_sched_latency) = 6ms  (默认)
最小粒度 (sysctl_sched_min_granularity) = 0.75ms

每个进程分配的时间片 = max(调度周期 / 可运行进程数, 最小粒度)

当进程数超过 8 时,6ms / 8 = 0.75ms 触发最小粒度限制,此时 CFS 退化为近似轮转。

2.2.4 睡眠公平性

进程睡眠醒来后,其 vruntime 可能远小于当前树中其他进程的 vruntime,导致它独占 CPU。CFS 的处理:

/* kernel/sched/fair.c: place_entity() */
if (sleeping) {
    /* 将新唤醒进程的 vruntime 设为树中最小值减去偏移 */
    se->vruntime = avg_vruntime() - sysctl_sched_latency;
}

这确保睡眠进程获得合理的时间份额,但不会"抢夺"过多 CPU。

2.3 优先级体系

Linux 优先级分三层,数值越小优先级越高:

实时优先级 (0~99)    ← SCHED_FIFO / SCHED_RR 使用
    ↓
普通优先级 (100~139) ← SCHED_NORMAL / SCHED_BATCH 使用
    ↓
                     nice 值 -20 ~ +19 对应 100~139

优先级换算关系:

prio (动态优先级) = max(普通优先级, 实时优先级)
rt_priority (用户态) = 99 - prio (实时进程时)
nice = prio - 120  (普通进程时)

nice 值是用户态接口,范围 -20(最高)到 +19(最低),每级差 1 个权重等级。

2.4 时间片与调度延迟

参数 默认值 说明
sysctl_sched_latency 6ms 所有进程至少运行一遍的目标周期
sysctl_sched_min_granularity 0.75ms 每个进程的最小运行时间片
sysctl_sched_wakeup_granularity 1ms 唤醒抢占粒度
sched_nr_latency 8 调度周期内可轮转的最大进程数

实时进程使用固定时间片(FIFO 不用时间片),优先级 0~99,通过 SCHED_FIFO / SCHED_RR 设置。实时进程始终优先于 CFS 进程。

2.5 调度类

Linux 内核通过调度类实现可插拔调度策略:

struct sched_class {
    const struct sched_class *next;   // 下一级调度类
    void (*enqueue_task)(...);        // 入队
    void (*dequeue_task)(...);        // 出队
    void (*pick_next_task)(...);      // 选择下一个进程
    void (*put_prev_task)(...);       // 放回上一个进程
    int  (*check_preempt_curr)(...);  // 检查是否可抢占
    /* ... */
};

五级调度类从高到低:

  1. stop_sched_class — 最高优先级,用于 CPU 热插拔、迁移
  2. dl_sched_class — Deadline 调度,SCHED_DEADLINE
  3. rt_sched_class — 实时调度,SCHED_FIFO / SCHED_RR
  4. fair_sched_class — CFS 公平调度,SCHED_NORMAL / SCHED_BATCH
  5. idle_sched_class — 空闲任务,每个 CPU 一个 idle 线程

pick_next_task() 从最高级调度类开始遍历,直到找到可运行的进程。这就是为什么实时进程总是抢占普通进程。


三、进程创建与退出

3.1 fork / vfork / clone

系统调用 共享资源 典型用途
fork() 无(写时复制 COW) 创建独立子进程
vfork() 地址空间 + 栈 + 文件描述符 fork 后立即 exec,避免复制
clone() 通过 flags 精确控制 创建线程、容器、子进程

clone() 的 flags 控制共享哪些资源:

#define CLONE_VM       0x00000100   // 共享内存空间(线程)
#define CLONE_FS       0x00000200   // 共享文件系统信息
#define CLONE_FILES    0x00000400   // 共享文件描述符表
#define CLONE_SIGHAND  0x00000800   // 共享信号处理
#define CLONE_THREAD   0x00010000   // 线程组(同一线程组内共享)

glibc 的 pthread_create() 底层调用 clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, ...)

3.2 do_fork / kernel_clone

内核统一入口是 kernel_clone()(旧版为 do_fork()),流程:

用户态 fork()/clone()
        │
        ▼
  kernel_clone()
        │
        ├── copy_process()          // 核心:复制进程描述符
        │   ├── dup_task_struct()   // 复制 task_struct + 内核栈
        │   ├── copy_creds()        // 复制权限信息
        │   ├── copy_mm()           // 复制内存映射 (COW)
        │   ├── copy_files()        // 复制文件描述符
        │   ├── copy_fs()           // 复制文件系统
        │   ├── copy_sighand()      // 复制信号处理
        │   ├── copy_signal()       // 复制信号信息
        │   └── sched_fork()        // 初始化调度相关字段
        │
        └── wake_up_new_task()      // 将子进程加入调度器

写时复制 (COW)

/* fork 时不复制物理页面,仅标记页表为只读 */
/* 子进程或父进程写入时触发缺页中断 */
/* 缺页处理函数为该页分配新的物理帧并复制内容 */

static int copy_page_range(...) {
    /* 仅复制页表项,设置 COW 标志 */
   pte_t *src_pte = pte_offset_map(...);
if (pte_present(*src_pte)) {
        pte_set_wrprotect(src_pte);  // 父进程页表改为只读
        copy_pte(dst, src_pte);      // 子进程映射同一页,只读
}
}

嵌入式优化: COW 避免了 fork 的物理内存复制开销。对于内存 < 256MB 的嵌入式系统,频繁 fork 仍需谨慎——页表复制本身也有开销。

3.3 进程退出

进程退出分两个阶段:

exit() / _exit()
      │
      ▼
  do_exit()
      ├── 释放当前线程持有的资源
      ├── 设置 PF_EXITING 标志
      ├── exit_mm()          // 释放内存映射
      ├── exit_files()       // 关闭文件描述符
      ├── exit_fs()          // 释放文件系统
      ├── exit_signal()      // 发送 SIGCHLD 给父进程
      ├── exit_state = EXIT_ZOMBIE  // 变为僵尸
      └── schedule()         // 最后一次调度
            │
            ▼
  父进程 wait() / waitpid()
      │
      ▼
  release_task()
      ├── 释放 task_struct
      ├── 释放内核栈
      └── 从进程表中移除

僵尸进程: 进程已退出但父进程尚未 wait() 回收,其 task_struct 仍占用内核内存。嵌入式系统若不 wait() 子进程,会导致进程表泄漏。


四、中断管理

4.1 中断向量表 (ARM Cortex-A7)

ARM Cortex-A7 使用异常向量表(固定地址 0xFFFF00000x00000000,由 VBAR 寄存器配置):

/* arch/arm/kernel/entry-armv.S */
    .section .vectors, "ax", %progbits
.Lvectors:
    W(b)   vector_swi        /* 0x00: SVC 异常 (系统调用) */
    W(b)   vector_und        /* 0x04: 未定义指令 */
    W(nop)                   /* 0x08: 预留 */
    W(b)   vector_pabt       /* 0x0C: 指令预取中止 */
    W(b)   vector_dabt       /* 0x10: 数据访问中止 */
    nop                      /* 0x14: 预留 */
    W(b)   vector_irq        /* 0x18: IRQ 中断 */
    W(b)   vector_fiq        /* 0x1C: FIQ 快速中断 */

Cortex-A7 有 8 个异常入口,IRQ 占一个。中断号通过 GIC (Generic Interrupt Controller) 确定,而非向量表偏移。

4.2 GIC 中断控制器

GIC (Generic Interrupt Controller) 是 ARM 的标准中断控制器,GIC-400 支持最多 1020 个 SPI 中断:

┌─────────────────────────────────────────────┐
│                  GIC-400                     │
│  ┌──────────────┐  ┌──────────────────────┐ │
│  │  Distributor  │  │   CPU Interface      │ │
│  │  (分发器)     │──│   (CPU 接口)         │ │
│  │              │  │                      │ │
│  │  GICD_ISENABLER  │  ICC_IAR  (中断确认) │ │
│  │  GICD_ICENABLER  │  ICC_EOIR (中断结束) │ │
│  │  GICD_IPRIORITYR  │  ICC_PMR  (优先级) │ │
│  │  GICD_ITARGETSR   │  ICC_CTLR (控制)   │ │
│  │  GICD_ICFGR      │                    │ │
│  └──────────────┘  └──────────────────────┘ │
└─────────────────────────────────────────────┘
         │                    │
    IRQ 线连接           CPU 接口连接
    (SPI / PPI / SGI)   (CPU0 / CPU1 / ...)

中断类型:

类型 编号范围 说明 典型
SGI 0~15 软件触发中断 (核间通信) IPI
PPI 16~31 私有外设中断 (每核独有) 定时器
SPI 32~1020 共享外设中断 UART、GPIO、DMA

4.3 中断处理流程

硬件中断触发 (IRQ)
      │
      ▼
CPU 保存现场 (R0-R15, CPSR → SPSR_irq)
      │
      ▼
切换到 IRQ 模式,跳转 vector_irq
      │
      ▼
gic_handle_irq()
      ├── 读取 ICC_IAR 获取中断号
      ├── 查找注册的 irq_desc
      │
      ▼
handle_irq_event()           ← 上半部 (Hardirq)
      │   ├── 执行注册的 irq_handler
      │   ├── 处理紧急事务 (读/清中断标志)
      │   └── 调度下半部 (如需)
      │
      ▼
写 ICC_EOIR 通知 GIC 中断结束
      │
      ▼
检查 softirq_pending 标志
      │
      ▼ (中断返回前)
  __do_softirq()             ← 下半部 (Softirq)
      │   ├── 执行已注册的 softirq
      │   ├── 执行 tasklet
      │   └── 时间片用完则在 softirqd 线程中执行
      │
      ▼
恢复现场 (SPSR_irq → CPSR, R0-R15 恢复)
      │
      ▼
返回被中断的进程/线程

4.4 中断 API

/* 申请中断 — 成功返回 0 */
int request_irq(unsigned int irq,
                irq_handler_t handler,       // 中断处理函数
                unsigned long flags,          // 标志
                const char *name,            // /proc/interrupts 中显示的名字
                void *dev);                  // 传给 handler 的 cookie

/* 释放中断 — 返回注册时的 handler */
void *free_irq(unsigned int irq, void *dev);

/* 中断标志 */
#define IRQF_SHARED        0x00000800   // 共享中断线
#define IRQF_ONESHOT       0x00002000   // 中断线在下半部完成前保持屏蔽
#define IRQF_NO_THREAD     0x00008000   // 不创建线程化的中断处理
#define IRQF_NO_SUSPEND    0x00004000   // 系统挂起时不屏蔽此中断

/* 禁止/使能中断 (本地 CPU) */
local_irq_disable();    // 关中断 (CPSR.I=1)
local_irq_enable();     // 开中断 (CPSR.I=0)

/* 保存并关中断 */
unsigned long flags;
local_irq_save(flags);  // 保存当前 CPSR 并关中断
local_irq_restore(flags); // 恢复 CPSR

/* 禁止/使能指定中断线 (其他 CPU 不受影响) */
disable_irq(irq);       // 同步等待正在执行的 handler 完成
disable_irq_nosync(irq); // 不等待,立即返回
enable_irq(irq);

disable_irq vs local_irq_disable: 前者仅屏蔽一条中断线,其他中断不受影响;后者屏蔽本 CPU 所有中断,应尽量少用。

4.5 中断上下半部

特性 上半部 (Hardirq) 下半部 (Softirq / tasklet / workqueue)
执行环境 中断上下文,不可睡眠 Softirq/tasklet: 不可睡眠;workqueue: 可睡眠
延迟要求 极短 (< 100μs) 可较长
并发性 仅在触发的 CPU 上运行 Softirq: 所有 CPU 可同时运行;tasklet: 同一 tasklet 不并发
典型操作 读硬件寄存器、清中断、唤醒下半部 数据拷贝、协议处理、延迟工作

五、软中断与 tasklet

5.1 软中断 (Softirq)

软中断是 Linux 内核的延迟处理机制,编译时静态定义,运行在中断返回路径中:

/* 定义的软中断类型 (include/linux/interrupt.h) */
enum {
    HI_SOFTIRQ = 0,        // 高优先级 tasklet
    TIMER_SOFTIRQ,          // 定时器
    NET_TX_SOFTIRQ,         // 网络发送
    NET_RX_SOFTIRQ,         // 网络接收
    BLOCK_SOFTIRQ,          // 块设备
    IRQ_POLL_SOFTIRQ,       // 中断轮询
    TASKLET_SOFTIRQ,        // tasklet
    SCHED_SOFTIRQ,          // 调度器负载均衡
    HRTIMER_SOFTIRQ,        // 高精度定时器 (未使用)
    RCU_SOFTIRQ,            // RCU 回调
    NR_SOFTIRQS
};

/* 注册软中断处理函数 */
open_softirq(TIMER_SOFTIRQ, timer_softirq);

/* 触发软中断 */
raise_softirq(NET_TX_SOFTIRQ);

/* 软中断处理函数原型 */
void (*action)(struct softirq_action *);

软中断执行流程:

中断返回 / ksoftirqd 被唤醒
        │
        ▼
  __do_softirq()
        ├── 检查 softirq_pending 位图
        ├── 循环执行所有 pending 的 softirq
        │   ├── while ((h = softirq_vec[i]) && pending)
        │   │       h->action(h)  // 执行处理函数
        │   └── 清除 pending 位
        ├── 若执行时间超过 2ms 或仍有 pending → 唤醒 ksoftirqd
        └── 重新检查 pending(防止丢失)

ksoftirqd: 每个 CPU 有一个 ksoftirqd 内核线程 (优先级 nice 19)。当软中断处理耗时过长或持续有新 softirq,内核切换到 ksoftirqd 中执行,避免长时间关中断。

5.2 tasklet

tasklet 基于软中断实现,是驱动开发中最常用的下半部机制:

/* tasklet 结构 */
struct tasklet_struct {
    struct tasklet_struct *next;
    unsigned long state;        // TASKLET_STATE_SCHED / TASKLET_STATE_RUN
    atomic_t count;             // 引用计数 (0=使能, >0=禁用)
    void (*func)(unsigned long); // 处理函数
    unsigned long data;         // 参数
};

/* 静态声明 */
DECLARE_TASKLET(my_tasklet, my_tasklet_func, (unsigned long)&my_data);

/* 动态声明 */
struct tasklet_struct my_tasklet;
tasklet_init(&my_tasklet, my_tasklet_func, (unsigned long)&my_data);

/* 处理函数原型 */
void my_tasklet_func(unsigned long data) {
    /* 此处不可睡眠 */
}

/* 调度 tasklet (可在中断上下文调用) */
tasklet_schedule(&my_tasklet);

/* tasklet 使用示例 — 网卡驱动 */
static irqreturn_t nic_irq_handler(int irq, void *dev_id) {
    struct nic_device *ndev = dev_id;

    /* 上半部:仅读取中断状态并清中断 */
    u32 isr = readl(ndev->regs + ISR);
    writel(isr, ndev->regs + ISR);

    /* 将 ISR 传给下半部 */
    ndev->irq_status = isr;

    /* 调度 tasklet 处理后续工作 */
    tasklet_schedule(&ndev->tasklet);

    return IRQ_HANDLED;
}

static void nic_tasklet_handler(unsigned long data) {
    struct nic_device *ndev = (struct nic_device *)data;

    /* 下半部:处理接收数据包、更新统计等耗时操作 */
    if (ndev->irq_status & RX_INT)
        nic_rx_poll(ndev);
    if (ndev->irq_status & TX_INT)
        nic_tx_complete(ndev);
}

tasklet 特性: 同一 tasklet 实例不会在多个 CPU 上并发执行,但不同 tasklet 可以并行。count > 0 时 tasklet 被禁用。

5.3 工作队列 (Workqueue)

工作队列运行在内核线程上下文(worker 线程),可以睡眠,适合需要阻塞操作的延迟工作:

/* 静态声明工作项 */
static void my_work_func(struct work_struct *work);
static DECLARE_WORK(my_work, my_work_func);

/* 动态声明 */
struct work_struct my_work;
INIT_WORK(&my_work, my_work_func);

/* 工作处理函数 — 可以睡眠 */
static void my_work_func(struct work_struct *work) {
    /* 可以调用 msleep()、mutex_lock()、kmalloc(GFP_KERNEL) 等 */
    struct my_data *data = container_of(work, struct my_data, work);
    msleep(100);  // 完全合法
}

/* 调度工作 */
schedule_work(&my_work);

/* 取消工作 (等待执行完毕) */
cancel_work_sync(&my_work);

/* 延迟工作 — N jiffies 后执行 */
struct delayed_work my_dwork;
INIT_DELAYED_WORK(&my_dwork, my_work_func);
schedule_delayed_work(&my_dwork, HZ);  // 1 秒后执行
cancel_delayed_work_sync(&my_dwork);

5.4 三种下半部机制对比

机制 执行上下文 可睡眠 并发性 延迟 适用场景
softirq 中断上下文 所有 CPU 可并行 极短 网络、定时器等内核核心
tasklet 中断上下文 同一 tasklet 不并发 驱动中断下半部
workqueue 进程上下文 (worker 线程) 工作项串行 可长 需要阻塞的延迟工作

选型建议: 驱动开发首选 tasklet(简单安全);需要睡眠操作时用 workqueue;softirq 一般不直接使用(内核子系统专用)。


六、跨平台对比

6.1 IMX6ULL vs STM32 vs RK3568 调度差异

特性 IMX6ULL (Cortex-A7) STM32F4 (Cortex-M4) RK3568 (Cortex-A55)
调度器 Linux CFS FreeRTOS / 裸机 Linux CFS (支持 EAS)
核心数 单核 单核 四核
中断控制器 GIC-400 NVIC GIC-500
中断嵌套 支持 (IRQ) 支持 (NVIC) 支持 (IRQ)
实时性 有限 (可 PREEMPT_RT 补丁) 强 (确定性) 有限 (可 PREEMPT_RT)
调度延迟 数毫秒 微秒级 < 1ms (EAS 优化)
负载均衡 多核需配置 EAS 能效感知自动调度
能效管理 基本 DVFS 大小核异构 + OPP

RK3568 EAS: 能效感知调度 (Energy Aware Scheduling) 根据 CPU 负载动态在大核 (A55@2.0GHz) 和小核间迁移任务,平衡性能与功耗。

6.2 中断控制器对比

NVIC (STM32)              GIC (ARM Cortex-A)
├── 嵌套向量中断控制器      ├── 通用中断控制器
├── 最多 240 个中断         ├── 最多 1020+ 个中断
├── 每个中断独立优先级       ├── 分组优先级 + 丢弃优先级
├── 硬件自动压栈             ├── 软件保存/恢复
├── 无核间中断              ├── SGI 支持核间通信
└── 中断延迟 12 周期         └── 中断延迟约 30~50 周期

七、面试精选

题目 1:CFS 如何保证"公平"?为什么用红黑树而非链表?

考察点: 调度算法数据结构

参考答案:

CFS 通过虚拟运行时间 (vruntime) 量化每个进程的 CPU 使用份额。进程每次运行时,实际 CPU 时间按其权重换算为 vruntime 增量——权重高的进程 vruntime 增长慢,获得的 CPU 时间多。调度器始终选择 vruntime 最小的进程运行,确保所有可运行进程的 vruntime 趋于一致。

数据结构选择红黑树而非链表的原因:

  • 链表按 vruntime 排序插入为 O(n),取最小值 O(1);但每次调度都要更新顺序
  • 红黑树取最左节点 O(1)(缓存了 rb_leftmost 指针),插入/删除 O(log n)
  • 数千进程时红黑树性能远优于链表
  • 旧版 O(1) 调度器使用位图+优先级数组,无法保证公平性

题目 2:中断上半部和下半部的本质区别是什么?为什么不能全部放在上半部处理?

考察点: 中断处理架构

参考答案:

上半部运行在中断上下文,硬件中断触发后立即执行,此时:

  • 本 CPU 的所有中断被屏蔽(IRQ 模式下 CPSR.I=1)
  • 其他中断被阻塞,系统响应能力下降
  • 不可睡眠、不可调用可能阻塞的函数
  • 必须极短(通常 < 100μs),仅处理最紧急事务(读寄存器、清中断、调度下半部)

下半部运行在软中断或进程上下文,中断已开放:

  • 可以处理耗时操作(数据拷贝、协议栈处理)
  • 不阻塞其他中断响应
  • 三类:softirq(高并发但需静态定义)、tasklet(简单易用)、workqueue(可睡眠)

若全部放在上半部:中断关闭时间过长 → 丢失中断 → 系统不稳定。

题目 3:fork 之后子进程如何避免复制父进程的物理内存?

考察点: COW 机制

参考答案:

Linux 的 fork 使用写时复制 (Copy-On-Write):

  1. fork 时仅复制页表(虚拟 → 物理映射),物理页面标记为只读
  2. 父子进程共享同一物理页面
  3. 任一方写入时触发缺页异常 → 内核分配新物理帧并复制内容
  4. 之后各自拥有独立副本

这使 fork 的开销从"复制全部内存"降为"复制页表"。对于嵌入式系统(256MB RAM),页表复制约几十 KB,远优于完整复制。

题目 4:disable_irq(irq)local_irq_disable() 有什么区别?各自有什么风险?

考察点: 中断屏蔽策略

参考答案:

disable_irq(irq) local_irq_disable()
作用范围 仅屏蔽指定中断线 屏蔽本 CPU 所有中断
其他 CPU 不受影响 不影响其他 CPU
嵌套调用 可嵌套,引用计数 不可嵌套(会丢失之前的 flags)
典型场景 驱动防止中断重入 临界区保护

风险:

  • disable_irq: 若长时间不 enable_irq,其他共享该中断线的设备中断丢失
  • local_irq_disable: 关闭所有中断,系统无法响应时钟、网络等关键中断;嵌入式实时系统中必须限制关中断时间
  • 两者都不应长期持有,应尽快 restore/enable

题目 5:为什么 TASK_UNINTERRUPTIBLE 进程不响应信号?嵌入式系统中如何避免"假死"?

考察点: 进程状态与系统稳定性

参考答案:

TASK_UNINTERRUPTIBLE 的设计目的是等待不可中断的硬件操作完成(如磁盘 I/O)。内核在此状态下不检查信号,直到操作完成被 wake_up_process() 唤醒。这保证了硬件操作的原子性——若允许信号中断,可能导致数据不一致。

嵌入式系统中"假死"场景:

  • 驱动在 TASK_UNINTERRUPTIBLE 状态下等待硬件响应,但硬件异常未响应
  • NFS 挂载超时(默认 60~120 秒)
  • 内核死锁(如持有自旋锁时睡眠)

避免措施:

  1. 驱动中设置超时:wait_event_timeout()wait_for_completion_timeout() 替代无超时等待
  2. 检测系统 D 状态进程:ps -eo state,pid,cmd | grep D
  3. 内核配置 CONFIG_DETECT_HUNG_TASK 检测 D 状态超时
  4. 使用 TASK_KILLABLETASK_WAKEKILL | TASK_UNINTERRUPTIBLE),既不可中断又可被 SIGKILL 杀死

最后更新: 2026-09-17