Linux内核如何知道在哪里查找驱动程序固件?

57

我正在Ubuntu下编译自定义内核,遇到的问题是我的内核似乎不知道在哪里查找固件。在Ubuntu 8.04中,固件与驱动程序模块一样与内核版本绑定。例如,内核2.6.24-24-generic将其内核模块存储在:

/lib/modules/2.6.24-24-generic

以及其固件在:

/lib/firmware/2.6.24-24-generic

当我按照“旧式 Debian 方法备选构建方法"编译 2.6.24-24-generic Ubuntu 内核时,我得到了适当的模块目录,所有设备都可以工作,除了那些需要固件的设备,比如我的英特尔无线网卡(ipw2200 模块)。
例如,内核日志显示,当 ipw2200 尝试加载固件时,控制固件加载的内核子系统无法找到它。
ipw2200: Detected Intel PRO/Wireless 2200BG Network Connection
ipw2200: ipw2200-bss.fw request_firmware failed: Reason -2

errno-base.h将其定义为:

#define ENOENT       2  /* No such file or directory */

(返回 ENOENT 的函数在其前面加上了减号。)

我尝试在 /lib/firmware 中创建一个符号链接,其中我的内核名称指向 2.6.24-24-generic 目录,但这导致相同的错误。这个固件是由英特尔提供的非 GPL 固件,由 Ubuntu 打包。我不认为它与特定的内核版本有任何实际关系。cmp 显示各个目录中的版本是相同的。

那么内核如何知道在哪里查找固件?

更新

我找到了这个解决方案来解决我遇到的确切问题,但由于 Ubuntu 已经删除了 /etc/hotplug.d 并且不再将其固件存储在 /usr/lib/hotplug/firmware 中,因此它不再起作用。

更新2

进行了更多的研究,找到了更多的答案。在 udev 版本 92 之前,程序 firmware_helper 是加载固件的方法。从 udev 93 开始,该程序被更换为名为 firmware.sh 的脚本,据我所知,它提供了相同的功能。这两个都将固件路径硬编码为 /lib/firmware。Ubuntu 仍然似乎在使用 /lib/udev/firmware_helper 二进制文件。

固件文件的名称通过环境变量$FIRMWARE传递给firmware_helper,并与路径/lib/firmware连接在一起,用于加载固件。
驱动程序(在我的情况下为ipw2200)通过系统调用进行实际的加载固件请求:
request_firmware(..., "ipw2200-bss.fw", ...);

现在,在驱动程序调用request_firmwarefirmware_helper查看$FIRMWARE环境变量之间的某个地方,内核包名称被添加到固件名称前面。

那么是谁在这样做呢?


有人可以添加一条评论解释为什么他们正在投票关闭这个问题吗?我会感激您的反馈。 - Robert S. Barnes
2
它正在被投票关闭,因为它“应该在serverfault.com上发布”。由于这个问题不是关于编程的,而更多地涉及管理方面,这符合两个网站的使命陈述。 - ephemient
谢谢您的反馈。我认为这是一个编程问题,因为在尝试构建自定义内核时遇到了问题。我想知道为什么有人会认为这不与编程有关,虽然我猜测了一些很好的原因。 - Robert S. Barnes
4个回答

44
从内核的角度来看,请参阅/usr/src/linux/Documentation/firmware_class/README
内核(驱动程序):调用request_firmware(&fw_entry, $FIRMWARE, device)
用户空间: - /sys/class/firmware/xxx/{loading,data}出现。 - 热插拔使用$FIRMWARE中的固件标识符,并使用常规的热插拔环境。 - 热插拔:echo 1 > /sys/class/firmware/xxx/loading
内核:丢弃任何以前的部分加载。
用户空间: - 热插拔:cat appropriate_firmware_image > \ /sys/class/firmware/xxx/data
内核:增加一个缓冲区,每次以PAGE_SIZE增量来保存图像。
用户空间: - 热插拔:echo 0 > /sys/class/firmware/xxx/loading
内核:request_firmware()返回,驱动程序在fw_entry->{data,size}中具有固件映像。如果出现错误,request_firmware()将返回非零值,并将fw_entry设置为NULL。
内核(驱动程序):驱动程序代码调用release_firmware(fw_entry)释放固件映像和任何相关资源。

内核实际上根本不加载任何固件。它只是通知用户空间,“我需要一个名为xxx的固件”,然后等待用户空间将固件映像传回内核。

现在,在Ubuntu 8.04上,

$ grep firmware /etc/udev/rules.d/80-program.rules
# 加载固件程序
SUBSYSTEM=="firmware", ACTION=="add", RUN+="firmware_helper"

正如您发现的那样,当内核请求固件时,udev被配置为运行firmware_helper

$ apt-get source udev
正在读取软件包列表... 完成
正在分析软件包的依赖关系树       
正在读取状态信息... 完成       
需要下载 312kB 的源代码包。
获取:1 http://us.archive.ubuntu.com hardy-security/main udev 117-8ubuntu0.2 (dsc) [716B]
获取:2 http://us.archive.ubuntu.com hardy-security/main udev 117-8ubuntu0.2 (tar) [245kB]
获取:3 http://us.archive.ubuntu.com hardy-security/main udev 117-8ubuntu0.2 (diff) [65.7kB]
已经下载 312kB,耗时 1s (223kB/s)
gpg: Signature made Tue 14 Apr 2009 05:31:34 PM EDT using DSA key ID 17063E6D
gpg: Can't check signature: public key not found
dpkg-source: 在 udev 中提取
dpkg-source: 正在解压缩 udev_117.orig.tar.gz
dpkg-source: 正在应用 ./udev_117-8ubuntu0.2.diff.gz
$ cd udev-117/
$ cat debian/patches/80-extras-firmware.patch
如果您阅读源代码,您会发现Ubuntu编写了一个"firmware_helper",它是硬编码的,首先查找"/lib/modules/$(uname -r)/$FIRMWARE",然后是"/lib/modules/$FIRMWARE",没有其他位置。用"sh"翻译,大致如下:
echo -n 1 > /sys/$DEVPATH/loading
cat /lib/firmware/$(uname -r)/$FIRMWARE > /sys/$DEVPATH/data \
    || cat /lib/firmware/$FIRMWARE      > /sys/$DEVPATH/data
