来自Wi-Fi专家的声音

 

Linux 驱动程序的话题

撰写于:2015年8月31日
作者:silex Wi-Fi专家

由于最近一直在讲述过去的话题和各种各样的话题,所以这次我想写一些现实相关的内容。这次我简单总结了一下关于Linux驱动程序和即插即用的内容。
在Linux中,设备检测和驱动程序加载的机制会因版本及配置而略有不同,本次的说明是以Linux 3.10版本左右的配置为前提的。

什么是 “驱动程序”?
Linux 是一种单体(Monolithic)内核操作系统。所谓 “单体”,指的是操作系统核心所需的所有功能(软件)都作为一种子程序,静态链接到一个庞大的可执行文件中。顺便说一下,与单体相对的概念是微内核(Micro Kernel),在这种情况下,各个功能都成为独立的程序,并通过某种非子程序调用的数据交换方式(IPC:进程间通信,Inter Process Communication)进行通信。
在 Linux 中,驱动程序也是一类链接到内核的子程序。可以将驱动程序视为针对特定硬件资源(例如 UART 串口的 /dev/ttymxc0 )的一系列操作(例如open、read、write、close、ioctl )所组成的子程序包。
那么,Linux 虽然是单体内核,但在系统启动后,也能够读取文件,并借助动态链接功能来实现功能扩展(※注)。这被称为可加载内核模块(LKM),以扩展名为 *.ko 的文件形式实现。另一方面,Linux也完全可以将驱动作为单体静态链接到内核镜像(vmlinux)中,并在系统启动时加载所有功能。在这种情况下,所使用的驱动必须在内核配置(Kernel Configuration)时就选定。

(※注)不过,即便使用后加载的内核模块,模块与内核之间的通信依然是通过子程序调用实现。如果内核与模块的编译条件不同,可能会导致结构体的排列顺序或参数传递方式不一致,进而极有可能导致严重的系统崩溃。因此,Linux 实现了一种名为 vermagic 的机制,用于验证内核与模块之间的一致性。另一方面,在微内核操作系统中,只要进程间通信(IPC)的规范保持一致,就无需统一内核与模块的版本。

内核模块
静态链接的模块成为内核镜像(vmlinux)的一部分,而动态链接的模块则是以扩展名为 *.ko 的文件形式存放在文件系统的某个位置,在需要时加载。当前已加载模块的列表可以通过 “lsmod” 命令查看。(由于博客系统的规格无法固定行距设置,所以显示有些参差不齐,还请谅解。)
 
# lsmod
Module                             Size        Used by
ov5642_camera                75119    0
mxc_v4l2_capture            22322    0
ipu_bg_overlay_sdc          4001     1  mxc_v4l2_capture
ipu_still                             1663     1  mxc_v4l2_capture
ipu_prp_enc                      4645      1  mxc_v4l2_capture
ipu_csi_enc                       2841      1  mxc_v4l2_capture
ipu_fg_overlay_sdc           4877      1  mxc_v4l2_capture
ov5640_camera_mipi       21074    0
ov5640_camera                17959    0
ath9k                                 76127    0
ath9k_common                1627      1  ath9k
ath9k_hw                          361268  2  ath9k_common,ath9k
ath                                     13650    3  ath9k_common,ath9k,ath9k_hw
mac80211                         227200  1  ath9k
mxc_dcic                           5334      0
cfg80211                           176095  3  ath,ath9k,mac80211
evbug                                1476      0
 
加载内核模块的基本命令是 “insmod”,通过该命令并在参数中指定 *.ko 文件的文件名(路径名)来实现模块加载。
 
# insmod/lib/modules/3.10.53-1.1.0_ga+g496fbe0/kernel/drivers/net/wireless/ath/ath9k/ath9k.ko

只要使用 insmod 命令,*.ko 文件放在任何地方都可以。但在这种情况下,设备的自动检测以及相应驱动的自动加载(即插即用)功能无法使用。为了让即插即用功能运行,相关模块必须处于能通过 “modprobe” 命令加载的状态。
modprobe 与 insmod 非常相似,但在两点上有很大差异。insmod 是将 “*.ko 文件的文件名” 作为参数指定,而 modprobe 则是将 “模块名” 作为参数。通常情况下,给模块名加上扩展名.ko 就成为文件名,但偶尔也会有不同的情况(例如,Freescale i.MX280 的 SDIO 驱动,文件名是 “mxs - mmc.ko”,但模块名却是 “mxs_mmc” )。

另一个区别在于,当多个模块存在相互引用的依赖关系时,insmod 必须从 “父” 模块开始依次加载,而 modprobe 则会像顺藤摸瓜一样判定所需的父模块并自动加载。
 
