内核编程基础理论
理解内核编程的核心概念和设计约束。
内核空间 vs 用户空间
编程环境的根本差异
用户空间编程:
- 有完整的 C 标准库
- 可以使用浮点运算
- 允许阻塞和睡眠
- 有虚拟内存保护
- 错误影响单个进程
内核空间编程:
- 没有标准库(只有内核 API)
- 不能使用浮点运算(会破坏用户进程状态)
- 某些上下文不能睡眠(中断、自旋锁内)
- 直接访问物理内存
- 错误可能导致系统崩溃
为什么这些限制
无标准库:
- 内核必须自给自足
- 减小内核体积
- 提供专用的内核 API
禁止浮点:
- FPU 状态属于用户进程
- 保存/恢复 FPU 状态开销大
- 内核计算通常不需要浮点
睡眠限制:
- 中断处理必须快速返回
- 自旋锁持有期间不能睡眠(死锁风险)
- 原子上下文不能调度
内核编程约束
栈空间限制
用户空间:
- 栈大小:8MB(可配置)
- 栈溢出触发段错误
- 影响单个进程
内核空间:
- 栈大小:通常 8KB 或 16KB
- 所有内核代码共享(每 CPU 一个)
- 栈溢出可能悄无声息地破坏数据
编程建议:
- 避免大型局部变量
- 不要深度递归
- 使用动态分配(kmalloc)处理大数据
同步原语选择
可睡眠上下文:
- 进程上下文
- 可以使用互斥锁(mutex)
- 可以使用信号量(semaphore)
- 允许阻塞操作
原子上下文:
- 中断处理程序
- 自旋锁持有期间
- 只能使用自旋锁
- 禁止睡眠和调度
判断方法:
in_interrupt() // 是否在中断上下文
in_atomic() // 是否在原子上下文
抢占和并发
内核抢占:
- 内核代码执行时可能被抢占
- 需要保护共享数据
- 临界区需要禁用抢占
并发来源:
- 多处理器:不同 CPU 同时执行
- 内核抢占:高优先级任务抢占
- 中断:随时可能发生
- 软中断:延迟处理
保护机制:
- 自旋锁:短期互斥
- 互斥锁:长期互斥(可睡眠)
- 读写锁:读多写少场景
- RCU:读多极端场景
内核模块机制
模块的生命周期
加载阶段:
- 内核分配内存
- 加载模块到内核空间
- 解析符号引用
- 调用模块初始化函数
运行阶段:
- 模块代码常驻内核
- 提供功能(设备驱动、文件系统等)
- 与内核其他部分交互
卸载阶段:
- 检查模块引用计数
- 调用模块清理函数
- 释放内存
- 移除符号导出
模块参数
设计目的:
- 模块行为可配置
- 无需重新编译
- 运行时可调整(某些参数)
参数类型:
- 基本类型: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_HANDLED或IRQ_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 支持
思考题:
- 为什么内核代码不能使用标准 C 库?
- 中断处理为什么必须快速返回?
- 什么情况下应该使用 kmalloc 而非 vmalloc?