--- title: 进程调度与中断管理 tags: [Linux内核, 进程调度, 中断管理, 嵌入式, CFS, tasklet] created: 2026-09-16 updated: 2026-09-17 --- # 进程调度与中断管理 > ⚠️ **来源说明**:本主题不属于《I.MX6U嵌入式Linux驱动开发指南》的讲授范围,内容基于 Linux 内核源码(Linux 4.1.15)整理,为扩展知识。 > **关联知识**: [[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 内核中最大的数据结构之一(32 位默认配置下通常 1~2KB,随内核选项浮动),定义在 `include/linux/sched.h`。核心字段分组: ```c 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 等) struct sched_entity se; // CFS 调度实体(vruntime 在这里) struct sched_rt_entity rt; // 实时调度实体 /* --- 内存 --- */ 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; // 信号信息 }; ``` > ⚠️ **易错点(内核源码级)**:`task_struct` 本身**没有** `vruntime`、`sum_exec_runtime` 字段——它们属于 `struct sched_entity`(`task_struct.se`):`se.vruntime`、`se.sum_exec_runtime`。`prio`/`static_prio`/`normal_prio`/`rt_priority`/`policy` 才是 `task_struct` 的直接字段。 ### 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_class` → `dl_sched_class` → `rt_sched_class` → `fair_sched_class` → `idle_sched_class`。高优先级类的进程始终优先于低优先级类。 ### 2.2 CFS 完全公平调度器 CFS 是 Linux 默认的普通进程调度器(`SCHED_NORMAL` / `SCHED_BATCH`),核心思想:**用虚拟运行时间 (vruntime) 衡量每个进程的"公平份额",始终调度 vruntime 最小的进程**。 > ⚠️ **来源说明**:以下 CFS 内容为内核源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码(`kernel/sched/fair.c`、`kernel/sched/sched.h`)整理。 #### 2.2.1 虚拟运行时间 ``` vruntime 增量 = 实际运行时间 × (NICE_0_LOAD / 该进程权重) ``` - **nice 值越低 → 权重越高 → vruntime 增长越慢 → 获得更多 CPU 时间** - nice -20 的权重是 nice 19 的约 5900 倍(`88761 / 15`) - 权重通过 `prio_to_weight[]` 数组映射,共 40 级(对应 nice -20 ~ 19),列于 `kernel/sched/sched.h` 权重表(部分,Linux 4.1.15): | nice 值 | 权重 | nice 值 | 权重 | | ------- | ----- | ------- | ---- | | -20 | 88761 | 0 | 1024 | | -10 | 9548 | 10 | 110 | | -5 | 3121 | 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_fair()` / `calc_delta_mine()` 负责把实际运行时间按权重换算为 vruntime 增量 > ⚠️ **版本差异**:Linux 4.1.15 的 CFS 运行队列用 `struct rb_root tasks_timeline` + `struct rb_node *rb_leftmost` 两个字段(`kernel/sched/sched.h`);把二者打包成 `rb_root_cached` 是 4.14+ 的改动。 #### 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 的处理: ```c /* kernel/sched/fair.c: place_entity()(Linux 4.1 实际实现) */ static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial) { u64 vruntime = cfs_rq->min_vruntime; if (initial) /* 新建进程 */ vruntime += sched_vslice(cfs_rq, se); if (!initial) { /* 唤醒进程:适度"追补"睡眠时间 */ unsigned long thresh = sysctl_sched_latency; if (sched_feat(GENTLE_FAIR_SLEEPERS)) thresh >>= 1; /* 减半,避免长期睡眠者抢占 CPU */ vruntime -= thresh; } se->vruntime = max_vruntime(se->vruntime, vruntime); } ``` 这确保睡眠进程获得合理的时间份额,但不会"抢夺"过多 CPU。(注意:4.1 使用的是 `cfs_rq->min_vruntime` 与 `max_vruntime()`,并非新版内核重写后的 `avg_vruntime()`。) ### 2.3 优先级体系 Linux 优先级分三层,数值越小优先级越高: ``` 实时优先级 (0~99) ← SCHED_FIFO / SCHED_RR 使用 ↓ 普通优先级 (100~139) ← SCHED_NORMAL / SCHED_BATCH 使用 ↓ nice 值 -20 ~ +19 对应 100~139 ``` 优先级换算关系: ``` 实时进程:prio = 99 - rt_priority (prio 0~99) 普通进程:prio = normal_prio = 120 + nice (prio 100~139) ``` (内核的 `effective_prio()` 按调度策略在上述两式中选择,并非对两个数值取大。) `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 内核通过调度类实现可插拔调度策略: ```c 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 控制共享哪些资源: ```c #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) Linux 4.1.15 的统一入口是 **`do_fork()`**(位于 `kernel/fork.c`,`SYSCALL_DEFINE0(fork)`/`clone`/`vfork` 都调用它)。`do_fork()` 在 4.2 被拆分为 `_do_fork()`,5.9 起更名为 `kernel_clone()`——所以新资料里的 `kernel_clone()` 与 4.1.15 的 `do_fork()` 是同一个函数。流程: ``` 用户态 fork()/clone() │ ▼ do_fork() // 5.9+ 内核中名为 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) ```c /* 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 使用**异常向量表**(固定地址 `0xFFFF0000` 或 `0x00000000`,由 VBAR 寄存器配置): ```asm /* arch/arm/kernel/entry-armv.S(Linux 4.1) */ .section .vectors, "ax", %progbits __vectors_start: W(b) vector_rst /* 0x00: 复位 Reset */ W(b) vector_und /* 0x04: 未定义指令 */ W(ldr) pc, __vectors_start + 0x1000 /* 0x08: SWI/SVC(跳到 vector_swi 指针)*/ W(b) vector_pabt /* 0x0C: 指令预取中止 */ W(b) vector_dabt /* 0x10: 数据访问中止 */ W(b) vector_addrexcptn /* 0x14: 保留 */ W(b) vector_irq /* 0x18: IRQ 中断 */ W(b) vector_fiq /* 0x1C: FIQ 快速中断 */ ``` > Cortex-A7 有 8 个异常入口,IRQ 占一个。SVC(系统调用)的向量在偏移 0x08,而 0x00 是复位向量——两者常被混为一谈。中断号通过 GIC (Generic Interrupt Controller) 确定,而非向量表偏移。 > ⚠️ **来源说明**:异常向量表布局为内核源码级细节,不属于教材讲授范围,基于 Linux 4.1.15 源码整理。 ### 4.2 GIC 中断控制器 GIC (Generic Interrupt Controller) 是 ARM 的标准中断控制器,GIC-400 最多支持 1020 个中断 ID(INTID 0~1019),其中 SPI 为 INTID 32~1019: ``` ┌─────────────────────────────────────────────┐ │ GIC-400 │ │ ┌──────────────┐ ┌──────────────────────┐ │ │ │ Distributor │ │ CPU Interface │ │ │ │ (分发器) │──│ (CPU 接口) │ │ │ │ │ │ │ │ │ │ GICD_ISENABLER │ GICC_IAR (中断确认) │ │ │ │ GICD_ICENABLER │ GICC_EOIR (中断结束) │ │ │ │ GICD_IPRIORITYR │ GICC_PMR (优先级) │ │ │ │ GICD_ITARGETSR │ GICC_CTLR (控制) │ │ │ │ GICD_ICFGR │ │ │ │ └──────────────┘ └──────────────────────┘ │ └─────────────────────────────────────────────┘ │ │ IRQ 线连接 CPU 接口连接 (SPI / PPI / SGI) (CPU0 / CPU1 / ...) ``` 中断类型: | 类型 | 编号范围 | 说明 | 典型 | | ---- | -------- | ----------------------- | --------------- | | SGI | 0~15 | 软件触发中断 (核间通信) | IPI | | PPI | 16~31 | 私有外设中断 (每核独有) | 定时器 | | SPI | 32~1019 | 共享外设中断 | UART、GPIO、DMA | ### 4.3 中断处理流程 ``` 硬件中断触发 (IRQ) │ ▼ CPU 保存现场 (R0-R15, CPSR → SPSR_irq) │ ▼ 切换到 IRQ 模式,跳转 vector_irq │ ▼ gic_handle_irq() ├── 读取 GICC_IAR 获取中断号 ├── 查找注册的 irq_desc │ ▼ handle_irq_event() ← 上半部 (Hardirq) │ ├── 执行注册的 irq_handler │ ├── 处理紧急事务 (读/清中断标志) │ └── 调度下半部 (如需) │ ▼ 写 GICC_EOIR 通知 GIC 中断结束 │ ▼ 检查 softirq_pending 标志 │ ▼ (中断返回前) __do_softirq() ← 下半部 (Softirq) │ ├── 执行已注册的 softirq │ ├── 执行 tasklet │ └── 时间片用完则在 softirqd 线程中执行 │ ▼ 恢复现场 (SPSR_irq → CPSR, R0-R15 恢复) │ ▼ 返回被中断的进程/线程 ``` ### 4.4 中断 API ```c /* 申请中断 — 成功返回 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); /* 中断标志(Linux 4.1.15 include/linux/interrupt.h) */ #define IRQF_SHARED 0x00000080 // 共享中断线 #define IRQF_ONESHOT 0x00002000 // 中断线在下半部完成前保持屏蔽 #define IRQF_NO_SUSPEND 0x00004000 // 系统挂起时不屏蔽此中断 #define IRQF_NO_THREAD 0x00010000 // 不创建线程化的中断处理 /* 禁止/使能中断 (本地 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 内核的延迟处理机制,编译时静态定义,运行在中断返回路径中: ```c /* 定义的软中断类型 (include/linux/interrupt.h) */ enum { HI_SOFTIRQ = 0, // 高优先级 tasklet TIMER_SOFTIRQ, // 定时器 NET_TX_SOFTIRQ, // 网络发送 NET_RX_SOFTIRQ, // 网络接收 BLOCK_SOFTIRQ, // 块设备 BLOCK_IOPOLL_SOFTIRQ, // 块设备轮询 I/O(4.1 为 BLOCK_IOPOLL,4.11+ 改为 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 基于软中断实现,是驱动开发中最常用的下半部机制: ```c /* 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` 线程),**可以睡眠**,适合需要阻塞操作的延迟工作: ```c /* 静态声明工作项 */ 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_KILLABLE`(`TASK_WAKEKILL | TASK_UNINTERRUPTIBLE`),既不可中断又可被 SIGKILL 杀死 --- **内容来源**:进程调度(`task_struct`/CFS/`sched_class`/`do_fork`)与中断管理(异常向量表/GIC/软中断/`IRQF_*`)部分为内核源码级扩展知识,不属于《I.MX6U嵌入式Linux驱动开发指南》讲授范围,已按 Linux 4.1.15 源码(`include/linux/sched.h`、`kernel/sched/*`、`kernel/fork.c`、`include/linux/interrupt.h`、`arch/arm/kernel/entry-armv.S`)核对。 **最后更新**: 2026-09-17