4. Linux内核驱动基础

在Android Vendor Module驱动工程中,Linux内核驱动是连接硬件与上层框架的根基。无论是摄像头、显示屏、传感器还是电源管理芯片,最终都要通过内核驱动完成寄存器配置、中断处理和数据搬运。本章围绕内核模块机制、字符设备驱动、设备树、platform总线以及sysfs/procfs接口五个核心知识点展开,帮助建立从“驱动如何加载”到“驱动如何与硬件匹配”再到“用户空间如何访问”的完整认知链路。

一、内核模块机制:驱动的可插拔基础

Linux内核模块(Loadable Kernel Module,LKM)允许驱动以.ko文件形式在运行时动态加载和卸载,而不必重新编译整个内核。对于Vendor Module而言,这意味着同一套内核镜像可以适配不同硬件配置,驱动以模块方式按需加载,显著缩短开发与调试周期。

一个最简内核模块至少包含入口和出口函数,并通过宏声明许可证与作者信息:

#include <linux/module.h>
#include <linux/kernel.h>

static int __init demo_init(void)
{
    pr_info("demo module init\n");
    return 0;
}

static void __exit demo_exit(void)
{
    pr_info("demo module exit\n");
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("vendor");
MODULE_DESCRIPTION("demo driver");

编译通常借助内核构建系统完成,核心命令如下:

make -C /path/to/kernel M=$PWD modules
insmod demo.ko
lsmod | grep demo
rmmod demo
模块加载时会执行module_init,卸载时执行module_exit。若入口函数返回非0,模块加载失败,内核会回滚已申请的资源,因此入口中应严格检查返回值。

二、字符设备驱动:字节流式访问模型

字符设备是驱动工程中最常见的设备类型,它以字节流方式被用户空间访问,典型代表包括串口、按键、LED、传感器等。字符设备驱动的核心是file_operations结构体,它把用户空间的open/read/write/ioctl等系统调用映射到内核函数。

字符设备驱动的基本流程如下:

  1. 申请设备号:使用alloc_chrdev_region动态申请,或register_chrdev_region静态指定。
  2. 初始化cdev并绑定file_operations。
  3. 调用cdev_add将字符设备加入内核。
  4. 创建类与设备节点,供用户空间通过/dev访问。
  5. 卸载时依次cdev_del、unregister_chrdev_region释放资源。
static const struct file_operations demo_fops = {
    .owner   = THIS_MODULE,
    .open    = demo_open,
    .release = demo_release,
    .read    = demo_read,
    .write   = demo_write,
    .unlocked_ioctl = demo_ioctl,
};
字符设备驱动中copy_to_user与copy_from_user不可用普通指针替代,否则会因用户空间与内核空间地址隔离导致崩溃或安全漏洞。

三、设备树:硬件描述与驱动解耦

设备树(Device Tree)用.dts文本描述硬件资源,编译为.dtb后由Bootloader传递给内核。它把“硬件是什么、挂在哪里、使用哪些引脚和中断”从驱动代码中剥离出来,使同一驱动可以适配不同板级配置。Android设备通常使用.dtsi包含公共定义,再由具体项目.dts覆盖差异。

demo_device: demo@1c28000 {
    compatible = "vendor,demo-device";
    reg = <0x1c28000 0x1000>;
    interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk_demo>;
    status = "okay";
};

驱动通过of_match_table与设备树中的compatible属性匹配,匹配成功后即可用of_property_read_u32、platform_get_irq等接口读取资源。

四、platform总线:SoC内设驱动的标准匹配模型

platform总线是Linux为SoC内部集成外设设计的一种虚拟总线。它把设备与驱动分离:设备信息来自设备树或板级文件,驱动则注册到platform总线上,由总线负责匹配。匹配成功后调用驱动的probe函数,卸载时调用remove。

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);

下图展示了从设备树到platform总线匹配,再到字符设备节点暴露给用户空间的完整数据流与匹配关系:

设备树 → platform总线 → 字符设备 → 用户空间 数据流 设备树 .dts/.dtb compatible reg / interrupts clocks / status platform总线 of_match_table 匹配 probe / remove 资源解析 字符设备驱动 file_operations cdev_add /dev/demo 硬件描述 注册设备 用户空间 open / read / write ioctl / mmap 设备节点 sysfs / procfs /sys/class/demo/ /proc/demo_info 属性读写 关键匹配链: compatible 字符串 → of_device_id → platform_driver.probe() → 申请设备号 → cdev_add → class_create/device_create → /dev 节点 调试入口:dmesg 查看 probe 日志;ls /sys/class/demo/ 查看属性;cat /proc/demo_info 查看运行状态

五、sysfs与procfs:用户空间的可观测接口

sysfs挂载在/sys,以“设备-驱动-总线”层次组织,适合暴露结构化属性;procfs挂载在/proc,更偏向运行时状态与调试信息。两者都是驱动向用户空间提供可读可写接口的重要方式。

sysfs属性通常通过DEVICE_ATTR或DEVICE_ATTR_RW定义,并在probe中创建:

static ssize_t mode_show(struct device *dev,
                         struct device_attribute *attr, char *buf)
{
    return sprintf(buf, "%d\n", demo_mode);
}

static ssize_t mode_store(struct device *dev,
                          struct device_attribute *attr,
                          const char *buf, size_t count)
{
    kstrtoint(buf, 10, &demo_mode);
    return count;
}

static DEVICE_ATTR_RW(mode);
device_create_file(&pdev->dev, &dev_attr_mode);

procfs则常用proc_create配合seq_file输出调试信息:

static int demo_show(struct seq_file *m, void *v)
{
    seq_printf(m, "irq=%d, reg=0x%x\n", demo_irq, demo_reg);
    return 0;
}
proc_create_single("demo_info", 0444, NULL, demo_show);

六、核心接口对比与常见误区

机制 主要用途 典型接口 用户空间路径
内核模块 动态加载/卸载驱动 module_init / module_exit insmod / rmmod
字符设备 字节流读写与ioctl控制 cdev_add / file_operations /dev/xxx
设备树 描述硬件资源与匹配信息 compatible / reg / interrupts 由内核解析
platform总线 SoC内设驱动匹配与生命周期管理 platform_driver / probe 由总线触发
sysfs 结构化属性读写 DEVICE_ATTR_RW /sys/class/xxx/
procfs 调试信息与运行状态输出 proc_create / seq_file /proc/xxx
在Vendor Module开发中,推荐优先使用设备树描述硬件,用platform总线完成匹配,用字符设备或sysfs暴露控制接口,用procfs输出调试信息。这样驱动结构清晰,也便于不同项目复用。

常见误区包括:在probe中未检查返回值导致资源泄漏;设备树compatible与驱动of_device_id不一致导致probe不执行;在中断上下文使用可能睡眠的接口;以及忘记在remove中释放设备号、类、设备节点和sysfs属性。排查时优先查看dmesg中的probe日志与匹配信息,再确认/sys和/proc下接口是否按预期生成。

掌握以上五个基础机制后,后续的Vendor Module驱动将围绕它们展开:模块机制负责加载,设备树负责描述,platform总线负责匹配,字符设备负责数据通道,sysfs与procfs负责可观测与可控制。理解这条链路,是编写稳定、可维护Android内核驱动的第一步。