2. 设备文件基础
在Linux“一切皆文件”的设计哲学中,设备文件是用户空间与内核驱动之间最直接的桥梁。应用程序无需了解磁盘控制器寄存器、网卡DMA描述符或串口波特率寄存器的细节,只要对/dev下的某个文件执行open、read、write、ioctl,内核就会根据该文件记录的主次设备号,把请求分派给对应的驱动。理解设备文件,是理解udev为何存在、如何工作的前提。
一、/dev目录:设备节点的统一入口
/dev是根文件系统中的一个特殊目录,通常由devtmpfs或tmpfs挂载。它并不存放普通数据文件,而是存放“设备节点”(device node)。每个节点在文件系统中表现为一个inode,其类型标记为字符设备或块设备,并携带一对关键数字:主设备号(major)和次设备号(minor)。
现代Linux中,内核在启动阶段通过devtmpfs自动创建基础设备节点,随后udev在用户空间接管,根据内核uevent和规则文件动态增删节点、设置权限、创建符号链接。因此,/dev的内容是“内核发现设备”与“用户空间策略”共同作用的结果。
$ ls -l /dev/sda /dev/ttyS0 /dev/null
brw-rw---- 1 root disk 8, 0 /dev/sda
crw-rw---- 1 root dialout 4, 64 /dev/ttyS0
crw-rw-rw- 1 root root 1, 3 /dev/null
输出中首字符b表示块设备,c表示字符设备;8, 0即主设备号8、次设备号0。权限位中的disk、dialout组,正是udev规则可以干预的部分。
二、字符设备与块设备:两种访问模型
字符设备以字节流方式访问,通常不支持随机寻址,读写操作直接与驱动交互。典型代表有终端/dev/tty*、串口/dev/ttyS*、空设备/dev/null、随机数/dev/urandom、输入设备/dev/input/event*。字符设备驱动实现file_operations中的read、write、ioctl等回调,数据不经过页缓存(除非驱动自行使用)。
块设备以固定大小的数据块为单位访问,支持随机读写,通常经过内核页缓存和I/O调度器。典型代表有磁盘/dev/sda、分区/dev/nvme0n1p1、回环设备/dev/loop0。块设备驱动向块层注册gendisk和请求队列,文件系统在块设备之上构建。需要注意的是,块设备节点本身也可以被dd等工具直接读写,但普通文件系统访问会经过更复杂的缓存与调度路径。
核心区别:字符设备面向“流”,块设备面向“块与随机访问”。但二者在用户空间都表现为文件,都通过主次设备号定位驱动。判断一个节点是字符还是块设备,最直接的方法是ls -l看首字符,或使用stat查看文件类型。
三、主次设备号:驱动与实例的坐标
主设备号标识“使用哪个驱动”,次设备号标识“该驱动管理的第几个设备实例”。内核通过dev_t类型保存二者,用户空间常用major:minor表示。例如/dev/sda为8:0,/dev/sda1为8:1,同一驱动下不同分区通过次设备号区分。
主设备号的分配有静态和动态两种方式。早期驱动常静态申请固定主设备号,容易冲突;现代驱动多使用alloc_chrdev_region或register_blkdev动态获取,内核在/proc/devices中列出当前已注册的字符和块设备主设备号。
$ cat /proc/devices
Character devices:
1 mem
4 tty
10 misc
13 input
...
Block devices:
8 sd
259 blkext
...
次设备号的范围由驱动注册时声明。若驱动注册了0~255的次设备号范围,则内核允许创建对应minor的节点。超出范围的节点即使存在,打开时也会因无驱动响应而失败。
四、mknod与设备节点创建
mknod是创建设备节点的底层命令,需要指定类型、主设备号和次设备号。普通用户通常无权限在/dev下创建设备节点,需要root权限或具备CAP_MKNOD能力。
# 创建字符设备节点
mknod /dev/mychar c 240 0
# 创建块设备节点
mknod /dev/myblk b 241 0
# 查看结果
ls -l /dev/mychar /dev/myblk
crw-r--r-- 1 root root 240, 0 /dev/mychar
brw-r--r-- 1 root root 241, 0 /dev/myblk
创建节点只是“在文件系统中登记了一个坐标”,并不代表驱动已经就绪。若主设备号240没有对应驱动注册,open会返回ENODEV。因此,手工mknod常用于调试驱动、制作根文件系统或恢复被误删的节点。
在udev环境中,设备节点通常由内核uevent触发udev自动创建,不需要手工mknod。但理解mknod有助于看清udev的本质:udev最终也是根据内核提供的major:minor和devname,在/dev下创建节点并设置属性。
五、案例:手工创建并验证一个字符设备节点
假设已加载一个调试用字符驱动,动态获得主设备号240,次设备号0。可按以下步骤验证:
- 查看
/proc/devices确认主设备号240已注册为字符设备。 - 使用
mknod /dev/mychar c 240 0创建节点。 - 执行
echo "hello" > /dev/mychar,观察驱动日志是否收到写入。 - 执行
cat /dev/mychar,验证读取路径。 - 使用
stat /dev/mychar确认设备号与权限。
$ sudo mknod /dev/mychar c 240 0
$ sudo chmod 666 /dev/mychar
$ echo "hello udev" > /dev/mychar
$ dmesg | tail -n 3
[ 8123.456789] mychar: received 11 bytes
$ cat /dev/mychar
hello udev
若打开失败,应依次检查:主设备号是否注册、次设备号是否在驱动声明范围内、节点类型是否正确、权限是否允许当前用户访问。这个排查顺序同样适用于udev自动创建节点失败时的分析。
六、常见误区与复盘
| 误区 | 现象 | 正确理解 |
|---|---|---|
| 认为设备节点就是驱动 | 节点存在但open失败 | 节点只是坐标,驱动需先注册主设备号 |
| 混淆字符设备与块设备 | 用dd读字符设备得到异常结果 | 字符设备面向流,块设备面向块与随机访问 |
| 手工mknod后忘记权限 | 普通用户无法访问 | 需chmod或由udev规则设置OWNER/GROUP/MODE |
| 次设备号超出驱动范围 | open返回ENXIO或ENODEV | 次设备号必须在驱动注册范围内 |
| 认为/dev内容永久不变 | 重启后节点消失或变化 | devtmpfs与udev动态管理,热插拔会增删节点 |
调试设备节点时,stat、ls -l、/proc/devices、dmesg是最常用的四件套。先确认“坐标”是否正确,再确认“驱动”是否就绪,最后确认“权限”是否允许,能快速定位大多数设备文件问题。
总结来看,设备文件是用户空间访问硬件的“门牌号”,/dev是门牌号的集合,字符与块设备是两类不同的门牌类型,主次设备号是门牌上的坐标,mknod是手工钉门牌的工具,而udev则是自动化的门牌管理系统。掌握这些基础,才能理解后续udev规则为何要匹配KERNEL、SUBSYSTEM、ATTRS,以及为何要设置SYMLINK和MODE。