【读书笔记】《Linux内核设计与实现》(第1章-第5章)
【读书笔记】《Linux内核设计与实现》(第1章-第5章)
通读《Linux内核设计与实现》的前五章,是理解Linux内核精髓的关键一步!
这五章由浅入深,为我们搭建了一个清晰的知识框架。
(关注不迷路哈!!!)
文章目录
- 【读书笔记】《Linux内核设计与实现》(第1章-第5章)
- 💡 前言
- 一、Linux内核简介
- 二、从内核出发
- 三、进程管理
- 四、进程调度
- 五、系统调用
- 💎 总结
💡 前言
这五章的内容环环相扣,有一条清晰的逻辑主线:
- 从背景到实践:第1章奠定了理论基础和历史背景,第2章则立刻转向动手实践,告诉你如何获取和探索内核源码。
- 从静态到动态:第3章介绍了进程的静态描述(
task_struct
),而第4章则深入讲解了这些静态描述的进程是如何被动态调度和执行的。 - 从内部机制到外部接口:第3、4章聚焦于内核内部的核心机制(进程、调度),第5章则解释了用户程序如何通过系统调用这个“窗口”与这些内部机制安全地交互。
章节 | 关键要点 | 一句话精髓 |
---|---|---|
第1章 Linux内核简介 | Unix历史渊源、操作系统与内核概念、单内核与微内核架构比较、Linux内核特点与版本号规则。 | Linux内核是继承Unix设计哲学,兼具高性能与模块化特性的开源单内核。 |
第2章 从内核出发 | 获取与编译内核源码、内核源码树结构、内核开发的特点(如无libc库、小内核栈、必须考虑并发)。 | 内核开发环境独特,需掌握源码结构与编译方法,并适应其资源限制和并发要求。 |
第3章 进程管理 | 进程描述符(task_struct )、进程状态、进程创建(fork() 与写时拷贝)、线程实现、进程终结。 | 进程是内核的核心抽象,其管理围绕进程描述符展开,贯穿创建、执行到消亡的全过程。 |
第4章 进程调度 | 调度策略(如I/O消耗型 vs CPU消耗型)、调度算法(CFS完全公平调度)、优先级与时间片、抢占。 | CFS调度器的目标是公平且高效地分配CPU时间,确保系统响应性和吞吐量。 |
第5章 系统调用 | 系统调用作用、实现机制(软中断与系统调用表)、参数验证、与C库的关系。 | 系统调用是用户程序安全访问内核服务的唯一桥梁,其实现高效且安全。 |
一、Linux内核简介
Linux的“基因”来源
下图可直观地理解各小节的逻辑流向(从背景介绍到总结):
小节 | 关键内容 |
---|---|
1.1 | 核心影响: • 起源: 1969年,AT&T贝尔实验室的Ken Thompson、Dennis Ritchie等人开发。最初用汇编,1973年用C语言重写,这是操作系统可移植性的关键一步。 • 设计哲学: “KISS”原则,强调简单性、模块化(小程序协同工作)、“万物皆文件” 的抽象。这深刻影响了Linux的设计思想。 • 分裂与演化: 由于AT&T的版权问题,导致了BSD(伯克利软件发行版)、System V等分支的出现,为后来的开源运动埋下伏笔。 |
1.2 | 诞生故事: • 动机: 1991年,芬兰大学生Linus Torvalds为了学习和使用Minix系统的功能,决定开发一个比Minix更强大的、适用于386架构的自由操作系统内核。 • 著名邮件: 1991年8月25日,Linus在comp.os.minix新闻组发布了那封著名的邮件:“我正在做一个(免费的)操作系统(只是个爱好,不会很大和专业…)”。 • 命名: Linus本想将项目命名为“Freax”,但管理员Ari Lemke将其目录命名为“Linux”,此名便沿用下来。 |
1.3 | 核心定义与架构: • 内核: 操作系统常驻内存的核心部分,负责管理系统的进程、内存、设备、文件和网络。它是硬件之上的第一层软件扩展。 • 操作系统架构: 主要分为宏内核 和微内核。 - 宏内核: Linux采用。所有内核服务(调度、文件系统等)都在一个大的内核地址空间运行,性能高。 - 微内核: 只提供最基础服务,其他功能作为用户态服务运行,安全性、稳定性更好,但性能有损耗。著名的“Tanenbaum-Torvalds辩论”即源于此。 |
1.4 | 继承与超越: • 相似性: 都采用宏内核架构;在API上高度相似(都遵循POSIX);设计哲学一致。 • Linux的创新/差异: - 可移植性: Linux可从嵌入式设备运行到超级计算机,而传统Unix通常与特定硬件绑定。 - 开源与免费: Linux采用GNU GPLv2许可证,完全自由免费。传统Unix多是商业闭源的。 - 内核特性: Linux支持内核抢占、更好的SMP(对称多处理)支持、动态加载模块等现代特性,许多传统Unix内核发展缓慢。 - 单一内核映像: Linux支持通过一个内核映像引导不同的硬件配置,而许多Unix需要为特定机器编译内核。 |
1.5 | 版本编号规则: • 旧规则(3.0之前): 主版本号.次版本号.修订号 。次版本号为奇数是开发版,偶数是稳定版(如2.6.x是稳定版系列)。 • 新规则(3.0之后): 取消了奇偶编号含义,版本号变得线性。稳定版由Linus维护和发布,长期支持版由特定维护者维护数年。 • 获取渠道: 官方源为Kernel.org。 |
1.6 | 运作模式: • 金字塔结构: 最顶端是Linus Torvalds(负责合并最终代码),其下是各子系统的维护者,再下层是大量贡献者。 • 工作流程: 通过Linux内核邮件列表进行代码审查和讨论。补丁通过邮件发送,经过层层审核,最终由Linus合并到主分支。 • 文化: 崇尚技术卓越,代码质量是唯一通行证。沟通直接、高效,有时甚至显得“苛刻”。 |
1.7 | 本章定位: 为全书奠定了历史和理念基础。它阐明了Linux内核不仅是技术产物,更是Unix哲学、GNU自由软件精神和互联网协作文化的共同结晶。理解了它的过去,才能更好地理解其现在的设计选择和未来的发展方向。 |
补充说明
-
关键主题:第一章主要围绕Linux内核的背景知识展开,包括Unix遗产、Linux诞生、内核基本概念、与Unix的对比、版本管理和开发者社区。这为后续深入探讨内核设计提供了基础。
-
基因继承:Linux在精神上是Unix的直系后代,完全继承了其“简单、清晰、模块化”的设计哲学。
-
时代机遇:Linux的成功得益于GNU项目提供了丰富的用户态工具、PC硬件的普及以及互联网的兴起,使得全球协作开发成为可能。
-
开源力量:GPL许可证是Linux生态繁荣的法律基石,它保证了代码的自由共享和持续改进,形成了强大的正反馈循环。
-
实用主义:Linux在技术选择上(如坚持宏内核)体现了强烈的实用主义倾向,一切以性能和实际效果为先。
二、从内核出发
下图清晰地展示了本章的组织逻辑和从获取源码到理解开发特点的完整工作流程:
小节 | 关键内容 |
---|---|
2.1 | 核心: 介绍官方渠道和方式。 1. 使用 Git: 首选方法。克隆主线仓库 git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git ,便于跟踪最新开发和提交补丁。 2. 安装内核源代码: 从 www.kernel.org下载 tar.xz 压缩包,适合稳定版测试。 3. 使用补丁: 通过 patch 命令从旧版本升级到新版本。 |
2.2 | 核心: 理解内核源代码的目录结构,这是导航和贡献的基础。 - arch/ : 体系结构相关代码。 - drivers/ : 设备驱动程序。 - fs/ : 文件系统。 - include/ : 头文件。 - kernel/ : 核心子系统(如调度、进程)。 - mm/ : 内存管理。 |
2.3 | 核心: 将源代码转化为可执行内核映像的步骤。 1. 配置内核: make menuconfig (文本界面) 或 make xconfig (图形界面) 是最常用方式。配置选项决定内核功能和驱动。 2. 减少编译垃圾信息: 使用 make -s 或重定向输出,保持终端清洁。 3. 衍生多个编译作业: make -jN (N=CPU核数+1),极大加速编译,这是必备实践。 4. 安装新内核: 将生成的 bzImage 和 modules 安装到系统,并配置引导加载器(如 GRUB)。 |
2.4 | 本章核心! 阐述内核空间开发与用户空间程序开发的根本区别,是内核开发者的“第一课”。 |
2.4.1 | 内核是自包含的,不能链接标准 C 库 (glibc)。它实现了所需函数(如 printk , kmalloc )。只能包含内核自带的头文件。 |
2.4.2 | 内核大量使用 GNU C 扩展,不属于 ANSI C 标准: - 内联函数 (inline): 减少函数调用开销。 - 内联汇编 (asm): 直接操作硬件。 - 分支预测 (likely/unlikely): 优化条件判断性能。 |
2.4.3 | 内核访问非法内存地址会导致 “oops” 错误,严重时直接导致内核崩溃 (panic),整个系统宕机。用户空间的段错误 (Segmentation Fault) 在这里是灾难性的。 |
2.4.4 | 内核通常不保存和恢复浮点单元 (FPU) 状态,因其开销大。在中断上下文中使用浮点数会破坏用户进程的 FPU 状态。如需使用,必须手动处理 FPU 状态。 |
2.4.5 | 用户空间栈:通常很大(如 8MB)。 内核栈:很小,历史上 x86 架构只有 4KB 或 8KB。 禁止深递归或在栈上分配大数组,否则会栈溢出。 |
2.4.6 | 内核是高度并发的:多核 (SMP)、中断、软中断、内核抢占等都可能同时执行。必须使用同步机制(自旋锁、信号量、互斥体等)保护共享数据,避免竞态条件。 |
2.4.7 | Linux 内核能运行在数十种 CPU 架构上。代码必须遵循规范,避免使用体系结构相关的代码,多用宏和通用函数,以保持可移植性。 |
2.5 | 总结本章:成功获取、配置和编译了内核源码,并初步理解了内核开发环境的独特限制和挑战,为后续深入内核子系统打下了基础。 |
补充说明
- 实践导向:第二章的核心是“动手”,通过“获取-理解-编译”的流程,建立对内核代码的直观感受。
- 思维转换:2.4 节是重中之重,它明确指出了从用户空间编程到内核空间编程需要完成的思维转换:从一个受保护的、资源丰富的环境,切换到一个敏感的、资源受限的、高度并发的基础环境。理解并接受这些约束,是写出稳定、高效内核代码的前提。
三、进程管理
下图清晰地展示了本章的组织逻辑,并形象地描绘了一个进程从创建到终结的完整生命周期,以及内核如何管理它:
小节 | 关键内容 |
---|---|
3.1 | 核心概念: • 进程:正在执行的程序代码的实例。它是资源分配(内存、文件等)的基本单位。 • 线程:进程中的一条执行流。它是CPU调度的基本单位。同一进程的线程共享大部分资源(如内存空间)。 |
3.2 | 核心数据结构: 内核通过一个称为 task_struct 的巨大结构体来管理每个进程/线程的所有信息。此结构体在 <linux/sched.h> 中定义。 |
3.2.1 | 分配方式: 通过 slab 分配器(一种高效的内存管理机制)分配 task_struct 对象,便于对象复用和快速分配。 |
3.2.2 | 定位当前进程: 内核需要快速获取当前运行进程的 task_struct 。在x86-64架构中,通常通过一个每CPU变量 current 来高效地找到它。 |
3.2.3 | 进程的生命阶段: • TASK_RUNNING: 可运行或正在运行。 • TASK_INTERRUPTIBLE: 可中断的睡眠,等待信号或资源。 • TASK_UNINTERRUPTIBLE: 不可中断的睡眠,通常等待硬件I/O。 • __TASK_TRACED: 被调试器跟踪(如ptrace)。 • __TASK_STOPPED: 进程执行被中止(如收到SIGSTOP信号)。 |
3.2.4 | 状态转换: 使用 set_current_state(...) 或 set_task_state(...) 宏来安全地修改进程状态。这通常与等待队列(wait queue)一起使用。 |
3.2.5 | 可执行环境: 当一个程序在用户空间执行时,称进程处于用户上下文。当它通过系统调用或中断陷入内核空间时,称进程处于进程上下文。在进程上下文中,current 宏是有效的。 |
3.2.6 | 进程关系: 所有进程通过 task_struct 中的指针(如 parent , children , sibling )组织成一棵树。进程0(swapper )是所有进程的祖先。 |
3.3 | 创建机制: Unix哲学:进程创建通过 fork() (复制当前进程)和 exec() (加载新程序)两个步骤完成。 |
3.3.1 | 关键优化: fork() 并不立即复制父进程的整个地址空间,而是让父子进程共享相同的物理内存页,并将这些页标记为只读。当任一进程尝试写入时,才会触发页错误,此时内核再为该进程复制一个副本。 |
3.3.2 | 系统调用实现: 由 clone() 系统调用实现,传入特定的标志参数,复制父进程的资源。 |
3.3.3 | 历史遗留: 早期为在没有MMU(内存管理单元)的系统上优化而生。它创建子进程时不复制页表,并保证子进程先运行,在调用 exec() 前父进程被阻塞。 |
3.4 | 核心思想: Linux内核并不区分线程和进程。线程被视为一种与其他进程共享特定资源(主要是虚拟内存空间) 的进程。 |
3.4.1 | 实现方式: 与创建进程一样,使用 clone() 系统调用,但通过传入一组资源共享标志(如 CLONE_VM , CLONE_FS , CLONE_FILES )来指定新“线程”与当前进程共享地址空间、文件系统信息、打开的文件描述符等。 |
3.4.2 | 内核守护进程: 由内核在后台启动的特殊“进程”,没有用户空间地址空间。它们始终运行在内核空间,用于执行周期性任务(如 kswapd 内存回收、kthreadd 内核线程管理等)。 |
3.5 | 终结过程: 进程通过调用 exit() 系统调用或从主函数返回来自我终结,也可能因为收到无法处理的信号而被强制终结。 |
3.5.1 | 两步走: 1. 终止:进程被置为 EXIT_ZOMBIE 状态,释放所有资源(内存、文件等),但保留 task_struct 和PID。 2. 回收:父进程调用 wait() 系列函数后,内核才最终删除僵尸进程的 task_struct 。 |
3.5.2 | 解决方案: 如果父进程在子进程退出前先退出,这些子进程会成为“孤儿进程”。Linux的解决方案是:将它们过继给进程1(init 进程或 systemd )。init 进程会例行调用 wait() ,从而清理这些孤儿进程的僵尸状态。 |
补充说明
- 统一模型:Linux内核用统一的
task_struct
结构管理所有执行上下文,无论是进程还是线程,简化了设计和实现。线程只是共享资源的特殊进程。 - 写时拷贝:这是
fork
性能的关键,是现代操作系统的重要优化技术。 - 生命周期清晰:进程的状态转换(运行、睡眠、僵尸等)和转换条件(调度、I/O、信号等)是理解系统行为的基础。
- 资源管理:进程的创建和终结本质上是资源(内存、文件描述符表等)的分配与回收。僵尸进程的存在是为了给父进程留下最后的信息记录。
四、进程调度
下图展示了本章的知识结构,并描绘了从进程就绪到被调度执行的核心工作流,特别是CFS调度器的决策过程:
小节 | 关键内容 |
---|---|
4.1 | 核心概念: 多任务操作系统通过快速切换运行的进程,制造出多个进程同时执行的假象。 • 协作式多任务: 进程主动让出CPU(旧系统)。 • 抢占式多任务: 由调度器强制分配CPU时间,是现代系统的标准。Linux通过时间片机制实现抢占。 |
4.2 | 发展历史: 从简单的调度器演变为今天的完全公平调度器。 • O(1)调度器: 解决了早期O(n)调度器的扩展性问题,但交互性处理复杂。 • CFS: 从Linux 2.6.23开始成为默认调度器,设计理念是“公平性”。 |
4.3 | 制定调度策略所需的基础概念和权衡。 |
4.3.1 | 进程分类: • I/O消耗型: 大部分时间在等待I/O(如GUI应用)。需要高响应速度,但不需要长CPU时间。 • 处理器消耗型: 大部分时间在执行代码(如科学计算)。需要长CPU时间。 调度器目标: 既要让交互式进程快速响应,又要让CPU密集型进程获得足够的计算资源。 |
4.3.2 | 调度依据: • 静态优先级 (nice值): 用户可调(-20到19),值越小优先级越高。影响进程的基础时间片权重。 • 动态优先级: 根据进程的交互性动态调整,防止CPU饥饿。 |
4.3.3 | 传统概念: 进程被调度后允许连续运行的时间长度。CFS调度器不再有固定的时间片概念,而是基于“目标延迟”和进程权重来分配CPU比例。 |
4.3.4 | 通过实例说明调度器如何在I/O消耗型和处理器消耗型进程之间进行平衡。 |
4.4 | CFS的核心设计思想。 |
4.4.1 | 模块化设计: Linux调度器被设计成一组调度类。每个调度类代表一种调度策略(如stop_sched_class , dl_sched_class , rt_sched_class , fair_sched_class )。调度器按照优先级依次遍历这些类。 |
4.4.2 | 分析传统Unix调度器(如SDL)的缺点: 为交互进程动态提高优先级的方法基于启发式规则,复杂且易产生“巫毒常量”(magic number),在不同负载下表现不稳定。 |
4.4.3 | CFS哲学: “完全公平”不是指时间片相等,而是指CPU时间比例的公平。一个进程的权重由nice值决定,它应获得的CPU时间比例应为 weight / sum(weight_of_all_runnable_tasks) 。 |
4.5 | CFS的具体实现细节,是本章最技术性的部分。 |
4.5.1 | 虚拟运行时(vruntime): CFS的核心数据结构。每个进程维护自己的vruntime ,它表示该进程已经运行的时间量经过其优先级(权重)加权后的值。调度器总是选择**vruntime 最小的进程**运行。 |
4.5.2 | 数据结构: 所有可运行进程按其vruntime 为键,存放在一个红黑树中。取vruntime 最小的进程就是取红黑树的最左子节点,这是一个O(log N)的高效操作。 |
4.5.3 | 核心函数: schedule() 是主调度函数。它会从最高优先级的非空调度类中选取下一个要运行的进程。 |
4.5.4 | 阻塞处理: 进程因等待资源(如I/O、锁)而睡眠(状态置为TASK_INTERRUPTIBLE 等),并从运行队列(红黑树)中移除。当资源可用时,通过wake_up() 函数将进程重新放回运行队列。 |
4.6 | 切换时机: 由context_switch() 函数完成进程切换(切换地址空间、硬件上下文等)。 • 用户抢占: 发生在从系统调用或中断处理返回用户空间时。 • 内核抢占: 允许在内核态执行的进程被高优先级进程抢占(需配置 CONFIG_PREEMPT )。 |
4.7 | 硬实时支持: Linux提供两种实时调度策略,其优先级高于普通(CFS)进程。 • SCHED_FIFO: 先进先出,会一直运行直到阻塞或主动让出。 • SCHED_RR: 时间片轮转,是带时间片的SCHED_FIFO。 |
4.8 | 供用户空间控制和查询调度行为的接口。 |
4.8.1 4.8.2 4.8.3 | • 策略与优先级: nice() , sched_setscheduler() , sched_getscheduler() 。 • 处理器绑定: sched_setaffinity() , sched_getaffinity() ,将进程绑定到特定CPU核心,利于缓存亲和性。 • 放弃处理器: sched_yield() ,进程主动暂时放弃CPU。 |
补充说明
- 公平性与效率:CFS的核心思想是比例公平,通过
vruntime
和红黑树高效地实现了这一目标,避免了传统调度算法的启发式规则带来的不确定性。 - 模块化与优先级:调度器类的设计使得Linux可以同时支持普通进程的公平调度和实时进程的严格优先级调度,应用场景广泛。
- 抢占机制:内核抢占是Linux响应性的一大飞跃,使得系统在执行内核代码时也能及时响应高优先级任务。
vruntime
是灵魂:理解vruntime
如何随着实际运行时间和进程优先级变化,是理解CFS工作原理的关键。
五、系统调用
下图清晰地展示了一个系统调用从用户发起到最后返回的完整生命周期,以及内核在其中执行的关键步骤和所处的上下文环境:
小节 | 关键内容 |
---|---|
5.1 | 核心作用: 系统调用是用户空间应用程序主动向内核发起请求的唯一合法途径。它提供了硬件抽象、系统稳定性(防止用户程序随意访问硬件)和安全性的基石。 |
5.2 | 层次关系: • POSIX标准: 定义了应用程序接口(API)的行为规范,如 open , read 等函数应该怎么做。 • C库 (glibc): 实现了POSIX标准,提供了系统调用的封装函数。用户通常调用C库函数,由C库负责触发真正的系统调用。 |
5.3 | 系统调用的基本特性。 |
5.3.1 | 唯一标识符: 每个系统调用被分配一个唯一的数字编号(如 __NR_read )。当触发系统调用时,这个号被放入一个特定寄存器(如eax ),内核据此查找系统调用表,跳转到正确的处理函数。 |
5.3.2 | • 性能分析: 系统调用比普通函数调用慢,因为它涉及硬件上下文切换(从用户态切换到内核态)、寄存器保存恢复、缓存失效等开销。 • 优化机制: Linux引入了更快的 sysenter /sysexit 指令对来替代传统的 int 0x80 软中断。x86-64上使用 syscall /sysret 指令,进一步降低了开销。 |
5.4 | 内核响应系统调用的详细过程。 |
5.4.1 | 路由过程: 用户空间将系统调用号存入 eax 寄存器。系统调用处理程序(entry_SYSCALL_64 )根据此号,在 sys_call_table 中进行索引,如果号有效,则执行 sys_call_table[n] 指向的函数。 |
5.4.2 | 参数规则: 系统调用的参数通过寄存器传递(x86-64下依次为 rdi , rsi , rdx , r10 , r8 , r9 )。参数个数被限制在6个以内。若超过,则需通过一个指针传递一个结构体。 |
5.5 | 如何在内核中实现一个自己的系统调用。 |
5.5.1 | 实现步骤: 1. 定义函数:使用 asmlinkage 宏明确参数从栈上获取,函数返回 long ,错误时返回负数错误码。 2. 在系统调用表中添加条目。 3. 为新系统调用号生成头文件。 |
5.5.2 | 安全生命线! 内核绝不能信任来自用户空间的任何数据。必须进行严格检查: • 指针有效性: 使用 copy_from_user 、get_user 等函数来安全地从用户空间读取数据。这些函数会检查地址是否在用户空间且进程有权限访问。 • 权限检查: 调用 capable() 检查进程的权限(如 CAP_SYS_ADMIN )。 • 数值有效性: 检查范围、标志位是否合法。 |
5.6 | 系统调用执行时所处的内核环境。 |
5.6.1 | 注册流程: 更新编译系统(如Makefile, Kconfig),使得新系统调用能被编译进内核。系统调用通常是内核核心的一部分,而非可加载模块。 |
5.6.2 | 调用方式: 1. 通过C库: 标准、可移植的方式。 2. 使用 syscall 函数: glibc 提供的通用接口,可以直接指定系统调用号和参数。 3. 内联汇编: 不推荐,用于特殊场景或底层编程。 |
5.6.3 | 设计哲学: 并非所有内核功能都适合做成系统调用。过度增加系统调用会使内核API膨胀、笨重。许多功能通过以下方式暴露更合适: • /proc 或 /sys (sysfs): 用于输出简单的状态信息或配置。 • 设备文件: 用于控制硬件。 • netlink: 用于内核与用户空间的网络配置通信。 • 信号: 用于异步通知。 |
补充说明
- 安全门卫:系统调用是用户态进入内核态的唯一安全门卫,其设计和实现的首要原则是安全性和稳定性。
- 性能开销:系统调用有不可忽视的开销,程序设计应尽量减少其调用次数(例如,使用缓冲区)。
- 绝不信任用户空间:这是内核开发的铁律。所有从用户空间传入的参数必须经过严格验证。
- 灵活的接口暴露:系统调用是重要机制,但并非唯一机制。Linux通过虚拟文件系统(如
/proc
,sysfs
)等方式提供了更灵活的交互接口,遵循了Unix的“万物皆文件”哲学。
💎 总结
简单来说,Linux内核是一个庞大而精密的系统:
- 它通过进程描述符(第3章)来管理所有任务;
- 并利用先进的调度算法(第4章)公平高效地分配CPU资源;
- 而用户程序则通过系统调用(第5章)这个安全通道来请求内核的服务;
- 所有这些代码都组织在一个结构清晰的源码树中(第2章);
- 并继承了Unix简单而强大的设计哲学(第1章)。