内核编程基础理论

理解内核编程的核心概念和设计约束。

内核空间 vs 用户空间

编程环境的根本差异

用户空间编程

  • 有完整的 C 标准库
  • 可以使用浮点运算
  • 允许阻塞和睡眠
  • 有虚拟内存保护
  • 错误影响单个进程

内核空间编程

  • 没有标准库(只有内核 API)
  • 不能使用浮点运算(会破坏用户进程状态)
  • 某些上下文不能睡眠(中断、自旋锁内)
  • 直接访问物理内存
  • 错误可能导致系统崩溃

为什么这些限制

无标准库

  • 内核必须自给自足
  • 减小内核体积
  • 提供专用的内核 API

禁止浮点

  • FPU 状态属于用户进程
  • 保存/恢复 FPU 状态开销大
  • 内核计算通常不需要浮点

睡眠限制

  • 中断处理必须快速返回
  • 自旋锁持有期间不能睡眠(死锁风险)
  • 原子上下文不能调度

内核编程约束

栈空间限制

用户空间

  • 栈大小:8MB(可配置)
  • 栈溢出触发段错误
  • 影响单个进程

内核空间

  • 栈大小:通常 8KB 或 16KB
  • 所有内核代码共享(每 CPU 一个)
  • 栈溢出可能悄无声息地破坏数据

编程建议

  • 避免大型局部变量
  • 不要深度递归
  • 使用动态分配(kmalloc)处理大数据

同步原语选择

可睡眠上下文

  • 进程上下文
  • 可以使用互斥锁(mutex)
  • 可以使用信号量(semaphore)
  • 允许阻塞操作

原子上下文

  • 中断处理程序
  • 自旋锁持有期间
  • 只能使用自旋锁
  • 禁止睡眠和调度

判断方法

in_interrupt()  // 是否在中断上下文
in_atomic()     // 是否在原子上下文

抢占和并发

内核抢占

  • 内核代码执行时可能被抢占
  • 需要保护共享数据
  • 临界区需要禁用抢占

并发来源

  1. 多处理器:不同 CPU 同时执行
  2. 内核抢占:高优先级任务抢占
  3. 中断:随时可能发生
  4. 软中断:延迟处理

保护机制

  • 自旋锁:短期互斥
  • 互斥锁:长期互斥(可睡眠)
  • 读写锁:读多写少场景
  • RCU:读多极端场景

内核模块机制

模块的生命周期

加载阶段

  1. 内核分配内存
  2. 加载模块到内核空间
  3. 解析符号引用
  4. 调用模块初始化函数

运行阶段

  • 模块代码常驻内核
  • 提供功能(设备驱动、文件系统等)
  • 与内核其他部分交互

卸载阶段

  1. 检查模块引用计数
  2. 调用模块清理函数
  3. 释放内存
  4. 移除符号导出

模块参数

设计目的

  • 模块行为可配置
  • 无需重新编译
  • 运行时可调整(某些参数)

参数类型

  • 基本类型:int、long、bool、charp
  • 数组:支持多值参数
  • 权限:控制 /sys 可见性

符号导出

为什么导出符号

  • 模块间共享功能
  • 避免代码重复
  • 模块化设计

导出类型

  • EXPORT_SYMBOL():所有模块可用
  • EXPORT_SYMBOL_GPL():仅 GPL 模块可用

符号版本

  • 内核符号版本控制
  • 防止 ABI 不兼容
  • 模块加载时检查

设备驱动基础

设备类型

字符设备

  • 顺序访问(如串口、键盘)
  • 以字节流方式读写
  • 不支持随机访问

块设备

  • 随机访问(如硬盘、U盘)
  • 以块为单位读写
  • 支持缓存和调度

网络设备

  • 数据包收发
  • 不使用文件操作接口
  • 专用的网络子系统 API

设备号

主设备号

  • 标识驱动程序
  • 内核用于查找驱动
  • 传统上是静态分配

次设备号

  • 标识具体设备
  • 驱动内部使用
  • 区分同一驱动的多个设备

设备号管理

  • 静态分配(固定设备号)
  • 动态分配(推荐方式)
  • 注册和注销