if [ $? = 0 ]; then
    echo -n  1 > /sys/$DEVPATH/loading
    echo -n -1 > /sys/$DEVPATH/loading
fi

这正是内核所期望的格式。


简而言之:Ubuntu的udev软件包具有自定义功能,始终首先查找/lib/firmware/$(uname -r)。此策略在用户空间中处理。

1
非常好的答案,谢谢!我正在查看udev源代码,试图找出Ubuntu的firmware_helper来自哪里。我应该只是在udev源代码树中进行递归grep以查找固件。 - Robert S. Barnes
1
如果不清楚的话,Hotplug已被Udev取代,内核树中的文档已经过时。Arch Udev WikiSuse udev wiki解释了这些变化。安迪·马特森的答案比这个更好。 - Kevin
@kevinf Andy的回答与此相同。在内核“热插拔”uevent上,一些用户空间程序加载数据。这通常与udev捆绑在一起。 - ephemient
3
给定的README链接指向当前Linux行为,自从这个回复发布以来已经发生了变化。现在,Linux会在多个目录中查找,包括/lib/firmware/$(uname -r)然后再询问udev。另请参阅lwn文章:“Udev和固件”。 - yonran
引用的文档说,当你完成后,必须将0写入“/sys/$DEVPATH/loading”,然而你的示例代码却先输出1,然后输出-1,仅在确实找到某些固件时才会这样做。这是简单的错误还是你知道我不知道的什么? - Simon
显示剩余2条评论

14
“哇,这是非常有用的信息,它帮助我解决了一个问题,当我为需要固件的设备制作自定义USB内核模块时。”
“基本上,每个Ubuntu都会带来hal、sysfs、devfs、udev等方面的新改进...而且事情总是在变化。实际上,我看到他们停止使用hal了。”
“所以,让我们再次逆向工程,使其与最新的[Ubuntu]系统相关。”
“在Ubuntu Lucid(撰写时的最新版本)中,使用/lib/udev/rules.d/50-firmware.rules。该文件调用二进制文件/lib/udev/firmware,其中发生了一些神奇的事情。”
“清单:/lib/udev/rules.d/50-firmware.rules
# firmware-class requests, copies files into the kernel
SUBSYSTEM=="firmware", ACTION=="add", RUN+="firmware --firmware=$env{FIRMWARE} --devpath=$env{DEVPATH}"

这种魔法的实现方式应该是这样的(来源:Linux设备驱动程序,第三版,第14章:Linux设备模型):

  • 将1输出到loading
  • 将固件复制到data
  • 如果失败,则将-1输出到loading并停止固件加载过程
  • 将0输出到loading(向内核发出信号)
  • 然后,特定的内核模块接收数据并将其推送到设备上

如果你查看Lucid的udev源代码页面,在udev-151/extras/firmware/firmware.c中,那个固件/lib/udev/firmware二进制文件的源代码就是这样。

节选自:Lucid源码,udev-151/extras/firmware/firmware.c

    util_strscpyl(datapath, sizeof(datapath), udev_get_sys_path(udev), devpath, "/data", NULL);
    if (!copy_firmware(udev, fwpath, datapath, statbuf.st_size)) {
            err(udev, "error sending firmware '%s' to device\n", firmware);
            set_loading(udev, loadpath, "-1");
            rc = 4;
            goto exit;
    };

    set_loading(udev, loadpath, "0");

此外,许多设备使用Intel HEX格式(包含校验和其他内容的文本文件)(请搜索“wiki it i have no reputation and no ability to link”)。内核程序ihex2fw(从kernel_source/lib/firmware中的Makefile调用.HEX文件)将这些HEX文件转换为任意设计的二进制格式,然后Linux内核使用request_ihex_firmware获取它们,因为他们认为在内核中读取文本文件很愚蠢(这会减慢速度)。

1
现在你已经有了一些声望,并且可以链接了。如果你发现了原帖或者别人的回答中的信息很有用,那就给他们点个赞吧。 - Robert S. Barnes

1

我使用的是Linux 3.5.7 Gentoo,我遇到了同样的问题。 已解决:

emerge ipw2200-firmware

然后进入 /usr/src/linux。
make menucofig

在设备驱动中,移除所有不必要的无线驱动程序,将Intell 2200设置为模块并重新编译。
make
make modules_install
cp arch/x86/boot/bzImage /boot/kernel-yourdefault

1
在当前的Linux系统中,这是通过udev和firmware.agent来处理的。

显然Ubuntu 8.04在udev中没有firmware.agent,它有另一个叫做firmware_helper的东西。无论如何,这怎么告诉内核在哪里找到固件? - Robert S. Barnes
无论 udev 调用什么命令,它都负责将固件加载到内核中。根据我对 Debian firmware.agent 的阅读,它会将固件写入 /sys 中的一个特殊文件中。 - David Schmitt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接