2. Linux内核与驱动基础
在嵌入式Linux驱动开发中,理解内核的整体架构是第一步。Linux内核并不是一个“大泥球”,而是由进程调度、内存管理、虚拟文件系统、网络协议栈、设备驱动等子系统分层协作构成的。驱动开发者最常打交道的是VFS层、设备模型和具体总线框架。内核空间与用户空间严格隔离,驱动代码运行在内核态,任何越界访问都可能直接导致系统崩溃,因此“先理解结构,再写代码”是必须遵守的原则。
一、内核架构与模块机制
Linux内核支持两种驱动集成方式:静态编译进内核(built-in)和动态加载模块(Loadable Kernel Module,LKM)。模块机制让驱动可以按需加载、卸载,极大方便了调试与裁剪。模块本质上是.ko文件,通过insmod、rmmod、modprobe等命令管理。
一个最小内核模块包含入口和出口函数,使用module_init()与module_exit()注册。模块必须声明许可证,否则内核会标记为“污染”。
#include <linux/module.h>
#include <linux/init.h>
static int __init demo_init(void)
{
pr_info("demo module loaded\n");
return 0;
}
static void __exit demo_exit(void)
{
pr_info("demo module unloaded\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("driver-team");
MODULE_DESCRIPTION("minimal lkm demo");
编译模块需要内核头文件或已配置的内核源码树。典型Makefile如下:
obj-m += demo.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
lsmod查看,用dmesg | tail观察打印信息;卸载前确保没有进程正在使用该模块,否则会返回“Device or resource busy”。二、设备模型:kobject、kset与device
Linux 2.6之后引入统一设备模型,核心是kobject、kset、device、driver和bus。kobject提供引用计数和sysfs入口,kset是kobject的集合,device表示具体硬件实例,driver表示驱动实现,bus负责匹配二者。设备模型解决了电源管理、热插拔、设备树和驱动绑定等系统性问题。
驱动与设备的匹配通常通过总线完成。以platform总线为例,设备树中的compatible属性与驱动中的of_device_id表进行匹配,匹配成功后调用驱动的probe()函数。
static const struct of_device_id demo_of_match[] = {
{ .compatible = "vendor,demo-device" },
{ }
};
MODULE_DEVICE_TABLE(of, demo_of_match);
static struct platform_driver demo_driver = {
.probe = demo_probe,
.remove = demo_remove,
.driver = {
.name = "demo-driver",
.of_match_table = demo_of_match,
},
};
module_platform_driver(demo_driver);
三、sysfs与procfs:内核与用户空间的窗口
sysfs挂载在/sys,以“一文件一属性”的方式暴露设备、驱动、总线、电源等信息,是设备模型的可视化接口。常见路径包括/sys/class、/sys/devices、/sys/bus、/sys/module。驱动可通过DEVICE_ATTR或sysfs_create_group()导出属性,实现用户态读写。
procfs挂载在/proc,偏重运行时状态与调试信息,如/proc/cpuinfo、/proc/interrupts、/proc/meminfo。驱动可用proc_create()创建调试节点。两者定位不同:sysfs强调“设备与驱动的结构化视图”,procfs强调“内核运行状态与统计”。
四、驱动开发环境搭建与验证流程
驱动开发环境通常由“宿主机 + 目标板”组成。宿主机安装交叉编译工具链、内核源码和必要的库;目标板通过串口输出日志,通过网络挂载根文件系统或传输模块。搭建流程可归纳为以下清单:
| 步骤 | 操作 | 验证命令 |
|---|---|---|
| 1 | 安装交叉编译工具链 | arm-linux-gnueabihf-gcc -v |
| 2 | 准备与目标板一致的内核源码 | make ARCH=arm menuconfig |
| 3 | 编译内核模块 | make -C $KDIR M=$PWD modules |
| 4 | 传输模块到目标板 | scp demo.ko root@target:/tmp |
| 5 | 加载并查看日志 | insmod demo.ko && dmesg | tail |
| 6 | 检查sysfs与procfs节点 | ls /sys/module/demo/ |
uname -r对应的内核头文件编译目标板模块,会导致“version magic”不匹配而加载失败。必须使用目标板实际运行内核的源码或Module.symvers。五、案例与复盘:一个platform驱动从加载到验证
假设目标板设备树中已定义节点vendor,demo-device,驱动加载后应能在/sys/bus/platform/drivers/demo-driver/下看到绑定设备。若probe()未被调用,排查顺序为:设备树status是否为okay、compatible是否拼写一致、驱动是否成功注册、内核日志是否有“probe failed”。
# 查看驱动绑定情况
ls /sys/bus/platform/drivers/demo-driver/
# 输出示例:bind demo-device uevent unbind
# 查看模块参数与引用计数
cat /sys/module/demo/refcnt
# 输出:0
# 查看中断统计
cat /proc/interrupts | grep demo
复盘要点:第一,模块机制让驱动可以快速迭代,但生产环境要评估是否静态编译;第二,设备模型是“设备-总线-驱动”三角关系的核心,理解匹配流程比死记API更重要;第三,sysfs与procfs是调试利器,但不要把它们当作稳定的用户态ABI,属性文件可能随内核版本变化;第四,环境搭建中最容易出错的是内核版本与工具链不一致,建议用脚本固化编译命令,减少手工失误。