《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 的四大对象:
    1. 超级块对象(struct super_block):表示一个已安装的文件系统
      • 操作:struct super_operations(alloc_inode、write_inode、put_super 等)
    2. 索引节点对象(struct inode):表示一个文件(磁盘上的文件元数据)
      • 操作:struct inode_operations(create、lookup、link、mkdir 等)
    3. 目录项对象(struct dentry):表示路径中的一个分量
      • 目录项状态:被使用、未被使用、负状态
      • 目录项缓存(dentry cache)
      • 操作:struct dentry_operations
    4. 文件对象(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():查找给定地址所在的 VMA
    • find_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):
    • 负责将脏页写回磁盘
    • 触发条件:
      1. 空闲内存低于阈值
      2. 脏页驻留时间超过阈值
      3. 用户调用 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 无符号)
  • 数据对齐:
    • 为什么需要对齐(性能 / 架构要求)
    • 避免对齐问题的方法
    • 结构体填补(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 机制
    • 提交到对应的维护者和邮件列表

三、核心概念速览

概念位置核心思想
写时复制 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. 入门路径:第 1-2 章(概览)→ 第 3-4 章(进程调度,最容易上手)→ 第 12 章(内存管理)→ 第 7-8 章(中断下半部)
  2. 实战导向:配合源码阅读(Linux 2.6.x 或更新版本),边读边查代码
  3. 动手实践:写几个简单的内核模块(字符设备、定时器、proc 文件)
  4. 进阶路径:读完本书后可以读《深入理解Linux内核》(更深入原理)、《Linux设备驱动程序》(驱动开发)

五、与其他经典内核书的关系

  • 《深入理解Linux内核》(Understanding the Linux Kernel):更偏原理和数据结构,篇幅更大,适合深入研究
  • 《Linux设备驱动程序》(Linux Device Drivers):专注驱动开发,和本书互补
  • 《Linux内核源代码情景分析》:代码级逐行分析,适合源码精读
  • 本书是中间路线:既有原理也有实践,是内核开发入门的最佳起点

关联笔记