tags: [source-summary] type: source
Linux内核是怎么设计的?各个子系统之间如何协作?为什么这样设计?
初学者常有的困惑:
读完本文,你能理解:
Linux内核采用宏内核架构,主要子系统包括:
┌─────────────────────────────────────────────────────────────┐
│ 用户空间应用程序 │
├─────────────────────────────────────────────────────────────┤
│ 系统调用接口 │
├─────────┬─────────┬─────────┬─────────┬─────────┬──────────┤
│ 进程调度 │ 内存管理 │ 文件系统 │ 网络子系统│ 设备驱动 │ 进程间通信 │
│ 子系统 │ 子系统 │ 子系统 │ 子系统 │ 子系统 │ 子系统 │
├─────────┴─────────┴─────────┴─────────┴─────────┴──────────┤
│ 硬件抽象层(HAL) │
├─────────────────────────────────────────────────────────────┤
│ 硬件 │
└─────────────────────────────────────────────────────────────┘
各子系统职责:
进程调度如何依赖内存管理:
文件系统如何依赖内存管理:
设备驱动如何依赖中断和内存映射:
完全公平 = 每个任务获得的CPU时间与其权重成正比
传统调度器(如O(n))的问题:
CFS的创新:
vruntime += 实际运行时间 * (NICE_0_LOAD / 权重)
关键点:
权重表(部分):
| nice值 | 权重 | 相对CPU时间 |
|---|---|---|
| -20 | 88761 | 88.76% |
| -10 | 35554 | 35.55% |
| 0 | 1024 | 10.24% |
| 10 | 254 | 2.54% |
| 19 | 15 | 0.15% |
数据结构:
struct cfs_rq {
struct rb_root_cached tasks_timeline; // 红黑树根
struct sched_entity *curr; // 当前运行实体
// ...
};
特性:
rb_cached缓存最左节点,调度时O(1)获取调度过程:
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);
// ...
};
调度类层次(从高到低):
stop_sched_class(最高优先级,不可抢占)
dl_sched_class(截止时间调度)
rt_sched_class(实时调度:FIFO/RR)
fair_sched_class(CFS)
idle_sched_class(空闲任务)
问题:外部碎片(空闲内存够但不连续)
解决思路:按2的幂次分配,空闲块可以合并
实现:
分配算法:
// 简化逻辑
1. 找到最小满足需求的order
2. 如果该order的free_list为空,向上查找更大order
3. 拆分大块,直到得到所需大小
4. 返回分配的块
优点:
问题:内核频繁分配释放小对象(如task_struct、inode)
解决思路:预分配一批对象,用完再从伙伴系统补充
三层结构:
slab缓存结构:
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // 每CPU缓存
struct kmem_cache_node *node[MAX_NUMNODES]; // 每节点缓存
// ...
};
分配流程:
好处:
作用:文件读写先经过页缓存,减少磁盘IO
数据结构:
struct address_space {
struct inode *host; // 所属inode
struct rb_root_cached i_pages; // 缓存的页面
// ...
};
工作流程:
回收策略:
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; // 地址空间
// ...
};
设备号组成:
kobject/kset/ktype:设备对象模型
总线-设备-驱动三元组:
总线(bus)←→ 设备(device)←→ 驱动(driver)
匹配机制:
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 */ }
};
为什么引入设备树:
DTS/DTB/DTC的关系:
设备树语法示例:
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);
特点:
典型操作:
目的:将耗时操作延迟执行,避免长时间关中断
实现方式:
softirq:静态分配,性能高,但数量固定
// 定义softirq
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
// 触发softirq
raise_softirq(NET_TX_SOFTIRQ);
tasklet:基于softirq,动态创建,不能睡眠
// 定义tasklet
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
// 调度tasklet
tasklet_schedule(&my_tasklet);
workqueue:基于内核线程,可以睡眠,最灵活
// 定义工作项
struct work_struct my_work;
// 初始化工作项
INIT_WORK(&my_work, my_work_func);
// 调度工作
schedule_work(&my_work);
目的:将中断处理放到内核线程中,提高灵活性
API:
request_threaded_irq(irq, handler, thread_fn, irqflags, name, dev);
参数:
优点:
传统时间片的问题:
vruntime的优势:
单层分配的问题:
两层分工:
内核硬编码硬件的问题:
设备树的优势:
实时性与完整性的权衡:
上下半部分工:
典型例子:
常见数据结构比较:
| 数据结构 | 查找 | 插入 | 删除 | 特点 |
|---|---|---|---|---|
| 链表 | 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) | 有序,平衡,综合最优 |
红黑树优势:
CFS选择红黑树的原因:
| Linux内核设计 | 操作系统理论 | 硬件基础 | FreeRTOS对应 |
|---|---|---|---|
| CFS调度器 | 进程调度算法 | 时钟中断 | 任务调度器(优先级+时间片) |
| 伙伴系统 | 内存管理 | MMU/TLB | 内存池(静态分配) |
| slab分配器 | 缓存管理 | CPU缓存 | 对象池(静态分配) |
| 页缓存 | 文件系统缓存 | 磁盘缓存 | 无直接对应(资源有限) |
| 设备驱动框架 | 设备管理 | I/O控制器 | HAL(硬件抽象层) |
| 中断处理 | 中断处理 | 中断控制器 | 中断服务程序(ISR) |
| 线程化中断 | 线程管理 | 无直接对应 | 任务通知(轻量级同步) |
| 设备树 | 硬件抽象 | 硬件描述 | 静态配置(宏定义) |
误区:CFS是时间片轮转调度 真相:CFS是基于vruntime的公平调度,没有固定时间片,时间片是vruntime的副产品
误区:伙伴系统只能分配2的幂次大小的内存 真相:伙伴系统管理页级别内存(4KB为单位),小对象由slab分配器管理
误区:slab分配器只用于内核对象 真相:slab分配器也用于用户空间的内存分配(如glibc的malloc实现)
误区:设备树是必须的 真相:设备树是ARM平台引入的,x86平台仍使用ACPI表
误区:中断下半部都在中断上下文执行 真相: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)的插入、删除和查找,且内核已有成熟实现。