3、存储设备节点与Uevent:Linux设备模型基础、Uevent机制详解、设备节点创建与权限管理

好,咱们今天聊点硬核的。存储设备节点和Uevent,这俩东西在AOSP存储适配里,属于那种「你不懂它,它就会在半夜搞崩你」的知识点。我当年刚接触Android存储时,就被设备节点权限问题坑过一回——明明驱动加载成功了,APP就是读不到数据,查了两天才发现是uevent的权限字符串写错了。嗯,从那以后,我对这套机制就格外上心。

3.1 Linux设备模型:别把它想得太玄乎

Linux设备模型,说白了就是内核用来管理所有硬件设备的一套「户口本」系统。每个设备在/sys目录下都有一个对应的目录,里面放着它的属性、状态、父子关系。你想想看,如果没有这套模型,内核怎么知道插了个U盘还是SD卡?

我个人习惯把设备模型拆成三个核心概念:

  • 总线(Bus):设备之间的通信通道,比如USB总线、PCIe总线、MMC总线。设备挂在哪条总线上,决定了它用什么协议通信。
  • 设备(Device):物理设备或虚拟设备的抽象。每个设备都有一个唯一的kobject,挂在/sys/devices/下面。
  • 驱动(Driver):告诉内核「这个设备该怎么用」。驱动和设备通过总线匹配,匹配上了就绑定。

举个例子,你插一个USB存储设备:

/sys/bus/usb/devices/1-1/          # USB设备节点
/sys/class/block/sda/              # 块设备节点
/sys/devices/pci0000:00/.../sda/   # 实际设备路径

这三个路径其实指向同一个物理设备,只是从不同角度描述它。这就是设备模型的魅力——统一抽象,灵活访问。

重点记住:设备模型的核心是kobject,它是所有/sys目录条目的基石。uevent的生成、设备节点的创建,都离不开kobject。

3.2 Uevent机制:内核怎么通知用户态?

Uevent,全称是User Event,就是内核向用户态发送的事件通知。设备插拔、状态变化、驱动加载,都会触发uevent。Android的vold(卷管理守护进程)就是靠监听uevent来知道「哦,有人插了U盘」。

Uevent的传输方式有两种:

方式 说明 典型路径
netlink socket 通过内核netlink协议直接发送,效率高,实时性好 NETLINK_KOBJECT_UEVENT
uevent_helper 调用用户态程序(如/sbin/hotplug)处理,已基本废弃 /proc/sys/kernel/hotplug

现在Android主要用netlink方式。vold会创建一个socket监听uevent,收到事件后解析字符串,然后做对应的挂载、卸载操作。

一个典型的uevent字符串长这样:

add@/devices/platform/soc/xxx/mmc_host/mmc0/mmc0:0001/block/mmcblk0
ACTION=add
DEVPATH=/devices/platform/soc/xxx/mmc_host/mmc0/mmc0:0001/block/mmcblk0
SUBSYSTEM=block
MAJOR=179
MINOR=0
DEVNAME=mmcblk0
DEVTYPE=disk
SEQNUM=1234

你看,它包含了动作(add/remove/change)、设备路径、子系统、主次设备号等信息。用户态程序拿到这些信息,就知道该创建设备节点了。

我的经验:调试uevent时,可以用udevadm monitor或者自己写个小程序监听netlink socket。我曾经在调试一个eMMC分区识别问题时,就是靠抓取uevent日志发现内核发送的DEVNAME跟预期不一致,才定位到问题。

3.3 设备节点创建:从uevent到/dev/xxx

设备节点,就是/dev目录下的那些文件,比如/dev/block/mmcblk0、/dev/sda1。它们是用户态访问硬件设备的入口。节点创建的核心流程是这样的:

  1. 内核检测到设备插入,生成uevent并发送到用户态。
  2. 用户态的uevent处理程序(Android里是vold或ueventd)收到事件。
  3. 根据uevent中的MAJOR、MINOR、DEVNAME,调用mknod()在/dev下创建节点。
  4. 设置节点权限(owner、group、mode)。

在Android早期版本里,ueventd负责这件事。它的配置文件在/ueventd.rc/vendor/ueventd.rc,里面定义了设备节点的权限规则:

/dev/block/mmcblk0     0660   root     root
/dev/block/mmcblk0p1   0660   root     root
/dev/block/sda         0660   root     root
/dev/block/sda1        0660   root     root

每一行指定了设备路径、权限、所属用户和组。ueventd收到uevent后,会匹配这些规则,然后创建节点并设置权限。

注意:如果ueventd.rc里没有匹配的规则,设备节点可能不会被创建,或者权限不对导致APP无法访问。我曾经遇到一个case,第三方SD卡读卡器插上后/dev下没有节点,查了半天发现是ueventd.rc里没加对应的MAJOR/MINOR规则。

3.4 权限管理:谁可以碰这个设备?

设备节点的权限管理,直接决定了APP能不能读写存储设备。Android的权限模型比标准Linux更严格,因为涉及多用户和应用沙箱。

核心原则:

  • 系统服务(如vold、sdcardfs)通常以root或system用户运行,可以访问所有块设备。
  • 普通APP不能直接访问块设备节点,必须通过Storage Access Framework(SAF)或MediaStore。
  • 特定场景(如OTG U盘),vold会通过FUSE或sdcardfs给APP提供访问路径。

权限设置通常在以下几个地方:

配置文件 作用 示例
ueventd.rc 定义设备节点创建时的权限 /dev/block/mmcblk0 0660 root root
init.rc 定义文件系统挂载和目录权限 mkdir /mnt/media_rw 0770 media_rw media_rw
sepolicy SELinux策略,控制进程对设备节点的访问 allow vold block_device:blk_file rw_file_perms;

我个人建议,在做存储适配时,一定要检查SELinux策略。很多权限问题不是Linux DAC(自主访问控制)的问题,而是SELinux MAC(强制访问控制)在拦截。你可以用avc审计日志来排查:

logcat -b events | grep avc
# 或者
dmesg | grep avc

如果看到类似avc: denied { read } for pid=1234 comm="vold" name="mmcblk0"的日志,那就是SELinux在搞鬼。

避坑指南:我曾经在适配一款车载平台的存储方案时,发现vold能正常挂载U盘,但第三方文件管理器就是打不开。查了两天,最后发现是SELinux策略里忘了给文件管理器进程添加sdcardfs的访问权限。加了一行allow appdomain sdcardfs:dir search;就解决了。

3.5 实战:如何调试设备节点问题?

如果你遇到存储设备无法识别或无法访问的问题,可以按这个顺序排查:

  1. 检查内核日志dmesg | grep -i mmcdmesg | grep -i usb,看设备是否被内核检测到。
  2. 检查ueventcat /sys/kernel/uevent_seqnum看序列号是否在增长,或者用udevadm monitor抓取实时事件。
  3. 检查设备节点ls -l /dev/block/看节点是否存在,权限是否正确。
  4. 检查SELinuxdmesg | grep avc看是否有权限拒绝。
  5. 检查vold状态logcat -s vold看vold是否正确处理了uevent。

嗯,这套排查流程我用了好多年,基本覆盖了90%的设备节点问题。你想想看,从内核到uevent,再到设备节点和权限,每个环节都可能出问题。但只要掌握了这套机制,排查起来就有章可循。

小技巧:在开发板上,你可以手动触发uevent来测试:echo add > /sys/class/block/mmcblk0/uevent。这样不用真的插拔设备,就能验证用户态处理逻辑是否正确。

好了,这一章的内容就到这里。设备节点和uevent是存储适配的底层基础,理解了它们,后面讲vold、sdcardfs、FUSE的时候就会轻松很多。下一章我们聊聊vold的架构和存储卷管理,那才是真正开始「干活」的部分。