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。
2.2 系统调用接口
系统调用是用户空间程序请求内核服务的唯一入口。当应用程序执行 open()、read()、write() 等函数时,实际上是通过 系统调用接口(System Call Interface, SCI) 陷入内核。
2.2.1 系统调用流程
- 用户态发起: 应用程序调用 glibc 封装函数(如
read(fd, buf, count))。 - 陷入内核: 封装函数将系统调用号存入寄存器(如 x86 的
eax),并执行int 0x80或syscall指令,CPU 切换到内核态。 - 系统调用处理: 内核中的
system_call入口根据系统调用号查找sys_call_table,调用对应的内核函数。 - 执行与返回: 内核函数完成操作(如从设备读取数据),将结果返回用户空间,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。 - 信号处理: 信号掩码、信号处理函数。
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 分配的地址,物理不连续 | 适用于大块内存分配 |
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 的幂次方页块为单位,有效减少外部碎片。