2. Linux 内核基础回顾:内核空间与用户空间,系统调用接口,进程管理基础,内存管理基础

在深入 Android 内核驱动开发之前,必须牢固掌握 Linux 内核的几项核心基础。本章将系统回顾内核空间与用户空间的隔离机制、系统调用接口(SCI)、进程管理以及内存管理的基本原理。这些概念是理解驱动程序如何与操作系统其他部分交互的基石。

2.1 内核空间与用户空间

现代操作系统通过硬件支持的 特权级(Privilege Level) 实现安全隔离。Linux 内核主要使用两种特权级:内核空间(Kernel Space) 和 用户空间(User Space)。驱动程序运行在内核空间,拥有最高权限;应用程序运行在用户空间,权限受限。

核心原则: 用户空间程序不能直接访问内核空间的内存地址或执行特权指令。所有对硬件资源的访问都必须通过内核提供的接口进行。

2.1.1 隔离机制与地址空间划分

在 32 位系统中,虚拟地址空间通常按 3:1 比例划分:用户空间占 0x00000000 – 0xBFFFFFFF(3GB),内核空间占 0xC0000000 – 0xFFFFFFFF(1GB)。在 64 位系统中,划分比例不同,但隔离原理一致。

驱动开发提示: 驱动程序运行在内核空间,因此必须极其谨慎地处理来自用户空间的指针。直接解引用用户空间指针会导致严重的安全漏洞或内核崩溃。必须使用 copy_from_user() 和 copy_to_user() 等专用 API。
Linux 内核空间与用户空间隔离架构 用户空间 (User Space) 应用程序 (App, Service, Daemon) 受限指令集 · 独立虚拟地址空间 通过系统调用陷入内核 系统调用接口 (SCI) 内核空间 (Kernel Space) 进程管理 · 内存管理 · 文件系统 · 网络协议栈 设备驱动程序 · 系统调用处理 完全访问硬件 · 高特权级 (Ring 0) 硬件层 (CPU, Memory, Devices)

2.2 系统调用接口

系统调用是用户空间程序请求内核服务的唯一入口。当应用程序执行 open()、read()、write() 等函数时,实际上是通过 系统调用接口(System Call Interface, SCI) 陷入内核。

2.2.1 系统调用流程

  1. 用户态发起: 应用程序调用 glibc 封装函数(如 read(fd, buf, count))。
  2. 陷入内核: 封装函数将系统调用号存入寄存器(如 x86 的 eax),并执行 int 0x80 或 syscall 指令,CPU 切换到内核态。
  3. 系统调用处理: 内核中的 system_call 入口根据系统调用号查找 sys_call_table,调用对应的内核函数。
  4. 执行与返回: 内核函数完成操作(如从设备读取数据),将结果返回用户空间,CPU 切换回用户态。
驱动开发关键: 驱动程序通常不直接实现系统调用,而是通过注册 file_operations 结构体来响应系统调用。例如,当用户程序调用 read() 时,VFS 层最终会调用驱动中注册的 .read 回调函数。

2.2.2 系统调用号与 sys_call_table

系统调用号 函数名 用途
0 sys_read 从文件描述符读取数据
1 sys_write 向文件描述符写入数据
2 sys_open 打开或创建文件
39 sys_getpid 获取当前进程 ID
60 sys_exit 终止当前进程

2.3 进程管理基础

进程是资源分配的基本单位,线程是 CPU 调度的基本单位。Linux 内核通过 task_struct 结构体管理所有进程和线程。

2.3.1 进程描述符 task_struct

每个进程在内核中都有一个 task_struct 实例,包含进程的所有信息:

  • 进程状态: TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE、TASK_STOPPED、TASK_ZOMBIE。
  • 进程标识: PID(进程 ID)、TGID(线程组 ID)。
  • 调度信息: 优先级、调度策略(CFS、实时)、时间片。
  • 内存信息: mm_struct 指针,描述进程地址空间。
  • 文件系统信息: fs_struct、files_struct。
  • 信号处理: 信号掩码、信号处理函数。
进程管理核心结构:task_struct task_struct state (进程状态) pid (进程ID) mm (内存描述符) files (打开文件) sched_entity (调度实体) signal (信号处理)

2.3.2 进程生命周期与调度

Linux 使用 完全公平调度器(CFS) 作为默认调度算法。CFS 基于虚拟运行时间(vruntime)选择下一个运行的进程,确保所有进程公平获得 CPU 时间。

驱动上下文: 驱动程序通常运行在进程上下文中(如 read() 调用),此时可以睡眠、持有锁、访问用户空间。中断处理程序运行在中断上下文中,不能睡眠,不能持有可重入锁。

2.4 内存管理基础

Linux 内存管理负责虚拟地址到物理地址的映射、内存分配与回收、页表管理。驱动程序开发者必须理解 虚拟地址、物理地址 和 总线地址 的区别。

2.4.1 虚拟内存与分页机制

每个进程拥有独立的 4GB(32位)虚拟地址空间。MMU(内存管理单元)通过 页表(Page Table) 将虚拟地址转换为物理地址。页表以 页(Page) 为单位,标准页大小为 4KB。

概念 说明 驱动开发关注点
虚拟地址 (VA) 进程看到的地址 用户空间指针必须通过 copy_from_user 访问
物理地址 (PA) 硬件总线上的真实地址 DMA 操作需要物理地址
内核逻辑地址 直接映射到物理内存的地址(ZONE_NORMAL) 使用 kmalloc 分配,物理连续
内核虚拟地址 通过 vmalloc 分配的地址,物理不连续 适用于大块内存分配
虚拟地址到物理地址的转换(分页机制) 虚拟地址 (VA) 0x00401000 (进程视角) MMU + 页表 地址转换 物理地址 0x1A3F4000 页表转换过程 (x86 四级页表示例) PGD PUD PMD PTE 页 虚拟地址被分割为多个索引,逐级查找页表项,最终得到物理页帧号

2.4.2 内核内存分配 API

驱动程序开发中常用的内存分配函数:

  • kmalloc(size, flags): 分配物理连续的内存,适用于小到中等大小(通常 < 128KB)。flags 常用 GFP_KERNEL(可睡眠)或 GFP_ATOMIC(不可睡眠)。
  • kzalloc(size, flags): 与 kmalloc 类似,但分配后自动将内存清零。
  • vmalloc(size): 分配虚拟连续但物理不连续的内存,适用于大块内存(如 > 128KB)。
  • alloc_pages(gfp_mask, order): 分配 2^order 个连续的物理页,返回 struct page *。
  • dma_alloc_coherent(dev, size, &dma_handle, gfp_mask): 分配 DMA 缓冲区,返回内核虚拟地址和 DMA 总线地址。
最佳实践: 在驱动中优先使用 kzalloc 分配结构体,避免未初始化数据导致的安全问题。对于 DMA 操作,必须使用 dma_alloc_coherent 或 dma_map_single 确保缓存一致性。

2.4.3 内存区域与伙伴系统

Linux 内核将物理内存划分为不同的 管理区(Zone):

管理区 范围 用途
ZONE_DMA 0 – 16MB 用于 ISA 设备 DMA 操作(历史原因)
ZONE_DMA32 0 – 4GB 用于 32位 DMA 设备
ZONE_NORMAL 16MB – 896MB (32位) 直接映射到内核逻辑地址空间
ZONE_HIGHMEM > 896MB (32位) 不能直接映射,需要 kmap 临时映射

内核使用 伙伴系统(Buddy System) 管理物理页的分配和回收,以 2 的幂次方页块为单位,有效减少外部碎片。

Android 特有考虑: Android 设备通常使用 64 位内核,ZONE_HIGHMEM 不再存在。但 Ion 内存分配器 和 dmabuf 是 Android 驱动开发中必须掌握的内存管理框架,用于在驱动和用户空间之间高效共享内存。