05-Linux内核设计与实现-原理与本质.md 20 KB


tags: [source-summary] type: source

created: 2026-09-19

Linux内核设计与实现:原理与本质

核心问题

Linux内核是怎么设计的?各个子系统之间如何协作?为什么这样设计?

初学者常有的困惑:

  • 内核代码庞大(数千万行),不知从何入手
  • 知道进程调度、内存管理等概念,但不知道它们如何协同工作
  • 学了驱动开发,但不理解底层框架的设计思想

读完本文,你能理解

  1. Linux内核的整体架构和子系统划分
  2. 进程调度(CFS)的设计哲学与实现原理
  3. 内存管理的两层分配机制(伙伴系统+slab)
  4. 设备驱动框架和中断处理机制
  5. 为什么内核要这样设计,而不是其他方式

原理讲解

第一部分:内核架构

1.1 Linux内核的整体架构图(子系统划分)

Linux内核采用宏内核架构,主要子系统包括:

┌─────────────────────────────────────────────────────────────┐
│                     用户空间应用程序                         │
├─────────────────────────────────────────────────────────────┤
│                      系统调用接口                            │
├─────────┬─────────┬─────────┬─────────┬─────────┬──────────┤
│ 进程调度 │ 内存管理 │ 文件系统 │ 网络子系统│ 设备驱动 │ 进程间通信 │
│  子系统  │  子系统  │  子系统  │   子系统  │  子系统  │   子系统   │
├─────────┴─────────┴─────────┴─────────┴─────────┴──────────┤
│                      硬件抽象层(HAL)                       │
├─────────────────────────────────────────────────────────────┤
│                         硬件                                │
└─────────────────────────────────────────────────────────────┘

各子系统职责

  1. 进程调度子系统:决定哪个进程获得CPU时间,管理进程状态转换
  2. 内存管理子系统:管理物理内存和虚拟内存,处理页面分配、回收、映射
  3. 文件系统子系统:VFS(虚拟文件系统)提供统一接口,具体文件系统(ext4、btrfs等)实现存储
  4. 网络子系统:处理网络协议栈(TCP/IP),管理网络设备
  5. 设备驱动子系统:管理各类硬件设备,提供统一的设备接口
  6. 进程间通信子系统:提供管道、共享内存、消息队列、信号量等IPC机制

1.2 子系统之间的协作关系

进程调度如何依赖内存管理

  • COW(Copy-On-Write):fork()时父子进程共享内存页,只有写入时才复制,依赖内存管理的页面引用计数
  • 页面回收:当内存不足时,调度器可能触发内存回收,将不活跃进程的内存换出到磁盘
  • OOM Killer:内存耗尽时,选择并杀死占用内存最多的进程

文件系统如何依赖内存管理

  • 页缓存(Page Cache):文件读写先经过页缓存,减少磁盘IO
  • 缓冲区缓存:块设备IO使用缓冲区缓存
  • 内存映射文件:mmap()将文件映射到进程地址空间,依赖虚拟内存管理

设备驱动如何依赖中断和内存映射

  • 中断处理:设备通过中断通知CPU完成操作
  • DMA(直接内存访问):设备直接访问内存,不经过CPU
  • MMIO(内存映射IO):设备寄存器映射到内存地址空间

第二部分:进程调度(CFS深入)

2.1 CFS的设计哲学

完全公平 = 每个任务获得的CPU时间与其权重成正比

传统调度器(如O(n))的问题:

  • 时间片固定,不适应不同优先级任务
  • 调度开销与任务数成正比

CFS的创新:

  • 虚拟运行时间(vruntime):标准化每个任务的运行时间
  • 权重系统:nice值决定权重,进而影响vruntime增长速度
  • 红黑树:按vruntime组织任务,支持O(log n)调度

2.2 vruntime的计算

vruntime += 实际运行时间 * (NICE_0_LOAD / 权重)

关键点

  • nice值越低,权重越高,vruntime增长越慢
  • 例如:nice=0的任务权重1024,nice=-5的任务权重3121
  • 权重高的任务vruntime增长慢,因此获得更多CPU时间

权重表(部分):

nice值 权重 相对CPU时间
-20 88761 88.76%
-10 35554 35.55%
0 1024 10.24%
10 254 2.54%
19 15 0.15%

2.3 红黑树组织

数据结构

struct cfs_rq {
    struct rb_root_cached tasks_timeline;  // 红黑树根
    struct sched_entity *curr;             // 当前运行实体
    // ...
};

特性

  • 左节点vruntime最小,最左节点就是下一个要运行的
  • 插入/删除/查找都是O(log n)
  • 通过rb_cached缓存最左节点,调度时O(1)获取

