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等系统调用映射到内核函数。
字符设备驱动的基本流程如下:
- 申请设备号:使用
alloc_chrdev_region动态申请,或register_chrdev_region静态指定。 - 初始化
cdev并绑定file_operations。 - 调用
cdev_add将字符设备加入内核。 - 创建类与设备节点,供用户空间通过
/dev访问。 - 卸载时依次
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总线匹配,再到字符设备节点暴露给用户空间的完整数据流与匹配关系:
五、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 |
常见误区包括:在probe中未检查返回值导致资源泄漏;设备树compatible与驱动of_device_id不一致导致probe不执行;在中断上下文使用可能睡眠的接口;以及忘记在remove中释放设备号、类、设备节点和sysfs属性。排查时优先查看dmesg中的probe日志与匹配信息,再确认/sys和/proc下接口是否按预期生成。
掌握以上五个基础机制后,后续的Vendor Module驱动将围绕它们展开:模块机制负责加载,设备树负责描述,platform总线负责匹配,字符设备负责数据通道,sysfs与procfs负责可观测与可控制。理解这条链路,是编写稳定、可维护Android内核驱动的第一步。