《Linux内核设计与实现》(第三版)
Robert Love 著 | 基于 Linux 2.6.34 内核 | 机械工业出版社 内核开发入门的经典实战指南,“从实用视角讲解内核开发该做什么、不该做什么”
一、书籍概览
- 定位:Linux 内核开发的入门与进阶实战指南,不同于《深入理解Linux内核》的”是什么”、《Linux内核源代码情景分析》的”怎么实现”,本书更侧重于”怎么用内核做开发”
- 特点:
- 实用导向:既讲设计原理,也讲编码实践
- 覆盖面广:20 章覆盖内核主要子系统
- 篇幅适中:353 页,不是砖头书,适合通读
- 基于 2.6.34 内核(2.6 系列是 Linux 内核发展的重要稳定期)
- 作者背景:Robert Love,资深 Linux 内核开发者,Google Android 内核团队成员,曾参与抢占式内核、进程调度器、内核事件层等工作
二、全书知识体系(20 章结构)
第一部分:内核入门(第 1-2 章)
第 1 章 Linux 内核简介
- Unix 的历史与设计哲学(一切皆文件、简洁、模块化)
- Linux 的诞生与发展(Linus Torvalds,1991 年)
- 操作系统与内核的概念(宏内核 vs 微内核)
- Linux 内核 vs 传统 Unix 内核的区别
- 支持动态加载内核模块
- 支持对称多处理(SMP)
- 内核可以抢占(preemptible)
- 线程被实现为轻量级进程
- 提供设备文件系统(devfs/sysfs)
- Linux 内核版本号规则(2.6.x 系列)
- 内核开发者社区
第 2 章 从内核出发
- 获取内核源码(Git、官方网站、补丁)
- 内核源码树结构
arch/架构相关代码block/块设备层crypto/加密算法drivers/设备驱动fs/文件系统include/头文件init/内核初始化ipc/进程间通信kernel/核心子系统(调度、时间等)lib/通用库函数mm/内存管理net/网络协议栈scripts/脚本工具
- 编译内核(配置 → 编译 → 安装)
- 内核开发的特点(重要):
- 无 libc 库、无标准头文件
- 使用 GNU C 扩展
- 没有内存保护机制(越界直接 panic)
- 不要轻易使用浮点数
- 容积小而固定的栈(通常 8KB / 4KB)
- 必须考虑同步和并发
- 可移植性很重要(支持多种架构)
第二部分:进程与调度(第 3-4 章)
第 3 章 进程管理
- 进程概念(程序的运行实例)
- 进程描述符
task_struct- 分配:
SLAB分配器 - 存放:通过
current宏访问当前进程
- 分配:
- 进程状态:
TASK_RUNNING运行中/可运行TASK_INTERRUPTIBLE可中断睡眠TASK_UNINTERRUPTIBLE不可中断睡眠TASK_STOPPED停止TASK_TRACED被追踪
- 进程创建:
- 写时复制(Copy-On-Write):fork 后不立即复制内存,写时才复制
fork():通过clone()系统调用实现vfork():不复制父进程页表,子进程先运行,父进程阻塞
- 线程实现:Linux 中线程是”轻量级进程”,通过
clone()标志位控制共享资源- 内核线程(kernel thread):只在内核空间运行,没有独立地址空间
- 进程终结:
do_exit()→ 释放资源 → 僵尸状态 → 父进程wait()回收- 孤儿进程的处理(init 进程收养)
第 4 章 进程调度
- 多任务概念(抢占式多任务)
- 调度策略:
- I/O 消耗型 vs 处理器消耗型
- 进程优先级(动态优先级 + nice 值)
- 时间片(quantum)
- CFS 完全公平调度器(Linux 2.6.23 引入):
- 调度器类(sched_class)的模块化设计
- 公平调度的核心思想:每个进程公平分享处理器时间
- 时间记账(虚拟运行时间
vruntime) - 进程选择:红黑树选择
vruntime最小的进程 - 调度器入口:
schedule()
- 睡眠与唤醒(等待队列)
- 抢占和上下文切换:
- 用户抢占:从中断/系统调用返回用户空间时检查
- 内核抢占:2.6 内核引入,持有锁时不可抢占
- 实时调度策略:
SCHED_FIFO:先进先出,无时间片SCHED_RR:时间片轮转
- 调度相关系统调用:
nice()、sched_setscheduler()、sched_setaffinity()、sched_yield()等
第三部分:内核基础设施(第 5-6 章)
第 5 章 系统调用
- 系统调用的作用(用户空间与内核的接口)
- API、POSIX 和 C 库的关系
- 系统调用号(每个调用唯一标识,通过
sys_call_table查找) - 系统调用处理程序:
- 指定调用(通过寄存器
eax传调用号) - 参数传递(寄存器传递,最多 6 个)
- 指定调用(通过寄存器
- 实现系统调用的步骤:
- 编写实现函数
- 添加系统调用号
- 绑定到系统调用表
- 参数验证:
- 指针有效性检查(用户空间指针不能直接读写)
- 使用
copy_from_user()/copy_to_user()
- 系统调用上下文(进程上下文,可以睡眠,可以被抢占)
- 为什么不滥用系统调用(稳定性、兼容性、接口简洁性)
第 6 章 内核数据结构
- 链表:
- 内核中的实现:
struct list_head侵入式链表 - 操作:增删改查、遍历
- 经典技巧:通过
container_of()从链表节点找到宿主结构
- 内核中的实现:
- 队列(kfifo):
- 无锁环形缓冲区
- 生产者-消费者模型
- 映射(idr):
- 将 ID 映射到指针的结构
- 用于文件描述符、PID 等场景
- 二叉树:
- 红黑树(自平衡二叉搜索树)
- 内核中广泛使用(CFS 调度、内存区域管理等)
- 数据结构选择指南
- 算法复杂度(大 O 符号、大 θ 符号)
第四部分:中断与下半部(第 7-8 章)
第 7 章 中断和中断处理
- 中断概念(硬件通知 CPU 的机制)
- 中断处理程序(ISR)
- 上半部 vs 下半部:
- 上半部:快速响应,只做必须立即处理的事
- 下半部:延后执行不紧急的工作
- 注册/释放中断处理程序(
request_irq()/free_irq())- 中断标志(
IRQF_DISABLED、IRQF_SHARED等)
- 中断标志(
- 编写中断处理程序:
- 共享中断的处理
- 实例分析
- 中断上下文:
- 不能睡眠(不能调用可能阻塞的函数)
- 时间受限,快速执行
- 中断处理机制的实现(内核层面)
/proc/interrupts查看中断统计- 中断控制:
- 禁止/激活本地中断(
cli/sti) - 禁止指定中断线
- 中断系统状态查询
- 禁止/激活本地中断(
第 8 章 下半部和推后执行的工作
- 为什么需要下半部(中断上半部太紧急,需要分离)
- 下半部环境(不允许访问用户空间、不能睡眠,等)
- 软中断(softirq):
- 静态分配,编译时确定
- 同一类型的软中断可以在多个 CPU 上并发执行
- 执行时机:中断返回、
ksoftirqd内核线程 - 使用场景:网络子系统、定时器、tasklet 等
- tasklet:
- 基于软中断实现
- 同一类型的 tasklet 不能并发(但不同类型可以)
- 动态注册,使用简单
- 推荐优先使用 tasklet 而非直接使用软中断
- 工作队列(workqueue):
- 运行在进程上下文,可以睡眠
- 由内核线程(worker thread)执行
- 适合需要睡眠的推后工作
- 下半部机制的选择:
机制 上下文 并发 能否睡眠 适用场景 软中断 中断上下文 同类型可多CPU并发 否 高性能、有锁保护 tasklet 中断上下文 同类型不能并发 否 多数下半部场景 工作队列 进程上下文 可并发(默认) 能 需要睡眠的工作 - 下半部之间的加锁
- 禁止下半部(
local_bh_disable())
第五部分:内核同步(第 9-10 章)
第 9 章 内核同步介绍
- 临界区和竞争条件
- 为什么需要保护(并发执行的源头)
- 中断、软中断、tasklet
- 内核抢占
- SMP 多处理器
- 加锁的基本概念
- 死锁:
- 自死锁(递归加锁同一把锁)
- ABBA 死锁(两个进程互相等待对方的锁)
- 避免死锁的原则:按顺序加锁、防止饥饿、避免嵌套
- 锁争用和扩展性:
- 锁粒度的设计(粗粒度 vs 细粒度)
- 扩展性(scalability)的考量
第 10 章 内核同步方法
- 原子操作:
- 原子整数操作(
atomic_t+ 各种atomic_*函数) - 64 位原子操作(
atomic64_t) - 原子位操作(
set_bit、clear_bit、test_and_set_bit等)
- 原子整数操作(
- 自旋锁(spinlock):
- 忙等待,不睡眠
- 持有时间必须短
- 自旋锁 + 下半部 / 中断的使用注意
- 读写自旋锁(rwlock):
- 读共享、写独占
- 适用于读多写少的场景
- 信号量(semaphore):
- 睡眠锁,持有时间可以较长
- 计数信号量(可以多个持有者)
- 二值信号量 / 互斥信号量(只有一个持有者)
- 读写信号量(rwsem)
- 互斥体(mutex):
- 更简单的互斥锁接口
- 优先使用 mutex 而非信号量做互斥
- 完成变量(completion):
- 一个任务等待另一个任务完成
- 大内核锁(BKL,Big Kernel Lock):
- 历史遗留,2.6 后期逐步移除
- 全局自旋锁,粗粒度
- 顺序锁(seqlock):
- 读写通过序号同步
- 写优先于读
- 适用于读多写少、数据简单的场景(如 jiffies)
- 禁止抢占:
preempt_disable()/preempt_enable()- 保护 per-CPU 数据
- 内存屏障(barrier):
- 编译器屏障(
barrier()) - CPU 内存屏障(
rmb()、wmb()、mb()、smp_rmb()等) - 解决乱序执行导致的可见性问题
- 编译器屏障(
第六部分:时间与内存(第 11-12 章)
第 11 章 定时器和时间管理
- 内核中的时间概念
- 节拍率 HZ:
- 系统定时器的频率(常见值:100、250、1000)
- 高 HZ 的优势:更高精度的定时器、更精确的进程统计
- 高 HZ 的劣势:更多时钟中断开销
- jiffies:
- 自系统启动以来的节拍计数
- 回绕问题及处理(
time_after()、time_before()等宏) - 用户空间与 HZ 的转换
- 硬时钟和定时器:
- 实时时钟(RTC):持久计时
- 系统定时器:提供周期性中断
- 时钟中断处理程序
- 实际时间(wall time):
xtime - 定时器(timer):
- 动态定时器(
struct timer_list) - 使用方法:
init_timer→ 设置 →add_timer - 定时器竞争条件
- 实现:定时器以链表形式管理(2.6 内核使用时间轮算法优化)
- 动态定时器(
- 延迟执行:
- 忙等待(
udelay(),短延迟) schedule_timeout():让出 CPU 等待
- 忙等待(
第 12 章 内存管理
- 页(page):内存管理的基本单位(通常 4KB)
- 区(zone):
ZONE_DMA:DMA 可用内存(低端)ZONE_NORMAL:正常映射内存ZONE_HIGHMEM:高端内存(不能直接映射)
- 获得页 / 释放页(
alloc_pages()/__free_pages()) kmalloc():- 内核中最常用的内存分配函数
gfp_mask标志:GFP_KERNEL:内核分配,可能睡眠GFP_ATOMIC:原子分配,不睡眠(中断上下文用)GFP_USER:用户空间分配__GFP_DMA:从 DMA 区分配__GFP_HIGHMEM:从高端内存分配__GFP_ZERO:清零
vmalloc():- 虚拟地址连续,物理地址不连续
- 用于分配大内存
- slab 分配器:
- 解决小对象分配的性能问题
- 三大目的:频繁分配释放的缓存、减少碎片、着色对齐
- slab 层设计:slab → 对象 → 缓存
- 接口:
kmem_cache_create()、kmem_cache_alloc()等
- 内核栈(小而固定)
- 高端内存映射(永久映射 / 临时映射)
- 每个 CPU 的数据(per-CPU)
- 减少锁竞争
- 编译时 / 运行时两种方式
- 分配函数选择指南
第七部分:I/O 与文件系统(第 13-14 章)
第 13 章 虚拟文件系统(VFS)
- 通用文件系统接口
- 文件系统抽象层:VFS 作为内核和具体文件系统之间的中间层
- Unix 文件系统的特点(一切皆文件)
- VFS 的四大对象:
- 超级块对象(
struct super_block):表示一个已安装的文件系统- 操作:
struct super_operations(alloc_inode、write_inode、put_super等)
- 操作:
- 索引节点对象(
struct inode):表示一个文件(磁盘上的文件元数据)- 操作:
struct inode_operations(create、lookup、link、mkdir等)
- 操作:
- 目录项对象(
struct dentry):表示路径中的一个分量- 目录项状态:被使用、未被使用、负状态
- 目录项缓存(dentry cache)
- 操作:
struct dentry_operations
- 文件对象(
struct file):表示进程打开的文件- 操作:
struct file_operations(read、write、open、release、llseek、mmap等)
- 操作:
- 超级块对象(
- 和文件系统相关的数据结构(
struct file_system_type、struct vfsmount) - 和进程相关的数据结构(
struct files_struct、struct fs_struct)
第 14 章 块 I/O 层
- 块设备的基本概念(扇区、块、段)
- 缓冲区和缓冲区头(
struct buffer_head,老旧接口) bio结构体(新一代 I/O 描述):- I/O 向量(bio_vec 数组)
- 比 buffer_head 更灵活、高效
- 请求队列(
struct request_queue) - I/O 调度程序:
- 工作:合并 + 排序请求,减少磁盘寻道
- Linus 电梯:简单的合并+排序(2.6 早期默认)
- 最终期限 I/O 调度程序(Deadline):
- 读/写队列 + 到期时间
- 防止饥饿(读优先)
- 适合数据库类 workload
- 预测 I/O 调度程序(Anticipatory):
- 在 Deadline 基础上增加”预测”:等待一小段时间看是否有相邻请求
- 适合桌面、机械硬盘
- 完全公正的排队 I/O 调度程序(CFQ):
- 每个进程一个队列,时间片轮转
- 公平分配 I/O 带宽
- 桌面系统默认
- 空操作 I/O 调度程序(Noop):
- 不排序,只做合并
- 适合 SSD、随机访问设备
第八部分:高级内存管理(第 15-16 章)
第 15 章 进程地址空间
- 地址空间概念(每个进程的虚拟地址空间)
- 内存描述符(
struct mm_struct):- 分配/撤销
- 内核线程没有独立地址空间(借用上一个进程的 mm)
- 虚拟内存区域(VMA,
struct vm_area_struct):- 描述地址空间中一段连续的内存区域
- VMA 标志(
VM_READ、VM_WRITE、VM_EXEC、VM_SHARED等) - VMA 操作
- 组织方式:红黑树 + 链表
- 操作内存区域:
find_vma():查找给定地址所在的 VMAfind_vma_prev()、find_vma_intersection()
mmap()和do_mmap():创建地址区间(内存映射)munmap()和do_munmap():删除地址区间- 页表(三级/四级页表,架构相关)
第 16 章 页高速缓存和页回写
- 缓存手段(空间换时间)
- 写缓存策略:写通 / 写回(write-back)
- 缓存回收(页替换算法)
- Linux 页高速缓存(page cache):
- 缓存文件的页面数据
address_space对象(一个文件的缓存)address_space操作(readpage、writepage等)- 基树(radix tree):快速查找缓存页
- 缓冲区高速缓存(buffer cache,已整合进页高速缓存)
- flusher 线程(pdflush → flusher):
- 负责将脏页写回磁盘
- 触发条件:
- 空闲内存低于阈值
- 脏页驻留时间超过阈值
- 用户调用
sync()等
- 膝上型计算机模式(省电模式)
- 历史:bdflush → kupdated → pdflush → flusher 线程(每磁盘一个)
第九部分:设备与调试(第 17-18 章)
第 17 章 设备与模块
- 设备类型:
- 字符设备(
cdev):字节流访问,如串口 - 块设备(
block_device):块为单位随机访问,如磁盘 - 网络设备(
net_device):通过套接字接口访问
- 字符设备(
- 模块(Linux Kernel Module):
- 内核的”插件”机制,动态加载/卸载
- Hello World 模块示例
- 构建模块(Makefile、Kbuild)
- 安装模块(
insmod、rmmod、modprobe) - 模块依赖性(
modprobe自动处理) - 模块参数(
module_param) - 导出符号表(
EXPORT_SYMBOL、EXPORT_SYMBOL_GPL)
- 设备模型(device model):
kobject:基础对象ktype:kobject 的类型kset:kobject 的集合- 三者的关系
- 引用计数
- sysfs:
- 将设备模型以文件系统形式呈现给用户空间
- 挂载点通常是
/sys - 添加/删除 kobject、添加属性文件
- 内核事件层(uevent,热插拔事件)
第 18 章 调试
- 准备工作(开启调试配置选项)
- 内核中的 bug 类型(oops、panic、死锁、内存泄漏等)
- 通过打印调试(printk):
- 日志等级(8 级:KERN_EMERG 到 KERN_DEBUG)
- 记录缓冲区(环形缓冲区)
syslogd/klogd
- oops:
- 内核的”严重错误”信息
- 包含寄存器值、回溯(backtrace)
ksymoops/kallsyms:解析符号
- 内核调试配置选项(
CONFIG_DEBUG_KERNEL、CONFIG_DEBUG_SLAB等) - 引发 bug 并打印信息(
BUG()、BUG_ON()、panic()) - 神奇的系统请求键(SysRq):
Alt+SysRq+键触发- 功能:打印信息、重启、紧急同步等
- 内核调试器:
- gdb 远程调试
- kgdb(内核调试补丁)
- 探测系统的技巧:
- 用 UID 作为选择条件
- 条件变量
- 统计量
- 重复频率限制
- 二分查找法定位 bug(git bisect)
- 社区求助(LKML 等)
第十部分:高级主题(第 19-20 章)
第 19 章 可移植性
- 可移植操作系统的重要性
- Linux 移植史
- 字长和数据类型:
- C 标准类型的大小不固定(
long在 32/64 位不同) - 不透明类型(如
pid_t、size_t) - 长度明确的类型(
u8、s32、u64等) - char 型的符号问题(有符号 vs 无符号)
- C 标准类型的大小不固定(
- 数据对齐:
- 为什么需要对齐(性能 / 架构要求)
- 避免对齐问题的方法
- 结构体填补(padding)
- 字节顺序(大小端)
- 时间(jiffies 与 HZ 的关系)
- 页长度(
PAGE_SIZE宏) - 处理器排序(内存屏障)
- SMP、内核抢占、高端内存的可移植性注意
第 20 章 补丁、开发和社区
- Linux 内核社区文化
- Linux 编码风格:
- 缩进:8 字符宽制表符
- switch 语句的 case 缩进
- 空格使用规范
- 花括号:K&R 风格
- 每行不超过 80 字符
- 命名:全小写 + 下划线(不使用驼峰)
- 函数:短小精悍,不超过一屏
- 注释:说明”为什么”而非”做什么”
- 少用 typedef(除非有充分理由)
- 多用现成的东西(不要重复造轮子)
- 减少使用
#ifdef
- 管理系统(邮件列表、补丁提交流程)
- 提交错误报告
- 补丁制作与提交:
- 用 Git 创建补丁(
git format-patch) - 补丁格式:
Subject: [PATCH] 描述 - Signed-off-by 机制
- 提交到对应的维护者和邮件列表
- 用 Git 创建补丁(
三、核心概念速览
| 概念 | 位置 | 核心思想 |
|---|---|---|
| 写时复制 COW | 第3章 | fork 后共享内存,写入时才复制 |
| CFS 调度器 | 第4章 | 红黑树 + vruntime,完全公平 |
| 上半部/下半部 | 第7-8章 | 中断快速响应,工作延后执行 |
| 软中断 vs tasklet vs 工作队列 | 第8章 | 三种下半部机制,场景不同 |
| VFS 四大对象 | 第13章 | 超级块/索引节点/目录项/文件 |
| I/O 调度 | 第14章 | 电梯算法 → Deadline/AS/CFQ/Noop |
| slab 分配器 | 第12章 | 小对象缓存,减少碎片 |
| 页高速缓存 | 第16章 | 内存缓存磁盘文件,写回策略 |
| 内核抢占 | 第4/10章 | 2.6 引入,提高响应性 |
| per-CPU 数据 | 第12章 | 减少锁竞争,提高扩展性 |
四、阅读建议
- 入门路径:第 1-2 章(概览)→ 第 3-4 章(进程调度,最容易上手)→ 第 12 章(内存管理)→ 第 7-8 章(中断下半部)
- 实战导向:配合源码阅读(Linux 2.6.x 或更新版本),边读边查代码
- 动手实践:写几个简单的内核模块(字符设备、定时器、proc 文件)
- 进阶路径:读完本书后可以读《深入理解Linux内核》(更深入原理)、《Linux设备驱动程序》(驱动开发)
五、与其他经典内核书的关系
- 《深入理解Linux内核》(Understanding the Linux Kernel):更偏原理和数据结构,篇幅更大,适合深入研究
- 《Linux设备驱动程序》(Linux Device Drivers):专注驱动开发,和本书互补
- 《Linux内核源代码情景分析》:代码级逐行分析,适合源码精读
- 本书是中间路线:既有原理也有实践,是内核开发入门的最佳起点
关联笔记
- 《结构化计算机组成》-Structured-Computer-Organization-第6版-Tanenbaum — 操作系统层的底层视角
- 《程序员的自我修养》-链接、装载与库 — 从用户空间视角看程序运行
- 《SICP》-计算机程序的构造和解释-第二版 — 抽象分层思想的另一体现