调度过程

  1. 选择vruntime最小的任务(最左节点)
  2. 运行该任务
  3. 更新vruntime
  4. 如果vruntime超过其他任务,重新插入红黑树

2.4 调度类(sched_class)

Linux采用模块化调度,不同任务类型使用不同调度策略:

struct sched_class {
    void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
    void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);
    void (*yield_task)(struct rq *rq);
    void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
    struct task_struct *(*pick_next_task)(struct rq *rq);
    // ...
};

调度类层次(从高到低):

  1. stop_sched_class(最高优先级,不可抢占)

    • 用于CPU热插拔、迁移任务等关键操作
    • 优先级最高,可以抢占任何其他任务
  2. dl_sched_class(截止时间调度)

    • 用于有严格时间要求的任务(如实时视频处理)
    • 保证任务在截止时间前完成
  3. rt_sched_class(实时调度:FIFO/RR)

    • SCHED_FIFO:先进先出,没有时间片
    • SCHED_RR:时间片轮转
    • 优先级范围:1-99(数值越大优先级越高)
  4. fair_sched_class(CFS)

    • 用于普通进程
    • 基于vruntime的公平调度
  5. idle_sched_class(空闲任务)

    • 当没有其他任务时运行
    • 优先级最低

第三部分:内存管理深入

3.1 伙伴系统(Buddy System)

问题:外部碎片(空闲内存够但不连续)

解决思路:按2的幂次分配,空闲块可以合并

实现

  • 11个free_list,大小从4KB到4MB(2^0到2^10页)
  • 每个free_list管理对应大小的空闲块
  • 分配时查找最小满足需求的块
  • 释放时检查伙伴块是否空闲,合并后继续向上合并

分配算法

// 简化逻辑
1. 找到最小满足需求的order
2. 如果该order的free_list为空,向上查找更大order
3. 拆分大块,直到得到所需大小
4. 返回分配的块

优点

  • 解决外部碎片问题
  • 分配/释放效率高(O(log n))
  • 支持大块连续内存分配

3.2 slab分配器

问题:内核频繁分配释放小对象(如task_struct、inode)

解决思路:预分配一批对象,用完再从伙伴系统补充

三层结构

  1. slab层:管理对象缓存
  2. 通用cache:管理slab的缓存(如size-32、size-64)
  3. 伙伴系统:底层内存分配

slab缓存结构

struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  // 每CPU缓存
    struct kmem_cache_node *node[MAX_NUMNODES]; // 每节点缓存
    // ...
};

分配流程

  1. 先从当前CPU的本地缓存分配(无锁,最快)
  2. 本地缓存为空,从节点缓存补充
  3. 节点缓存为空,从伙伴系统分配新页
  4. 将新页切分成对象,加入缓存

好处

  • 避免碎片(对象大小固定)
  • 加速分配(无锁本地缓存)
  • 支持对象缓存(构造/析构函数)

3.3 页缓存(Page Cache)

作用:文件读写先经过页缓存,减少磁盘IO

数据结构

struct address_space {
    struct inode *host;           // 所属inode
    struct rb_root_cached i_pages; // 缓存的页面
    // ...
};

工作流程

  1. 读文件:先查页缓存,命中则直接返回;未命中则从磁盘读入缓存
  2. 写文件:先写入页缓存,标记为脏页;定期或显式同步时写回磁盘
  3. 内存回收:按LRU(最近最少使用)回收不活跃页面

回收策略

  • 活跃链表非活跃链表:页面在两个链表间移动
  • 第二次机会算法:检查页面访问位,未被访问则回收
  • 脏页回收:先写回磁盘,再回收

第四部分:设备驱动框架

4.1 字符设备驱动的核心数据结构

cdev:字符设备抽象

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;  // 操作函数集
    dev_t dev;                          // 设备号
    // ...
};

file_operations:文件操作函数集

struct file_operations {
    struct module *owner;
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*open)(struct inode *, struct file *);
    int (*release)(struct inode *, struct file *);
    // ...
};

inode:设备号+文件元信息

struct inode {
    umode_t i_mode;           // 文件类型和权限
    kdev_t i_rdev;            // 设备号
    struct address_space *i_mapping;  // 地址空间
    // ...
};

设备号组成

  • 主设备号:标识设备类型(如/dev/sda的主设备号是8)
  • 次设备号:标识同类设备中的具体设备

4.2 设备模型(sysfs)

kobject/kset/ktype:设备对象模型

  • kobject:设备对象基类,提供引用计数
  • kset:同类kobject的集合
  • ktype:kobject的操作方法

总线-设备-驱动三元组

总线(bus)←→ 设备(device)←→ 驱动(driver)

匹配机制

  1. 设备注册时,遍历总线上所有驱动,尝试匹配
  2. 驱动注册时,遍历总线上所有设备,尝试匹配
  3. 匹配成功则调用驱动的probe()函数

platform总线:嵌入式最常用

struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    struct device_driver driver;
    // ...
};

设备树匹配

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,device" },
    { /* sentinel */ }
};

4.3 设备树(Device Tree)

为什么引入设备树

  • 问题:内核中硬编码硬件信息(如地址、中断号),导致内核臃肿
  • 解决:将硬件描述信息移到设备树文件中,内核启动时解析

DTS/DTB/DTC的关系

  • DTS(Device Tree Source):人类可读的设备树源文件
  • DTB(Device Tree Blob):编译后的二进制文件
  • DTC(Device Tree Compiler):编译工具

设备树语法示例

my_device@0x10000000 {
    compatible = "vendor,device";
    reg = <0x10000000 0x1000>;
    interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk 0>;
};

*of* API_*:内核解析设备树的接口

// 读取属性
of_property_read_u32(node, "reg", &value);

// 获取设备树节点
of_find_compatible_node(NULL, NULL, "vendor,device");

// 获取中断号
irq_of_parse_and_map(node, 0);

第五部分:中断处理框架

5.1 上半部(硬中断)

特点

  • 快速处理,不能睡眠
  • 关中断执行(本地中断关闭)
  • 处理最关键的硬件相关操作

典型操作

  • 读取设备状态寄存器
  • 清除中断标志
  • 将数据从设备拷贝到内存

5.2 下半部(延迟处理)

目的:将耗时操作延迟执行,避免长时间关中断

实现方式

  1. softirq:静态分配,性能高,但数量固定

    // 定义softirq
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    
    // 触发softirq
    raise_softirq(NET_TX_SOFTIRQ);
    
    • 执行上下文:中断上下文,不能睡眠
    • 典型应用:网络收发、定时器
  2. tasklet:基于softirq,动态创建,不能睡眠

    // 定义tasklet
    DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
    
    // 调度tasklet
    tasklet_schedule(&my_tasklet);
    
    • 特点:同一tasklet不会并发执行
    • 典型应用:驱动程序的延迟处理
  3. workqueue:基于内核线程,可以睡眠,最灵活

    // 定义工作项
    struct work_struct my_work;
    
    // 初始化工作项
    INIT_WORK(&my_work, my_work_func);
    
    // 调度工作
    schedule_work(&my_work);
    
    • 执行上下文:进程上下文,可以睡眠
    • 典型应用:需要睡眠的延迟操作

5.3 线程化中断(threaded irq)

目的:将中断处理放到内核线程中,提高灵活性

API

request_threaded_irq(irq, handler, thread_fn, irqflags, name, dev);

参数

  • handler:硬中断处理函数(快速,不能睡眠)
  • thread_fn:线程化中断处理函数(可以睡眠)
  • irqflags:中断标志(如IRQF_ONESHOT)

优点

  • 可以设置优先级
  • 可以睡眠
  • 便于调试(可以通过ps查看中断线程)

第六部分:为什么这样设计

6.1 为什么CFS用vruntime而不是时间片

传统时间片的问题

  • 固定时间片无法适应不同优先级任务
  • 优先级调整需要重新计算时间片

vruntime的优势

  • 动态权重调整:nice值变化时,vruntime连续变化,无跳变
  • 公平性保证:所有任务的vruntime最终会趋于一致
  • 实现简单:只需要维护一个红黑树

6.2 为什么需要伙伴系统+slab两层

单层分配的问题

  • 伙伴系统:分配效率高,但小对象浪费内存(最小4KB)
  • slab分配器:小对象高效,但需要底层内存管理

两层分工

  • 伙伴系统:管理大块内存(页级别),解决外部碎片
  • slab分配器:管理小对象,解决内部碎片,提供对象缓存

6.3 为什么引入设备树

内核硬编码硬件的问题

  • 同一硬件在不同平台需要不同配置
  • 内核代码膨胀,维护困难
  • 新增硬件需要修改内核代码

设备树的优势

  • 硬件描述与内核分离:同一内核支持多种硬件配置
  • 可维护性:硬件信息集中管理,易于修改
  • 可扩展性:新增硬件只需添加设备树节点

6.4 为什么中断要分上下半部

实时性与完整性的权衡

  • 实时性要求:快速响应硬件中断,避免丢失事件
  • 完整性要求:完整处理中断逻辑,可能耗时