# modprobe ath9k
cfg80211: Calling CRDA to update world regulatory domain
cfg80211: World regulatory domain updated:
cfg80211:   (start_freq - end_freq @ bandwidth), (max_antenna_gain, max_eirp)
cfg80211:   (2402000 KHz - 2472000 KHz @ 40000 KHz), (300 mBi, 2000 mBm)
cfg80211:   (2457000 KHz - 2482000 KHz @ 20000 KHz), (300 mBi, 2000 mBm)
cfg80211:   (2474000 KHz - 2494000 KHz @ 20000 KHz), (300 mBi, 2000 mBm)
cfg80211:   (5170000 KHz - 5250000 KHz @ 40000 KHz), (300 mBi, 2000 mBm)
cfg80211:   (5735000 KHz - 5835000 KHz @ 40000 KHz), (300 mBi, 2000 mBm)
cfg80211: Calling CRDA for country: JP
ieee80211 phy0: Atheros AR9280 Rev:2 mem=0xc1120000, irq=155
root@imx6qsabresd:~# cfg80211: Regulatory domain changed to country: JP
IPv6: ADDRCONF(NETDEV_UP): wlan0: link is not ready
cfg80211:   (start_freq - end_freq @ bandwidth), (max_antenna_gain, max_eirp)
cfg80211:   (2402000 KHz - 2482000 KHz @ 40000 KHz), (N/A, 2000 mBm)
cfg80211:   (2474000 KHz - 2494000 KHz @ 20000 KHz), (N/A, 2000 mBm)
cfg80211:   (4910000 KHz - 4990000 KHz @ 40000 KHz), (N/A, 2300 mBm)
cfg80211:   (5030000 KHz - 5090000 KHz @ 40000 KHz), (N/A, 2300 mBm)
cfg80211:   (5170000 KHz - 5250000 KHz @ 40000 KHz), (N/A, 2000 mBm)
cfg80211:   (5250000 KHz - 5330000 KHz @ 40000 KHz), (N/A, 2000 mBm)
cfg80211:   (5490000 KHz - 5710000 KHz @ 40000 KHz), (N/A, 2300 mBm)

虽然 modprobe 如此便利,但要使其正常运行,必须遵守一些规则。首先,modprobe 所处理的所有模块都必须存放在规定的目录中。通常,这些模块位于 /lib/modules/ 目录下。
然而,仅仅将 *.ko 文件放置在 /lib/modules/ 目录下还不够,还需要将 /lib/modules/ 下的内核文件列表注册到 modules.dep 文件(以及其二进制版本 modules.dep.bin)中。这一注册过程是通过在 /lib/modules/ 下运行 depmod 命令来完成的(※注)。

(※注)也就是说,每次对 /lib/modules 下的内容进行任何更改(添加、删除、替换)时,若不执行 depmod 命令,modprobe 的运行就无法得到保证。在桌面 Linux 的自定义环境中,通常会通过 make modules_install 等脚本自动完成 /lib/modules 下 *.ko 文件的复制以及 depmod 的执行。然而,在嵌入式系统的交叉开发环境中,由谁、在何处以及如何复制 *.ko 文件并执行 depmod 命令,会因环境而异。所以,要是不考虑这些,仅手动追加复制 *.ko 文件,就很容易出现 “无法运行” 的问题。另外,depmod 的工作仅仅是 “生成列表”,它不会对存在多个同名模块文件的情况发出警告。因此,明明以为安装了新驱动,但 modprobe 却仍在使用残留的旧驱动,这类问题也相当常见。


即插即用
对于像 USB 和 SDIO 这类支持在运行中插拔(热插拔)的总线而言,每次检测到新设备插入时,都会产生一种名为 UEVENT 的内核事件。即使像 PCI 这种在运行中无法插拔的情况,在启动后立即进行的总线探测(probe)过程中搜索到设备时,同样也会产生 UEVENT 事件。
UEVENT 会附加一个用于标识检测到的设备的 ID 字符串,我们将其称为设备别名。设备别名是类似 “pci:v0000168Cd0000002A” 这样的字符串,其中小写字母表示类别,大写字母表示数值。在这种情况下,“pci:” 表示这是一个 PCI 设备,“v0000168C” 表示供应商 ID 为 0x168C(即 Qualcomm Atheros),“d0000002A” 表示设备 ID 为 0x2A(即 AR9280 芯片组)。
Linux 内核本身并不具备通过 UEVENT 自动加载驱动程序的功能,这一功能由运行在用户空间的守护进程实现。UEVENT 的实现方式也因 Linux 版本而异,在 3.10 版本前后的 Linux 系统中,通常由一个名为 udevd 的守护进程来处理(在更新的 Linux 系统中,运行着名为 systemd 的守护进程,不过 udevd 几乎原封不动地以被 systemd 整合的形式保留了下来)。
udevd 通常存储在 /lib/udev 目录下,它会根据 /lib/udev/rules.d 目录下存放的 “规则文件” 来处理 UEVENT。udevd 的工作范围广泛,包括自动挂载外部存储介质和自动应用网络设置等,但本次我们将仅围绕驱动程序的自动加载继续进行说明。

