2. Linux 内核架构基础:核心概念与故障定位视角
在进入具体的 crash 与 kdump 实战之前,必须建立对 Linux 内核架构的清晰认知。故障定位的本质,是在内核空间与用户空间的边界、系统调用路径、中断/异常上下文以及进程/内存管理的数据结构中寻找异常痕迹。本章从故障诊断工程师的视角,梳理这些核心概念。
2.1 内核空间与用户空间
Linux 操作系统将虚拟地址空间划分为两个特权级区域:用户空间(User Space)和内核空间(Kernel Space)。这种隔离是系统安全与稳定的基石。
- 用户空间:运行应用程序,使用虚拟地址的低位部分(如 0x00000000 – 0xBFFFFFFF,32 位典型划分)。CPU 运行在 Ring 3 级别,无法直接访问硬件或内核数据结构。
- 内核空间:运行操作系统内核代码、驱动、内核模块。使用虚拟地址的高位部分(如 0xC0000000 – 0xFFFFFFFF)。CPU 运行在 Ring 0 级别,拥有最高权限。
故障诊断要点: 当 crash 工具分析 vmcore 时,首先需要区分当前 CPU 处于用户态还是内核态。通过查看
struct pt_regs 中的 CS 寄存器(代码段选择子)可以判断特权级。内核态崩溃通常指向内核 bug 或驱动问题;用户态崩溃则可能由非法指针、栈溢出等引起,但若用户态传递了错误参数给系统调用,也可能导致内核路径异常。
2.2 系统调用:用户与内核的桥梁
系统调用(System Call)是用户空间程序请求内核服务的唯一合法入口。当应用程序需要打开文件、分配内存、创建进程时,必须通过系统调用陷入内核。
系统调用流程
- 用户态准备:应用程序调用 glibc 封装的函数(如
read()),将参数放入特定寄存器,并触发软中断(如int 0x80)或使用syscall/sysenter指令。 - 陷入内核:CPU 切换到 Ring 0,保存用户态上下文(寄存器、栈指针),跳转到内核预置的入口点(如
entry_SYSCALL_64)。 - 系统调用分发:内核根据系统调用号(如
__NR_read为 0)在sys_call_table中查找对应的处理函数。 - 执行与返回:执行内核函数,将结果写入寄存器,恢复用户态上下文,切换回 Ring 3。
| 系统调用 | 功能 | 常见故障场景 |
|---|---|---|
read() / write() |
文件 I/O | 文件系统损坏、块设备驱动 bug 导致内核 panic |
mmap() |
内存映射 | 映射非法地址、触发缺页异常(page fault)导致 Oops |
ioctl() |
设备控制 | 传递错误命令或参数,触发驱动中的空指针解引用 |
clone() / fork() |
进程创建 | 资源耗尽、内核栈分配失败导致系统挂死 |
crash 分析提示: 在 vmcore 中,通过回溯
sys_call_table 和查看 struct pt_regs 中的 orig_ax(系统调用号)与参数寄存器,可以精确还原用户程序发起的系统调用请求。这是判断故障是否由用户态非法输入触发的关键证据。
2.3 中断与异常:内核的异步与同步事件
中断和异常是 CPU 响应外部设备或内部指令异常的事件机制。它们会强制 CPU 切换到内核态,执行预定义的处理程序。
- 中断(Interrupt):异步事件,由硬件设备(如网卡、磁盘控制器)产生。中断处理程序(ISR)在关闭本地中断的上下文中执行,要求快速完成,通常将耗时工作推迟到软中断(SoftIRQ)或 tasklet 中。
- 异常(Exception):同步事件,由 CPU 执行指令时产生。包括:
- 故障(Fault):如缺页异常(Page Fault),可恢复,处理完成后重新执行触发指令。
- 陷阱(Trap):如系统调用(
int 0x80),用于主动请求内核服务。 - 中止(Abort):如机器检查异常(Machine Check),通常不可恢复,直接导致系统崩溃。
2.4 进程调度核心概念
进程调度器(Scheduler)负责在多个可运行进程之间分配 CPU 时间。Linux 使用完全公平调度器(CFS, Completely Fair Scheduler)作为默认调度策略。
关键数据结构
- task_struct:进程描述符,包含进程的所有信息(PID、状态、优先级、打开文件、内存描述符
mm_struct、内核栈等)。 - 调度实体(sched_entity):CFS 调度的基本单位,内嵌在
task_struct中,包含虚拟运行时间(vruntime)。 - 运行队列(rq):每个 CPU 一个,包含红黑树(CFS 就绪队列)和实时进程队列。
| 进程状态 | 内核宏 | 故障诊断含义 |
|---|---|---|
| 运行 (Running) | TASK_RUNNING | 进程正在执行或等待调度。若大量进程处于此状态且 CPU 繁忙,可能为死循环或资源争抢。 |
| 可中断睡眠 (Interruptible) | TASK_INTERRUPTIBLE | 等待 I/O 或信号。若大量进程阻塞于此,可能为 I/O 瓶颈或死锁。 |
| 不可中断睡眠 (Uninterruptible) | TASK_UNINTERRUPTIBLE | 等待内核态资源(如磁盘 I/O)。若进程长期处于此状态,通常表示内核或驱动 bug,是 crash 分析的重点。 |
| 僵尸 (Zombie) | EXIT_ZOMBIE | 进程已终止但父进程未回收。大量僵尸进程可能耗尽 PID 空间。 |
| 停止 (Stopped) | TASK_STOPPED / TASK_TRACED | 收到 SIGSTOP 或被调试器暂停。 |
crash 命令实战: 在 vmcore 中,使用
ps 命令查看所有进程状态;使用 task 命令查看特定 task_struct 的详细信息;使用 runq 命令查看每个 CPU 的运行队列,判断是否存在调度异常。
2.5 内存管理核心概念
内存管理子系统负责虚拟地址到物理地址的映射、物理内存分配与回收、以及用户进程地址空间的管理。
核心组件
- MMU(内存管理单元):硬件单元,负责将虚拟地址转换为物理地址。转换基于页表(Page Table)。
- 页表(Page Table):多级结构(如 x86_64 的 4 级页表:PGD -> P4D -> PUD -> PMD -> PTE),每个表项指向下一级或物理页帧。
- struct mm_struct:进程的内存描述符,包含页表基址(PGD)、代码段/数据段/堆/栈的虚拟地址区间。
- 伙伴系统(Buddy System):物理内存分配算法,以 2 的幂次页块为单位管理空闲内存。
- slab 分配器:用于内核对象(如
task_struct、inode)的缓存分配,避免频繁的伙伴系统操作。
内存故障常见模式
- 空指针解引用(NULL Pointer Dereference):访问虚拟地址 0x0,触发缺页异常,导致 Oops 或 panic。这是最常见的 crash 原因。
- 释放后使用(Use-After-Free):访问已释放的内存对象,slab 分配器中的对象被重用后,旧指针仍被引用。
- 双倍释放(Double Free):对同一内存块调用两次
kfree(),破坏 slab 分配器的内部链表。 - 内存泄漏(Memory Leak):内核模块或驱动未释放已分配的内存,导致系统内存耗尽,触发 OOM Killer。
crash 分析技巧: 当遇到疑似内存故障时,使用
kmem -i 查看系统内存使用概况;使用 slab 命令查看 slab 缓存状态;使用 search 命令在内存中搜索特定模式或指针,帮助定位损坏的对象。