4、设备节点管理:/dev目录详解、静态与动态设备节点、mdev/udev配置

设备节点,说白了就是用户空间访问内核驱动的「门把手」。你想想看,Linux 下一切皆文件,硬件设备也不例外。但硬件怎么变成文件?靠的就是 /dev 目录下的这些特殊文件——设备节点。

我记得刚入行那会儿,有次调试一块 SPI 闪存,死活读写不了。折腾半天,发现是 /dev 下压根没生成对应的节点。嗯,从那以后,我对设备节点管理就格外上心了。

4.1 /dev 目录到底长什么样?

先看看 /dev 里都有啥。随便找个嵌入式板子,执行 ls -l /dev,你会看到类似这样的输出:

crw-rw---- 1 root root 1, 3 Jan 1 00:00 null
brw-rw---- 1 root root 8, 0 Jan 1 00:00 sda
crw-rw-rw- 1 root root 5, 2 Jan 1 00:00 ptmx

注意看第一列:c 开头的是字符设备,b 开头的是块设备。第五列和第六列是主设备号和次设备号——这是内核识别设备的关键。

主设备号:告诉内核用哪个驱动程序。
次设备号:告诉驱动程序操作哪个具体设备实例。

举个例子,1, 3 表示主设备号 1(内存设备驱动),次设备号 3(/dev/null)。内核看到这个组合,就知道该调用内存驱动里的 null 处理函数。

4.2 静态设备节点:老派但可靠

静态设备节点,就是手动用 mknod 命令创建的。我早期做项目时,用的就是这种办法。那时候内核小,设备少,手动创建倒也干脆。

# 创建一个字符设备,主设备号 240,次设备号 0
mknod /dev/mydevice c 240 0

# 创建一个块设备,主设备号 8,次设备号 1
mknod /dev/mydisk b 8 1

静态节点的好处是简单、可控。坏处呢?你想想看,如果设备热插拔,或者驱动加载顺序变了,主次设备号对不上,系统就抓瞎了。我在一个工控项目里就吃过这个亏——换了块内核,设备号变了,结果 /dev 下的节点全废了。

注意:静态节点需要提前知道设备号,且设备号一旦变化,节点就失效。现代嵌入式系统基本不推荐纯静态方案。

4.3 动态设备节点:现代系统的标配

动态设备节点,就是让系统自动创建和删除 /dev 下的文件。内核检测到设备接入,自动分配设备号,用户空间程序自动创建节点。设备移除,节点自动消失。

这背后有两个主流方案:devtmpfs 和 udev/mdev。

4.3.1 devtmpfs:内核自带的「懒人方案」

devtmpfs 是内核 2.6.32 引入的。它会在内核启动时自动挂载一个虚拟文件系统到 /dev,内核每注册一个设备,devtmpfs 就自动在 /dev 下创建对应的节点。

说白了,你不需要额外配置,内核帮你把节点建好了。我现在的项目基本都开这个选项:

Device Drivers --->
  Generic Driver Options --->
    [*] Maintain a devtmpfs filesystem to mount at /dev
    [*] Automount devtmpfs at /dev, after the kernel mounted the rootfs

但 devtmpfs 有个小问题——它只负责创建节点,不负责设置权限、属主、创建符号链接。这些活儿还得交给 udev 或 mdev。

4.3.2 udev:桌面和服务器的标配

udev 是 systemd 的一部分,功能强大,但体积也大。它通过 netlink 监听内核的 uevent,然后根据规则文件执行操作。

规则文件放在 /etc/udev/rules.d/ 下,举个例子:

# 为 USB 串口设备创建符号链接
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="myusb"

# 设置特定设备的权限
SUBSYSTEM=="block", KERNEL=="sda", OWNER="root", GROUP="disk", MODE="0660"

我个人习惯在开发板上用 udev,因为它灵活。但如果你做的是资源受限的嵌入式设备,udev 可能太重了——它依赖 systemd,光 systemd 就得占好几 MB 空间。

4.3.3 mdev:嵌入式系统的轻量之选

mdev 是 busybox 自带的设备管理工具,体积小,依赖少。它通过读取 /proc/sys/kernel/hotplug 来接收热插拔事件。

配置 mdev 很简单,三步走:

  1. 内核配置里开启热插拔支持
  2. 启动脚本里执行 mdev -s 扫描已有设备
  3. 编写 /etc/mdev.conf 规则文件

mdev.conf 的格式很直观:

# 设备名 属主:组 权限 [可选操作]
ttyS0 root:root 0660
sd[a-z] root:disk 0660 */sbin/mount.sh
usb0 root:root 0660 @/sbin/usb_connect.sh

注意最后两列:* 表示设备创建后执行脚本,@ 表示设备移除后执行脚本。我在一个 IoT 网关项目里就用 mdev 管理 USB 4G 模块的节点,稳得很。

我的建议:如果板子 flash 小于 16MB,用 mdev;如果大于 64MB 且跑 systemd,用 udev。中间地带?看心情,反正都能用。

4.4 设备节点管理的核心流程

为了让你更直观地理解,我画了一张流程图,展示从设备插入到节点生成的完整链路:

硬件设备插入/检测 内核检测到设备 devtmpfs 自动创建设备节点 内核发送 uevent 事件 udev/mdev 接收事件 执行规则:设置权限/创建链接 方案一 方案二

这张图展示了两种路径:左边是 devtmpfs 的自动创建,右边是 udev/mdev 的事件驱动。实际系统中,两者经常配合使用——devtmpfs 负责快速创建节点,udev/mdev 负责后续的权限和符号链接管理。

4.5 实战:在嵌入式板子上配置 mdev

说了这么多,来点实际的。假设你有一块 ARM 开发板,跑 busybox 文件系统,想用 mdev 管理设备节点。怎么做?

第一步,确保内核配置了热插拔:

General setup --->
  [*] Support for hot-pluggable devices

第二步,在 /etc/init.d/rcS 启动脚本里加上:

# 挂载 /proc 和 /sys
mount -t proc proc /proc
mount -t sysfs sysfs /sys

# 挂载 devtmpfs(如果内核支持)
mount -t devtmpfs devtmpfs /dev

# 启动 mdev
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

第三步,编写 /etc/mdev.conf。举个例子,我想让 USB 存储设备自动挂载:

sd[a-z][0-9] root:root 0660 @/etc/mdev/automount.sh

automount.sh 脚本内容:

#!/bin/sh
if [ "$ACTION" = "add" ]; then
    mkdir -p /mnt/usb
    mount /dev/$MDEV /mnt/usb
elif [ "$ACTION" = "remove" ]; then
    umount /mnt/usb
fi

我曾经踩过的坑:mdev.conf 里的设备名支持通配符,但别写太宽泛。比如 sd* 会匹配到 sda、sdb,也会匹配到 sda1、sda2。如果你只想匹配分区,用 sd[a-z][0-9] 更精确。

4.6 设备节点管理的避坑指南

做嵌入式这么多年,关于设备节点,我总结了几条血泪教训:

  • 设备号冲突:不同驱动用了相同的主设备号,会导致节点指向错误的驱动。解决办法是检查 /proc/devices,确保设备号唯一。
  • 权限问题:普通用户无法访问 /dev 下的某些节点。我习惯在 udev/mdev 规则里显式设置 MODE,比如 MODE="0666" 让所有用户可读写。
  • 节点残留:设备拔掉后节点没删除,再次插入时可能冲突。用 udev/mdev 的动态管理能避免这个问题。
  • 启动顺序:mdev -s 必须在根文件系统挂载之后执行,否则 /dev 目录不可写。我见过有人把 mdev -s 放在挂载 /dev 之前,结果节点全创建到内存里了。

嗯,设备节点管理看似简单,但真出了问题,排查起来也挺头疼。掌握了静态和动态两种方案,再根据项目需求选型,基本就能游刃有余了。

一句话总结:/dev 是用户空间和内核空间的桥梁,静态节点适合固定设备,动态节点(devtmpfs + udev/mdev)是现代嵌入式系统的标配。选型时考虑资源、灵活性和维护成本,没有银弹。

可可精品资料库