设备检测时的驱动程序加载通常在 rules.d/80-drivers.rules 中进行处理。虽然这个规则文件的内容会因 udev 的版本或 Linux 版本的不同而略有差异,但大致应该包含如下内容。
 
ACTION=="remove", GOTO="drivers_end"

ENV{MODALIAS}=="?*", RUN{builtin}="kmod load $env{MODALIAS}"
SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="SD", RUN{builtin}="kmod load tifm_sd"
SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="MS", RUN{builtin}="kmod load tifm_ms"
SUBSYSTEM=="memstick", RUN{builtin}="kmod load ms_block mspro_block"
SUBSYSTEM=="i2o", RUN{builtin}="kmod load i2o_block"
SUBSYSTEM=="module", KERNEL=="parport_pc", RUN{builtin}="kmod load ppdev"
KERNEL=="mtd*ro", ENV{MTD_FTL}=="smartmedia", RUN{builtin}="kmod load sm_ftl"

LABEL="drivers_end"

驱动程序的加载由 RUN {builtin}="kmod load $env{MODALIAS}" 来处理。在udev稍旧的版本中,可能会使用RUN+="/sbin/modprobe -bv $env {MODALIAS}" 。前者使用的是 udevd 内置的模块加载功能(与 modprobe 兼容),后者则指示使用外部的 modprobe 来加载模块。无论哪种情况,参数都是 $env {MODALIAS},这里会代入从 UEVENT 作为环境变量传递过来的设备别名。


modinfo 和别名
上文提到 “modprobe 以模块名作为参数运行”,但实际上 modprobe 也可以以设备别名作为参数运行。例如,如果 AR9280 对应的驱动程序是 ath9k.ko,那么无论是输入 "modprobe ath9k" 还是 "modprobe pci:v0000168Cd0000002A",都会产生相同的效果。
这其中有一个巧妙的机制:在 /lib/modules//modules.alias 文件中,详细列出了设备别名与模块名的对应关系。modprobe 会参照这个文件,从设备别名反向查找出模块名,然后再依据这个模块名,从 modules.dep 文件中查找对应的文件名以及依赖信息。modules.alias 和 modules.dep 一样,也有二进制版本的 modules.alias.bin 与之配套,同样是由 depmod 命令生成的。

这些信息…… 每个驱动对应的别名以及依赖模块的信息,都被嵌入在驱动文件 *.ko 自身当中。用于查看这些信息的是 “modinfo” 命令,例如,对 ath9k.ko 执行 modinfo 命令,就会显示出如下结果。
 
