ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Linux内核驱动开发实战:从drivers目录到可加载模块与设备树匹配

Linux内核驱动开发实战:从drivers目录到可加载模块与设备树匹配 简介这份资源是《Linux设备驱动开发详解基于最新的Linux 4.0内核》一书的配套样例代码包面向具备一定C语言与操作系统基础、希望系统学习Linux内核驱动开发的工程师与高校学生帮助读者把书中理论落到可编译、可运行的代码实践上。压缩包共122个文件约659KB以c源码与cmd编译脚本为主辅以makefile构建文件、o目标文件、ko内核模块、mod模块信息及symvers符号版本等覆盖字符设备、块设备、网络设备、中断处理、I/O调度、内存管理、模块化设计与电源管理等核心主题。资源中已有197人学习下载样例围绕globalmem、globalfifo、vmem_disk等典型驱动展开读者可借此理解设备注册、读写请求处理、设备文件创建与内核接口调用方式为实际项目中的驱动开发与调试打下基础。1. drivers_drivers_linux_Kernel_从目录名到可加载模块的完整路径第一次看到drivers_drivers_linux_Kernel_这个标题很多人会以为它指向某个具体的驱动仓库或内核补丁集。实际上它更像一个信号你手上有一份 Linux 内核源码树或者一个按drivers/目录组织过的驱动代码包需要把它变成能跑、能加载、能调试的东西。Linux 内核驱动开发不是写一个.c文件然后gcc一下就能收工的它涉及内核配置、模块编译、设备树匹配、符号导出、版本适配这一整条链路。这篇文章面向的是嵌入式 Linux 工程师、BSP 移植人员以及正在从应用层往内核层过渡的开发者。我会按“先理解 drivers 目录的组织逻辑再动手编译一个可加载模块最后处理真实硬件匹配和排错”的顺序展开每一步都给出可复现的命令和参数说明。如果你正在做国产 Linux 发行版适配、高通 CAF kernel 移植或者只是想把一个字符设备驱动跑通下面的内容可以直接抄作业。2. Linux 内核 drivers 目录的组织逻辑与模块编译链路2.1 drivers 目录不是随便放的Kconfig、Makefile 与模块的三方约定Linux 内核源码树里的drivers/目录是整棵树上最庞大的部分之一按设备类型分成char/、block/、net/、i2c/、spi/、gpio/、usb/、pci/等子目录。每个子目录下通常有三类文件驱动源码.c、Kconfig 配置项、Makefile 编译规则。这三者构成一个闭环Kconfig 决定这个驱动是否出现在make menuconfig的菜单里Makefile 决定它编译成内置还是模块源码里的module_init/module_exit决定加载和卸载行为。我一般会先确认三件事第一目标内核版本是多少uname -r和源码树顶层Makefile里的VERSION/PATCHLEVEL是否一致第二交叉编译工具链前缀是什么比如aarch64-linux-gnu-还是arm-linux-gnueabihf-第三当前内核的.config里CONFIG_MODULES是否打开。这三件事没确认就动手后面大概率会翻车。一个典型的驱动子目录结构如下drivers/mydriver/ ├── Kconfig ├── Makefile ├── mydriver.c └── mydriver.hKconfig 内容示例config MYDRIVER tristate My example driver depends on GPIOLIB help This is a demo driver for GPIO-based device. Say M to build as module.Makefile 内容示例obj-$(CONFIG_MYDRIVER) mydriver.otristate表示这个配置项有三种状态N不编译、Y编进内核、M编译成模块。obj-$(CONFIG_MYDRIVER)会根据配置值展开成obj-y或obj-m分别对应内置和模块。这里的关键点是如果你希望驱动能动态加载必须让CONFIG_MYDRIVERm并且内核本身开启了模块支持。2.2 从零编译一个可加载模块命令、参数与产物验证假设你已经有一份内核源码树路径是/home/user/kernel交叉编译工具链是aarch64-linux-gnu-目标架构是 arm64。下面是我常用的编译流程。第一步准备配置。如果源码树里还没有.config可以用默认配置或厂商配置cd /home/user/kernel make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- defconfig如果厂商提供了xxx_defconfig优先用厂商的因为默认配置可能没打开你需要的子系统。第二步打开你的驱动配置项make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- menuconfig在菜单里找到Device Drivers下的My example driver按M选中保存退出。或者直接用脚本改scripts/config --module MYDRIVER make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- olddefconfigolddefconfig会把新增配置项的默认值补齐避免交互式提问。第三步编译模块make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- Mdrivers/mydriver modulesMdrivers/mydriver告诉内核构建系统只编译这个子目录不重新编译整个内核。编译完成后你会在drivers/mydriver/下看到mydriver.ko。第四步验证模块信息modinfo drivers/mydriver/mydriver.ko输出里会包含filename、license、description、depends、vermagic等字段。vermagic必须和目标板运行内核的版本字符串完全一致否则insmod会报invalid module format。这是最常见的坑之一。第五步推送到目标板并加载scp drivers/mydriver/mydriver.ko root192.168.1.100:/tmp/ ssh root192.168.1.100 insmod /tmp/mydriver.ko dmesg | tail -20如果驱动里用了printkdmesg里能看到对应输出。卸载用rmmod mydriver前提是模块没有被引用。提示insmod不会自动解决依赖如果模块依赖其他模块用modprobe并确保/lib/modules/$(uname -r)/下有正确的modules.dep。2.3 内置驱动与模块驱动的选择边界不是所有驱动都适合编译成模块。我一般按下面的边界来判断场景推荐方式原因启动阶段必须初始化内置Y模块加载时机晚于根文件系统挂载调试阶段频繁改代码模块M改完只需重编模块不用重启内核依赖早期中断或时钟内置Y模块加载时中断子系统可能还没就绪厂商 BSP 强制要求按厂商有些 SoC 的电源域和时钟依赖顺序固定高通 CAF kernel 里很多驱动默认是内置的因为涉及电源管理和时钟树初始化顺序。如果你强行改成模块可能会出现 probe 时时钟未使能、寄存器读写失败的情况。这类问题不是代码写错了而是加载时机不对。3. 设备树匹配与 probe 流程驱动怎么找到硬件3.1 compatible 字符串驱动和设备树的握手协议在 ARM/ARM64 嵌入式 Linux 里驱动和硬件的匹配主要靠设备树。驱动里写一个of_device_id表设备树里写一个compatible属性两者字符串完全一致时内核才会调用驱动的probe函数。驱动侧代码示例#include linux/module.h #include linux/platform_device.h #include linux/of.h static int mydriver_probe(struct platform_device *pdev) { struct device *dev pdev-dev; dev_info(dev, mydriver probed\n); return 0; } static int mydriver_remove(struct platform_device *pdev) { dev_info(pdev-dev, mydriver removed\n); return 0; } static const struct of_device_id mydriver_of_match[] { { .compatible vendor,mydriver-v1 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydriver_of_match); static struct platform_driver mydriver_driver { .probe mydriver_probe, .remove mydriver_remove, .driver { .name mydriver, .of_match_table mydriver_of_match, }, }; module_platform_driver(mydriver_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Demo platform driver);设备树侧节点示例mydriver10000000 { compatible vendor,mydriver-v1; reg 0x0 0x10000000 0x0 0x1000; interrupts 0 42 4; status okay; };compatible是匹配的关键reg描述寄存器基地址和长度interrupts描述中断号和触发类型。status okay表示启用这个节点如果写disabled驱动不会 probe。3.2 probe 失败的排查顺序从 dmesg 到 /sys 的完整链路probe 失败是驱动开发里最耗时的环节。我一般按下面的顺序排查第一看dmesg里有没有probe failed或failed to get resource之类的输出。内核会在 probe 失败时打印错误码比如-ENODEV、-EINVAL、-EPROBE_DEFER。第二确认设备树节点是否被内核解析到ls /sys/firmware/devicetree/base/如果节点存在再检查compatible是否和驱动里的字符串完全一致。注意大小写和连字符vendor,mydriver-v1和vendor,myDriver-v1是不匹配的。第三检查EPROBE_DEFER。这个错误码表示驱动依赖的资源还没准备好内核会稍后重试。常见原因是时钟、 regulator、GPIO 控制器还没初始化。解决办法是确认依赖驱动的 probe 顺序必要时在设备树里调整节点顺序或使用phandle引用。第四看/sys/bus/platform/drivers/mydriver/下有没有绑定成功的设备ls /sys/bus/platform/drivers/mydriver/如果目录下只有bind、unbind、uevent没有设备名说明没有设备绑定到这个驱动。第五用dev_info在 probe 入口打印确认函数是否被调用。如果根本没进 probe问题在匹配阶段如果进了 probe 但失败问题在资源获取阶段。注意pr_info和dev_info的输出级别不同dev_info会带上设备名更容易定位。生产驱动里建议用dev_err打印错误用dev_dbg打印调试信息。3.3 字符设备注册file_operations 与设备号的分配platform_driver 负责和硬件匹配字符设备负责和用户空间交互。两者可以合在一个驱动里也可以分开。下面是一个最小的字符设备注册示例#include linux/fs.h #include linux/cdev.h #include linux/uaccess.h static dev_t mydev_num; static struct cdev mydev_cdev; static struct class *mydev_class; static int mydev_open(struct inode *inode, struct file *file) { return 0; } static ssize_t mydev_read(struct file *file, char __user *buf, size_t len, loff_t *offset) { char msg[] hello from kernel\n; if (*offset sizeof(msg)) return 0; if (copy_to_user(buf, msg, sizeof(msg))) return -EFAULT; *offset sizeof(msg); return sizeof(msg); } static const struct file_operations mydev_fops { .owner THIS_MODULE, .open mydev_open, .read mydev_read, }; static int __init mydev_init(void) { alloc_chrdev_region(mydev_num, 0, 1, mydev); cdev_init(mydev_cdev, mydev_fops); cdev_add(mydev_cdev, mydev_num, 1); mydev_class class_create(THIS_MODULE, mydev); device_create(mydev_class, NULL, mydev_num, NULL, mydev); return 0; } static void __exit mydev_exit(void) { device_destroy(mydev_class, mydev_num); class_destroy(mydev_class); cdev_del(mydev_cdev); unregister_chrdev_region(mydev_num, 1); } module_init(mydev_init); module_exit(mydev_exit); MODULE_LICENSE(GPL);alloc_chrdev_region动态分配设备号cdev_add注册字符设备class_create和device_create自动在/dev下创建设备节点。用户空间用open(/dev/mydev, O_RDONLY)打开read读取。copy_to_user是内核向用户空间传数据的标准接口不能直接用memcpy否则会触发内核页错误。4. 驱动开发避坑从编译报错到运行时崩溃的 5 个真实记录4.1 现象insmod 报 invalid module format原因模块的vermagic和目标内核版本不一致。常见于换了内核源码树但没重新编译模块或者交叉编译工具链版本不同导致内核版本字符串带或-dirty后缀。解决用modinfo对比模块和内核的vermagic确保源码树顶层Makefile的版本号和目标板uname -r一致。如果目标板内核是厂商定制的必须用厂商提供的源码树编译模块。4.2 现象probe 函数不执行dmesg 无任何输出原因设备树compatible字符串和驱动of_device_id不匹配或者设备树节点status是disabled或者驱动根本没编译进内核。解决先确认/sys/firmware/devicetree/base/下节点存在且status为okay再确认驱动.ko文件已加载且modinfo里alias字段包含正确的of:前缀。如果驱动是内置的检查.config里对应配置项是否为Y。4.3 现象probe 返回 -EPROBE_DEFER反复重试但不成功原因驱动依赖的时钟、regulator、GPIO 控制器还没 probe 完成。内核会延迟重试但如果依赖驱动永远不 probe你的驱动也永远不会成功。解决检查依赖驱动是否已加载设备树里phandle引用是否正确。可以用cat /sys/kernel/debug/devices_deferred查看延迟设备列表。如果是依赖顺序问题考虑把依赖驱动改成内置或者调整设备树节点顺序。4.4 现象copy_to_user 返回 -EFAULT用户空间读不到数据原因用户空间传入的缓冲区地址无效或者长度参数超过了实际缓冲区大小。也可能是内核里直接用了memcpy而不是copy_to_user。解决在read/write里先检查access_ok再用copy_to_user/copy_from_user。注意copy_to_user返回的是未拷贝的字节数返回 0 表示成功。如果返回非 0说明有部分数据没拷贝成功。4.5 现象rmmod 报 Device or resource busy原因模块的引用计数不为 0可能有用户空间进程还打开着设备节点或者内核里有其他模块依赖它。解决用lsmod查看模块引用计数用fuser /dev/mydev或lsof /dev/mydev找到占用进程并关闭。如果是内核依赖先卸载依赖模块。调试阶段可以在mydev_open里加try_module_get(THIS_MODULE)在release里加module_put确保引用计数正确。5. 用 QEMU 验证驱动不接硬件也能跑通 probe 和读写5.1 QEMU buildroot 的最小验证环境不是每个人都有现成的开发板。我常用 QEMU 加 buildroot 搭一个最小环境验证驱动的基本逻辑。buildroot 可以生成内核镜像、根文件系统和设备树QEMU 负责模拟运行。配置 buildroot 时关键选项如下配置项值说明Target ArchitectureARM (little endian)或 AArch64KernelLinux 4.19 或更高版本按需选择ToolchainBuildroot toolchain内置工具链省去交叉编译配置Filesystemext2/ext4根文件系统格式Device tree启用用于传递硬件描述编译完成后用 QEMU 启动qemu-system-arm -M vexpress-a9 \ -kernel output/images/zImage \ -dtb output/images/vexpress-v2p-ca9.dtb \ -drive fileoutput/images/rootfs.ext2,ifsd,formatraw \ -append root/dev/mmcblk0 consolettyAMA0 \ -nographic-M vexpress-a9指定模拟板型-dtb指定设备树-append传递内核命令行。启动后进入 shell就可以用insmod加载模块用dmesg看输出。5.2 在 QEMU 里验证字符设备读写把编译好的mydev.ko通过scp或挂载共享目录传到 QEMU 里加载后检查/dev/mydev是否存在insmod /tmp/mydev.ko ls -l /dev/mydev cat /dev/mydev如果cat输出hello from kernel说明字符设备注册和读写链路都通了。如果/dev/mydev不存在检查class_create和device_create的返回值以及udev或mdev是否在运行。提示QEMU 里没有真实硬件所以依赖具体寄存器操作的驱动无法完整验证。但 probe 流程、字符设备注册、文件操作接口这些逻辑可以跑通能提前发现大部分代码错误。5.3 用 ftrace 跟踪 probe 调用链如果 probe 没执行可以用 ftrace 看内核函数调用cd /sys/kernel/debug/tracing echo function current_tracer echo mydriver_probe set_ftrace_filter echo 1 tracing_on insmod /tmp/mydriver.ko cat traceset_ftrace_filter只跟踪指定函数避免输出过多。如果trace里没有mydriver_probe说明匹配阶段就失败了。如果有但后面报错可以结合dmesg看具体错误码。最后一章我想说的是驱动开发最怕的不是代码写不出来而是不知道哪一层出了问题。我的习惯是每加一个功能点就验证一次先确认模块能编译再确认能加载再确认 probe 能进再确认字符设备能注册最后才测读写。每一步都有对应的检查命令不要等全部写完再一起调。另外内核版本和工具链版本一定要锁死换版本后先重新编译一个已知能用的模块确认环境没问题再改代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表