上下半部分工

  • 上半部:快速响应,保证实时性
  • 下半部:完整处理,保证完整性

典型例子

  • 网络收包:上半部拷贝数据到内存,下半部处理协议栈
  • 磁盘IO:上半部通知完成,下半部更新文件系统

6.5 为什么选红黑树而不是其他结构

常见数据结构比较

数据结构 查找 插入 删除 特点
链表 O(n) O(1) O(1) 简单,但查找慢
哈希表 O(1) O(1) O(1) 快速,但无序
二叉搜索树 O(log n) O(log n) O(log n) 有序,但可能退化
红黑树 O(log n) O(log n) O(log n) 有序,平衡,综合最优

红黑树优势

  • 有序:支持范围查询(如查找vruntime在某个范围的任务)
  • 平衡:最坏情况下仍保持O(log n)
  • 缓存友好:相比AVL树,旋转次数少,缓存命中率高

CFS选择红黑树的原因

  • 需要按vruntime排序,支持快速找到最小vruntime任务
  • 需要频繁插入/删除(任务状态变化)
  • 红黑树是内核中已有的通用数据结构

跨学科对应表

Linux内核设计 操作系统理论 硬件基础 FreeRTOS对应
CFS调度器 进程调度算法 时钟中断 任务调度器(优先级+时间片)
伙伴系统 内存管理 MMU/TLB 内存池(静态分配)
slab分配器 缓存管理 CPU缓存 对象池(静态分配)
页缓存 文件系统缓存 磁盘缓存 无直接对应(资源有限)
设备驱动框架 设备管理 I/O控制器 HAL(硬件抽象层)
中断处理 中断处理 中断控制器 中断服务程序(ISR)
线程化中断 线程管理 无直接对应 任务通知(轻量级同步)
设备树 硬件抽象 硬件描述 静态配置(宏定义)

常见误区

  1. 误区:CFS是时间片轮转调度 真相:CFS是基于vruntime的公平调度,没有固定时间片,时间片是vruntime的副产品

  2. 误区:伙伴系统只能分配2的幂次大小的内存 真相:伙伴系统管理页级别内存(4KB为单位),小对象由slab分配器管理

  3. 误区:slab分配器只用于内核对象 真相:slab分配器也用于用户空间的内存分配(如glibc的malloc实现)

  4. 误区:设备树是必须的 真相:设备树是ARM平台引入的,x86平台仍使用ACPI表

  5. 误区:中断下半部都在中断上下文执行 真相:softirq和tasklet在中断上下文执行,workqueue在进程上下文执行


面试要点

Q1:CFS如何保证公平性? A:通过vruntime标准化每个任务的运行时间。vruntime = 实际运行时间 × (NICE_0_LOAD / 权重)。权重高的任务vruntime增长慢,因此获得更多CPU时间。调度器总是选择vruntime最小的任务运行。

Q2:伙伴系统和slab分配器如何协作? A:伙伴系统管理页级别内存(4KB到4MB),解决外部碎片问题。slab分配器从伙伴系统分配页面,然后将页面切分成小对象,解决内部碎片问题。分配小对象时,先从slab缓存分配;缓存为空时,从伙伴系统补充新页面。

Q3:为什么中断要分上下半部? A:实时性要求快速响应硬件中断,避免丢失事件;完整性要求完整处理中断逻辑,可能耗时。分上下半部是权衡:上半部快速响应(关中断执行),保证实时性;下半部延迟处理(开中断执行),保证完整性。

Q4:设备树解决了什么问题? A:解决了内核硬编码硬件信息的问题。传统方式将硬件描述(如地址、中断号)写在内核代码中,导致内核臃肿、维护困难。设备树将硬件描述独立出来,内核启动时解析,实现硬件描述与内核分离。

Q5:为什么CFS使用红黑树而不是哈希表? A:CFS需要按vruntime排序,支持快速找到最小vruntime任务(调度)。哈希表虽然查找快,但无法按顺序遍历。红黑树是有序的平衡二叉搜索树,支持O(log n)的插入、删除和查找,且内核已有成熟实现。


参考资料

  1. 《Linux内核设计与实现》(第3版)- Robert Love
  2. 《深入理解Linux内核》(第3版)- Daniel P. Bovet
  3. 《Linux设备驱动程序》(第3版)- Jonathan Corbet
  4. 《深入Linux内核架构》- Wolfgang Mauerer
  5. Linux内核源码:https://github.com/torvalds/linux
  6. LWN.net:https://lwn.net/ (Linux内核新闻和技术文章)
  7. The Linux Kernel Documentation:https://www.kernel.org/doc/html/latest/