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。可按以下步骤验证:

  1. 查看/proc/devices确认主设备号240已注册为字符设备。
  2. 使用mknod /dev/mychar c 240 0创建节点。
  3. 执行echo "hello" > /dev/mychar,观察驱动日志是否收到写入。
  4. 执行cat /dev/mychar,验证读取路径。
  5. 使用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是最常用的四件套。先确认“坐标”是否正确,再确认“驱动”是否就绪,最后确认“权限”是否允许,能快速定位大多数设备文件问题。

设备文件访问路径与主次设备号定位 用户空间程序 open / read / write ioctl / mmap 系统调用 /dev 目录 设备节点 inode 类型 + major:minor 权限 / 属主 / 符号链接 按类型分流 字符设备 c tty / input / null 字节流访问 块设备 b sd / nvme / loop 块与随机访问 字符驱动 file_operations major 240, minor 0~255 块驱动 gendisk / 请求队列 major 8, minor 0~15 mknod 创建节点 = 登记 major:minor 坐标;驱动注册 = 让坐标可被内核分派;udev = 根据 uevent 自动完成节点创建与权限设置

总结来看,设备文件是用户空间访问硬件的“门牌号”,/dev是门牌号的集合,字符与块设备是两类不同的门牌类型,主次设备号是门牌上的坐标,mknod是手工钉门牌的工具,而udev则是自动化的门牌管理系统。掌握这些基础,才能理解后续udev规则为何要匹配KERNEL、SUBSYSTEM、ATTRS,以及为何要设置SYMLINK和MODE。