文件操作接口

核心概念

  • 驱动实现标准文件操作
  • 用户空间通过文件接口访问设备
  • VFS 层屏蔽具体驱动差异

主要操作

  • open:打开设备,初始化
  • release:关闭设备,清理
  • read:从设备读取数据
  • write:向设备写入数据
  • ioctl:设备控制操作
  • mmap:内存映射

中断处理

中断上下文特点

严格限制

  • 不能睡眠
  • 不能访问用户空间
  • 不能调用可能阻塞的函数
  • 尽可能快速返回

为什么

  • 中断可能在任意时刻发生
  • 中断处理期间可能禁止其他中断
  • 长时间处理影响系统响应性

中断处理策略

顶半部(Top Half)

  • 在中断上下文执行
  • 时间敏感的紧急处理
  • 确认中断、读取数据
  • 调度底半部

底半部(Bottom Half)

  • 延迟处理耗时工作
  • 可以睡眠和调度
  • 处理数据、唤醒进程

实现机制

  • Softirq:高优先级,静态定义
  • Tasklet:基于 softirq,动态注册
  • 工作队列:可睡眠上下文

中断共享

概念

  • 多个设备共享一条中断线
  • 节省中断资源
  • PCI 设备常见

实现要求

  • 设置 IRQF_SHARED 标志
  • 检查是否是自己的中断
  • 返回 IRQ_HANDLEDIRQ_NONE

内存管理

内核内存分配

kmalloc

  • 物理地址连续
  • 适合小内存(< 128KB)
  • 可能失败(内存不足)
  • 有多种分配标志

vmalloc

  • 虚拟地址连续
  • 物理地址可不连续
  • 适合大内存
  • 性能略低(TLB 映射)

分配标志(GFP)

  • GFP_KERNEL:可睡眠,进程上下文
  • GFP_ATOMIC:不可睡眠,中断上下文
  • GFP_DMA:DMA 可访问内存
  • GFP_HIGHUSER:高端内存

用户空间访问

为什么需要特殊函数

  • 用户空间地址可能无效
  • 页面可能未映射(缺页)
  • 安全检查

安全访问函数

  • copy_to_user():内核 → 用户
  • copy_from_user():用户 → 内核
  • get_user():读取单个值
  • put_user():写入单个值

错误处理

  • 返回未复制的字节数
  • 0 表示成功
  • 非 0 表示部分或完全失败

调试技术

打印调试

printk 日志级别

  • KERN_EMERG:系统不可用
  • KERN_ERR:错误条件
  • KERN_WARNING:警告
  • KERN_INFO:信息
  • KERN_DEBUG:调试信息

动态调试

  • pr_debug() 宏
  • 运行时开启/关闭
  • 减少性能影响

内核调试器

KGDB

  • 远程调试
  • 需要串口或网络连接
  • 类似 GDB 体验

KASAN

  • 地址消毒器
  • 检测内存错误
  • 越界访问、使用释放后内存

lockdep

  • 锁依赖检测
  • 发现潜在死锁
  • 开发阶段启用

性能分析

perf

  • CPU 性能计数器
  • 热点函数分析
  • 火焰图可视化

ftrace

  • 函数跟踪
  • 延迟分析
  • 事件追踪

最佳实践

代码风格

内核编码规范

  • Tab 缩进(8 字符)
  • 80 列限制
  • 函数简短
  • 清晰的命名

错误处理

  • 检查所有返回值
  • 使用 goto 清理资源
  • 返回负的错误码

可移植性

字节序

  • 网络字节序(大端)
  • CPU 字节序可能不同
  • 使用转换宏

对齐

  • 数据结构对齐
  • DMA 缓冲区对齐
  • 平台相关差异

安全考虑

输入验证

  • 检查用户空间参数
  • 防止缓冲区溢出
  • 整数溢出检查

权限检查

  • capable() 函数
  • UID/GID 检查
  • SELinux/AppArmor 支持

思考题

  1. 为什么内核代码不能使用标准 C 库?
  2. 中断处理为什么必须快速返回?
  3. 什么情况下应该使用 kmalloc 而非 vmalloc?