在 第 6 章 我们构建 systemd 时,已经安装了 udev 包。在我们详细说明 udev 系统的用法之前,我们先大致了解早先的设备控制方式。
传统的 Linux 不管硬件是否真实存在,都以创建静态设备的方法来处理硬件,因此需要在 /dev
目录下创建大量的设备节点文件(有时会有上千个)。这通常由 MAKEDEV 脚本完成,它通过大量调用 mknod
程序为这个世界上可能存在的每一个设备建立对应的主设备号和次设备号。
而使用 udev 方法,只有当内核检测到硬件接入,才会建立对应的节点文件。因为需要在系统启动的时候重新建立设备节点文件,所以将它存储在
devtmpfs
文件系统中(完全存在于内存中的虚拟文件系统)。设备节点文件无需太多的空间,所以占用的内存也很小。
2000 年 2 月,一种名叫 devfs
的文件系统被合并到
2.3.46 版本的内核之中,而在 2.4
系列的稳定内核中基本可用。尽管它存在于内核代码中,但是这种动态创建设备的方法却从来都没有到核心开发者的大力支持。
问题存在于它处理设备的检测、创建和命令的方式。其中最大的问题莫过于它对设备节点的命名方式。大部分开发者的观点是:设备的命名应该由系统的所有者决定,而不是开发者。而且
devfs
存在严重的竞争条件(race
condition)问题,如不对内核做大量的修改就无法修正这一问题。最终,因为缺乏有效的维护,在 2006 年 6
月终被移出内核源代码。
后来,有一种新的虚拟文件系统 sysfs
在 2.5
系列测试版本内核中引入,并且加入了 2.6 系列的稳定版本内核之中。sysfs
系统的任务就是将系统中的硬件配置状态导出至用户空间,而这给开发一种运行于用户空间的新型 devfs
系统带来了可能。
上文简单的提及了 sysfs
文件系统。有些人可能会问,sysfs
到底是如何知道当前系统有哪些设备、这些设备又该使用什么设备号呢。对于那些已经编译进内核的设备,会在内核检测到时被直接注册为
sysfs
对象(由 devtmpfs
内建)。对于编译为内核模块的设备,将会在模块载入的时候注册。一旦 sysfs
文件系统挂载到 /sys,已经在 sysfs
注册的硬件数据就可以被用户空间的进程使用,随后也就可以被 udevd
处理了(包括对设备节点进行修改)。
设备文件是通过内核中的 devtmpfs
文件系统创建的。任何想要注册的设备都需要通过 devtmpfs
(经由驱动程序核心)实现。每当一个 devtmpfs
实例挂载到
/dev
,就会建立一个设备节点文件,它拥有固定的名称、权限以及所有者。
很短的时间之后,内核将给 udevd
一个 uevent。基于 /etc/udev/rules.d
、/lib/udev/rules.d
和 /run/udev/rules.d
目录内文件指定的规则,udevd
将会建立到设备节点文件的额外符号链接,这有可能更改其权限、所有者和所在组,或者是更改 udevd 内建接口(名称)。
这三个文件夹中的规则文件都应以数字编号,并会被一起处理。当发现一个新的设备时,若 udevd 无法找到对应的规则,将会使用
devtmpfs
中初始的权限以及所有者。
编译成模块的设备驱动可能会包含别名。别名可以通过 modinfo
命令查看,一般是模块支持的特定总线的设备描述符。举个例子,驱动 snd-fm801 支持厂商 ID 为 0x1319 以及设备 ID 为
0x0801
的设备,它包含一个「pci:v00001319d00000801sv*sd*bc04sc01i*」的别名,总线驱动导出该驱动别名并通过
sysfs
处理相关设备。例如,文件 /sys/bus/pci/devices/0000:00:0d.0/modalias
应该会包含字符串「pci:v00001319d00000801sv00001319sd00001319bc04sc01i00」。udev
采用的默认规则会让 udevd 根据
uevent 环境变量 MODALIAS
的内容(它应该和 sysfs 里的
modalias
文件内容一样)调用 /sbin/modprobe,这样就可以加载在通配符扩展后能和这个字符串一致的所有模块。
这个例子意味着,除了 snd-fm801 之外,一个已经废弃的(不是我们所希望的)驱动 forte 如果存在的话也会被加载。下面有几种可以避免加载多余驱动的方式。
内核本身也能够根据需要加载网络协议,文件系统以及 NLS 支持模块。
在你插入一个设备时,例如一个通用串行总线(USB)MP3 播放器,内核检测到设备已连接就会生成一个 uevent。这个 uevent 随后会被上面所说的 udevd 处理。
在自动创建设备节点时可能会碰到一些问题。
udev 只会加载包含有特定总线别名而且已经被总线驱动导出到 sysfs
下的模块。在其它情况下,你应该考虑用其它方式加载模块。采用
Linux-5.2.8,udev 可以加载编写合适的 INPUT、IDE、PCI、USB、SCSI、SERIO 和
FireWire 设备驱动。
要确定你希望加载的驱动是否支持 udev,可以用模块名字作为参数运行 modinfo。然后查看 /sys/bus
下的设备目录里是否有个 modalias
文件。
如果在 sysfs
下能找到 modalias
文件,那么就能驱动这个设备并可以直接操作它,但是如果该文件里没有包含设备别名,那意味着这个驱动有问题。我们可以先尝试不依靠
udev 直接加载驱动,并寄希望于这个问题能在日后得到解决。
如果在 /sys/bus
下的相应目录里没有 modalias
的话,意味着内核开发人员还没有为这个总线类型增加 modalias
支持。使用 Linux-5.2.8 内核,应该是 ISA 总线的问题。希望这个可以在后面的内核版本里得到解决。
udev 根本不打算加载 snd-pcm-oss 这样的「封装」驱动程序和 loop 这样的非硬件设备驱动。
如果「封装」模块只是强化其它模块的功能(比如,snd-pcm-oss 模块通过允许 OSS 应用直接访问声卡的方式加强了
snd-pcm 模块的功能),需要配置
modprobe 在 udev
加载硬件驱动模块后再加载相应的封装模块。为此,可以在对应的 /etc/modprobe.d/
文件中增加「softdep」行。例如:
<filename>
.conf
softdep snd-pcm post: snd-pcm-oss
请注意「softdep」也支持 pre:
的依赖方式,或者混合
pre:
和 post:
。查看 modprobe.d(5)
手册了解更多关于「softdep」语法和功能的信息。
如果问题模块并非一个封装,其本身也是有用的话,配置 modules
开机脚本在引导系统的时候加载模块。这样需要把模块名字添加到 /etc/sysconfig/modules
文件里的单独一行。这也可以用于封装模块,但是仅作为备选方案。
要么不要编译该模块,要么把它加入到模块黑名单 /etc/modprobe.d/blacklist.conf
里,如下的例子中屏蔽了
forte 模块:
blacklist forte
被屏蔽的模块仍然可以用 modprobe 强行加载。
这个情况通常是因为设备匹配错误。例如,一条写的不好的规则可能同时匹配到 SCSI 磁盘(希望加载的)和对应厂商的 SCSI 通用设备(错误的)。找出这条问题规则,并通过 udevadm info 命令的帮助改得更具体一些。
这可能是上个问题的另一种表现形式。若非如此,而且你的规则使用了 sysfs
特性,那可能是内核时序问题,希望在后面版本的内核中能得以解决。目前的话,你可以暂时建立一条规则等待使用的 sysfs
特性,并附加到 /etc/udev/rules.d/10-wait_for_sysfs.rules
文件里(如果没有这个文件就创建一个)。如果你是这样做的,并且起作用了,请务必通过 LFS 开发邮件列表通知我们。
后面的内容会假设驱动已经静态编译进内核或已经作为模块加载,而且你也已经确认 udev 没有创建相应的设备节点。
如果内核驱动没有将一个设备的信息导出至 sysfs
系统,则
udev 无法创建相应的设备结点。这种情况经常会在内核树之外的第三方驱动程序中出现。其解决方法是在文件 /lib/udev/devices
中,使用正确的主设备号和次设备号创建一个静态设备结点(相应的设备号可以在内核文档中的 devices.txt
文件或者由第三方驱动程序的文档中找到)。之后 udev 会根据这些信息在 /dev
中创建一个静态设备结点。
这是因为 udev 被设计成并行处理 uevents 并加载模块,所以是不可预期的顺序。这个不会被「修复」。你不应该依赖稳定的内核模块名称。而是在检测到设备的稳定特征,比如序列号或 udev 安装的一些 *_id 应用的输出,来判断设备的稳定名称,之后创建自己的规则生成相应的软链接。可以参考 第 7.4 节 「设备管理」 和 第 7.2 节 「通用网络配置」。
其他的帮助文档可以参考下面的链接: