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强调“内核运行状态与统计”。

Linux 内核架构与驱动开发环境全景 用户空间 User Space 应用程序 insmod / modprobe /sys 读写 /proc 读取 dmesg 系统调用 / 文件接口边界 内核空间 Kernel Space VFS 虚拟文件系统 sysfs / procfs / devtmpfs 设备模型 Device Model kobject / kset / device 总线 Bus platform / i2c / spi / usb 驱动 Driver probe / remove match 硬件层:SoC 外设 / GPIO / I2C / SPI / PCIe / 中断控制器 设备树 DTS 描述硬件资源,驱动通过 of_* 接口解析 开发环境:内核源码树 + 交叉编译工具链 + 目标板 + NFS/TFTP + 串口调试 模块编译 → 加载 → sysfs/procfs 验证 → dmesg 排错

四、驱动开发环境搭建与验证流程

驱动开发环境通常由“宿主机 + 目标板”组成。宿主机安装交叉编译工具链、内核源码和必要的库;目标板通过串口输出日志,通过网络挂载根文件系统或传输模块。搭建流程可归纳为以下清单:

步骤 操作 验证命令
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,属性文件可能随内核版本变化;第四,环境搭建中最容易出错的是内核版本与工具链不一致,建议用脚本固化编译命令,减少手工失误。

掌握内核架构、模块机制、设备模型以及sysfs/procfs的使用,是后续字符设备、平台设备、I2C/SPI驱动开发的基础。建议先亲手完成一个“最小模块 + platform驱动”的加载与卸载实验,再进入具体子系统。