$ modinfo ath9k
filename:       /lib/modules/3.11.10-301.fc20.i686/kernel/drivers/net/wireless/ath/ath9k/ath9k.ko
license:        Dual BSD/GPL
description: Support for Atheros 802.11n wireless LAN cards.
author:       Atheros Communications
alias:          platform:qca955x_wmac
alias:          platform:ar934x_wmac
alias:          platform:ar933x_wmac
alias:          platform:ath9k
alias:          pci:v0000168Cd00000036sv*sd*bc*sc*i*
alias:          pci:v0000168Cd00000037sv*sd*bc*sc*i*
alias:          pci:v0000168Cd00000034sv*sd*bc*sc*i*
alias:          pci:v0000168Cd00000034sv000010CFsd00001783bc*sc*i*
alias:          pci:v0000168Cd00000034sv000014CDsd00000064bc*sc*i*
alias:          pci:v0000168Cd00000034sv000014CDsd00000063bc*sc*i*
alias:          pci:v0000168Cd00000034sv0000103Csd00001864bc*sc*i*
alias:          pci:v0000168Cd00000034sv000011ADsd00006641bc*sc*i*
alias:          pci:v0000168Cd00000034sv000011ADsd00006631bc*sc*i*
alias:          pci:v0000168Cd00000034sv00001043sd0000850Ebc*sc*i*
alias:          pci:v0000168Cd00000034sv00001A3Bsd00002110bc*sc*i*
alias:          pci:v0000168Cd00000034sv00001969sd00000091bc*sc*i*
alias:          pci:v0000168Cd00000034sv000017AAsd00003214bc*sc*i*
alias:          pci:v0000168Cd00000034sv0000168Csd00003117bc*sc*i*
alias:          pci:v0000168Cd00000034sv000011ADsd00006661bc*sc*i*
alias:          pci:v0000168Cd00000034sv00001A3Bsd00002116bc*sc*i*
alias:          pci:v0000168Cd00000033sv*sd*bc*sc*i*
alias:          pci:v0000168Cd00000032sv*sd*bc*sc*i*
alias:          pci:v0000168Cd00000032sv0000105Bsd0000E075bc*sc*i*
alias:          pci:v0000168Cd00000032sv00001A3Bsd00002152bc*sc*i*
alias:          pci:v0000168Cd00000032sv00001A3Bsd00002126bc*sc*i*
alias:          pci:v0000168Cd00000032sv00001A3Bsd00001237bc*sc*i*
alias:          pci:v0000168Cd00000032sv00001A3Bsd00002086bc*sc*i*
alias:          pci:v0000168Cd00000030sv*sd*bc*sc*i*
alias:          pci:v0000168Cd0000002Esv*sd*bc*sc*i*
alias:          pci:v0000168Cd0000002Dsv*sd*bc*sc*i*
alias:          pci:v0000168Cd0000002Csv*sd*bc*sc*i*
alias:          pci:v0000168Cd0000002Bsv*sd*bc*sc*i*
alias:          pci:v0000168Cd0000002Asv*sd*bc*sc*i*
alias:          pci:v0000168Cd00000029sv*sd*bc*sc*i*
alias:          pci:v0000168Cd00000027sv*sd*bc*sc*i*
alias:          pci:v0000168Cd00000024sv*sd*bc*sc*i*
alias:          pci:v0000168Cd00000023sv*sd*bc*sc*i*
depends:    ath9k_hw,ath9k_common,mac80211,ath,cfg80211
intree:         Y
vermagic:    3.11.10-301.fc20.i686 SMP mod_unload 686
signer:        Fedora kernel signing key
sig_key:      30:6A:44:54:85:F5:93:A7:D1:76:66:ED:6E:88:70:81:51:C7:51:D4
sig_hashalgo:   sha256
parm:          debug:Debugging mask (uint)
parm:          nohwcrypt:Disable hardware encryption (int)
parm:          blink:Enable LED blink on activity (int)
parm:          btcoex_enable:Enable wifi-BT coexistence (int)
parm:          enable_diversity:Enable Antenna diversity for AR9565 (int)

在 "alias:" 后面,列出了与该驱动程序对应的设备别名,depmod 会提取这些别名来生成 modules.alias 文件。同时,在 "depends:" 后面,列出了 ath9k.ko 运行所需的父模块列表,depmod 会提取这些信息来生成 modules.dep 文件。


总结
总结来说,在 Linux 中设备检测与驱动自动加载的过程如下:

(1) 通过设备热插拔或启动时的总线探测检测到设备,触发以设备别名字符串为参数的 UEVENT。

(2) udevd 等系统守护进程接收 UEVENT,并以别名字符串为参数执行 modprobe 命令(或者使用内置的 kmod load 功能,它与 modprobe 兼容)。

(3) modprobe 会在 /lib/modules//modules.alias 中查找别名。如果找到了对应的别名,它将使用对应的模块名作为参数再次执行 modprobe。

(4) modprobe 会在 /lib/modules//modules.dep 中查找模块名。如果找到了对应的模块名,它会检查该模块的依赖模块列表,并且如果这些依赖模块尚未加载,则会以父模块名作为参数进一步执行 modprobe。

(5) 当所有依赖模块都已加载后,从 modules.dep 中提取与模块名对应的模块文件名,然后执行 insmod 命令(或其等效功能)来加载该文件。

也就是说,整个过程是按照这样的步骤进行的。在 Linux 系统中,“即插即用无法正常工作” 这类问题,是由于上述步骤中的某个环节出现了问题而引发的。换句话说,如果 Linux 的即插即用功能无法正常运行,那么反向梳理这些步骤,便是故障排查的基本方法。

- 能否使用 insmod 直接指定文件名来加载驱动程序?
- 能否使用 modprobe 指定模块名来加载驱动程序?
- 能否使用 modprobe 指定设备别名来加载驱动程序?
- 设备是否被检测到?(可以使用 /sys/bus 等路径查看)
- 是否触发了设备检测的 UEVENT?(可以使用 dmesg、udevadm 等工具进行检查)
- 检测到的设备别名是否已注册到 modules.alias 中?

比较简单的情况通过这些方法就能解决,但在 Linux 的世界里,也存在一些因更为复杂的情况导致无法正常运行的案例。其实,我写这篇文章,也是因为在排查 “特定 udev 版本下,特定条件的驱动有时无法加载” 这一棘手问题时,将相关资料重新整理利用了起来。
 




 

相关文章

  •  

Silex 产品信息

我们的技术和产品旨在建立设备之间的通信,并确保这种通信能够顺畅地稳定进行。
而且,更重要的是,使用 Silex 技术和产品的客户可以绝对放心。 我们正是致力于实现这一目标。