返回

设备树覆盖后Linux模块加载失败问题解析及方案

Linux

设备树覆盖应用后 Linux 内核模块无法加载

在嵌入式Linux系统开发中,经常需要动态配置硬件,设备树覆盖 (device-tree overlay) 提供了一种灵活的方式。它允许在运行时修改设备的信息,无需重新编译整个内核。利用configfs可以实现这一功能,通过修改配置项动态地应用设备树覆盖。一种常见的使用场景是将驱动模块和设备树配置关联起来。即:应用包含新设备信息的覆盖时,对应的驱动模块应当自动加载。但实践中,可能会遇到这样的问题:设备树覆盖应用后,驱动模块并没有自动加载,需要手动modprobe才能工作。 这给开发者带来了困扰,需要仔细分析其背后的原因。

模块加载时序与依赖问题

内核模块的加载并非随意发生,依赖于内核中的多种机制。 udev 在设备树改变时会被触发,这看似应该直接加载驱动,实际上加载行为并非直接的udev行为。udev会传递事件通知,促使其他子系统或脚本采取行动。设备树被覆盖应用后,内核会检测到新的设备,寻找匹配的驱动程序,驱动程序的匹配涉及modules.aliasmodules.order。如果匹配机制存在问题,即使modules.alias 中定义了 compatible 字符串, 内核也不会加载。问题可能出在内核模块加载依赖的初始化次序中,设备树节点定义出现在模块初始化前,也会导致无法自动加载,或加载失败,出现如下现象:

  • modprobe sample-driver可以正常加载。这表明模块本身没有问题,加载器本身能够找到模块并且正确解析。
  • modules.alias 中有 compatible 条目。表明内核有能力将当前节点和当前模块建立匹配。

解决思路与实践方案

要解决这个问题,需要深入了解模块加载机制和设备树管理的相关知识,需要一些更具体的调整手段,以下方案可供参考。

方案一: 强制重新加载驱动

一种比较直接的解决办法是:在应用设备树覆盖后,显式触发模块的重新扫描和加载。这通过使用udev机制来达成。我们可以创建一个udev规则,当设备树相关的设备被更新时,卸载所有与该 compatible 属性相关的所有模块,然后再通过modprobe加载它们。这种方案虽然简单,却需要配置udev,维护多个相关的 udev 文件。 考虑到此文章是解决特定问题而不是一个通用的解决方案,为了控制复杂性,我们可以用脚本模拟此流程。

  • 代码示例:
#!/bin/bash

MOD_NAME="sample-driver"  #模块名称
COMPAT="sample-compat-string" #兼容字符串, 需要根据设备树定义修改
# 卸载所有使用该 compatible 字符串的模块
for mod in $(find /sys/module -name 'holders' | while read -r holders ;do grep "$COMPAT" "$holders"| awk '{print $1}'; done ); do
	modprobe -r  "$mod"; 
done
modprobe  $MOD_NAME; #加载指定模块

  • 操作步骤:
    1. 保存上述脚本为 load_module.sh, 修改为实际的模块名称以及设备树覆盖文件中定义的兼容性字符串。
    2. 添加可执行权限: chmod +x load_module.sh
    3. 在应用 configfs 配置后,运行这个脚本: sudo ./load_module.sh

该脚本的原理为,首先利用find找到所有正在使用的设备模块,然后根据他们的名字进行过滤,找出当前设备树overlay 需要使用compat string。最后强制卸载它们。 再次执行modprobe,此时modprobe 根据最新配置再次匹配需要的驱动,确保能够正确加载模块。

方案二: 修改驱动的模块初始化次序

有些时候问题出在模块初始化时序上,可以在模块的初始化阶段设置模块在稍后时间被加载,从而确保其他设备信息已经完全初始化,此时使用 module init call.

  • 代码示例: (以下示例需要修改你的驱动代码)
#include <linux/module.h>
#include <linux/init.h>
#include <linux/iio/iio.h>
#include <linux/of.h>

/* 定义 compatible string */
#define COMPATIBLE "sample-compat-string"

/*
* 这里假设 module 初始化的工作可以晚一点进行。如果一些全局变量需要在 early init 使用,
* 这样的做法则会带来问题。你需要自己仔细评估当前module 的生命周期
*/
static int __init sample_driver_late_init(void){
   struct device_node *np;
    
     np = of_find_compatible_node(NULL, NULL, COMPATIBLE);
	 if (np) {
		pr_info("Found the device node by compatible");
	 } else {
	     pr_info("Did not found any device node by compatible %s\n", COMPATIBLE);
	   }
      // 在此处加载iio 驱动相关模块,调用相应的api进行注册,略去

   return 0;

}
module_init(sample_driver_late_init);

static void __exit sample_driver_exit(void)
{
    //退出程序,相关模块注销。 
    
    return ;
}

module_exit(sample_driver_exit);


MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Sample Driver for Device-tree Overlay Issue");
MODULE_COMPATIBLE(COMPATIBLE);

  • 操作步骤
  1. 根据设备树定义修改兼容性字符串(宏 COMPATIBLE 的值)
  2. 编译此模块。注意重新编译驱动可能需要更新内核模块缓存或者再次运行 depmod 命令。
  3. 将新的内核模块安装到对应的模块目录中/lib/modules/<version>/kernel/drivers/iio/dac/sample-driver.ko
  4. 应用 configfs 配置。

方案二,调整模块加载时间需要深入修改驱动本身,对驱动设计的依赖度较高,因此你需要结合你自身的情况选择。

额外提示

  • 确认 modules.order 文件内容是否包含对应的模块路径。
  • 尝试在 modprobe 中加入 -vvv 参数进行详细日志查看, 能够获得模块加载更多的相关信息。 例如: modprobe -vvv sample-driver.

选择方案时,请根据自身情况权衡不同方案的复杂性,找到最适合你项目的解决方案。 良好的调试习惯和详尽的日志分析可以帮助定位并解决此类问题。