ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RK3568 Android 11 Vold-DiskInfo:U盘与硬盘识别原理与定制

2026/10/4 21:46:09 拓冰建站 浏览量
RK3568 Android 11 Vold-DiskInfo:U盘与硬盘识别原理与定制 项目标题里的“Vold-DiskInfo”已经把这个需求点得很透了Android 11.0 在 RK3568 平台上跑起来之后系统里插着的存储设备到底算 U 盘还是硬盘不是靠“感觉”来的而是由 Vold 上报给上层的 DiskInfo 决定的。如果这个环节没搞好应用层看到的设备就全是“一块磁盘”不知道谁是移动存储、谁是固定存储后面做文件管理、OTA、默认存储切换都会出乱子。这阵子我在 RK3568 板子上做 Android 11.0 的系统集成正好被这个“区分 U 盘和硬盘”的问题卡了两天。网上能搜到的资料大部分是讲手机平台怎么适配 OTG 的很少有专门讲 RK3568 这种工规/商规板卡上既带 USB 口又带原生 SATA 口时Vold 是怎么给存储设备“定身份”的。整理一下我的完整排查思路和实测结果给做同类型项目的朋友一个参考。1. 为什么 RK3568 方案里必须搞清楚“是谁插进来了”RK3568 这颗芯片和普通手机 SoC 最大的不同就是它把存储接口做得非常全原生 SATA、PCIe、USB 2.0/3.0、eMMC、SDIO 全部可以同时存在。做商显、NAS、边缘网关、一体机的时候经常是一个产品上既留了 USB 口给用户随便插 U 盘又内置了一块 SATA 硬盘或者 M.2 SSD 做系统数据盘。这就带来一个很实际的麻烦应用层根本分不清存储设备的“身份”。举个例子我在板子上同时插了 U 盘和一块 3.5 寸 SATA 硬盘dumpsys mount里两块设备都能正常挂载。但上层 APP 拿到的 DiskInfo 如果不做区分那么文件管理器里就会把内置硬盘和 U 盘混在一起展示用户不知道该往哪个盘存数据才安全系统做“默认存储迁移”的时候也可能把用户的数据倒腾到一块随时可能拔掉的 U 盘上OTA 升级包如果正好放在 U 盘里升级完成重启后 U 盘还没被重新挂载升级脚本就找不到升级包了。这些问题在手机平台上不太会遇到因为手机几乎只有一个外部存储入口USB OTG判断逻辑天然简单。但 RK3568 这类平台是“多入口”的存储设备的来源、总线类型、是否可移除必须有一个明确的判定机制。Vold-DiskInfo 做的事情就是把这些信息从内核层带上来告诉上层这块盘来自 USB 总线、那块盘来自 SATA 控制器、SD 卡是 MMC 总线、可移动标志是 1 还是 0。把这条链路搞明白比在应用层自己猜设备类型要可靠得多。2. 从内核 uevent 到 DiskInfoVold 的上报链路到底长什么样2.1 起点内核 block 子系统的 uevent不管是 U 盘还是 SATA 硬盘只要在 Linux 内核里被识别成一个块设备都会通过 block 子系统向上层发送 uevent。用udevadm monitor或者直接在 Vold 的日志里都能看到类似这样的信息UEVENT[123456.789012] add /devices/platform/usb/xhci-hcd.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda (block)注意这里的DEVPATH也就是/devices/platform/usb/xhci-hcd.0/usb1/.../block/sda。这个路径非常关键因为它完整记录了设备在 sysfs 里的“出身”——是从哪个总线、哪个控制器挂上来的。U 盘一定会在路径里出现 usb而 SATA 硬盘的路径长这样/devices/platform/sata0/ahci/.../host4/target4:0:0/4:0:0:0/block/sdb里面能找到sata0、ahci但搜不到 usb。这就是系统区分两者的最底层依据。2.2 中游NetlinkManager 接包、VolumeManager 建 DiskAndroid 的 Vold 进程/system/bin/vold内部有个 NetlinkManager专门监听内核 netlink socket把 uevent 解析成 NetlinkEvent 对象。随后 VolumeManager 会根据事件类型决定是创建一块新的 Disk还是给已有的 Disk 添加 Partition。创建 Disk 的时候Vold 会把前面那条DEVPATH保存下来作为这块磁盘的 sysfs 路径。之后 Vold 会去读取一系列属性比如/sys/class/block/sda/size得到设备容量/sys/class/block/sda/device/model得到设备型号字符串/sys/class/block/sda/removable得到可移动标志需要注意/sys/class/block/下面的removable是内核根据设备类型自动生成的USB 大容量存储设备基本都会被标记成 1SATA 内置盘是 0。我们做上层定制时这个值也是重要的参考维度。2.3 终点framework 层的 DiskInfo.javaVold 侧完成磁盘对象创建后会通过 Binder 接口把 Disk 信息传给 framework。framework 的StorageManagerService维护着一份mDisks列表DiskInfo.java就是这个列表里每个元素的具体数据结构。Android 11 的DiskInfo.java里定义了一组标志位public static final int FLAG_ADOPTABLE 1 0; public static final int FLAG_DEFAULT_PRIMARY 1 1; public static final int FLAG_SD 1 2; public static final int FLAG_USB 1 3; public static final int FLAG_MMC 1 4;对应的方法很直观public boolean isUsb() { return (mFlags FLAG_USB) ! 0; } public boolean isSd() { return (mFlags FLAG_SD) ! 0; } public boolean isMmc() { return (mFlags FLAG_MMC) ! 0; }所以从业务代码的角度看区分 U 盘和硬盘最终就是读这几个方法。但这个mFlags是怎么从 Vold 底层传上来的这就是我们要定的关键点。3. DiskInfo 的 USB/SD/MMC 标志位在实际板卡上如何命中3.1 Vold 侧 sysfs 路径扫描的核心逻辑Android 11 的 Vold 代码已经移到了system/vold/model/Disk.cpp在构建 Disk 对象周期里会对前面保存的 sysfs 路径做字符串匹配来判断设备的类别。核心逻辑大致可以概括成这段基于 AOSP 代码的简化还原// system/vold/model/Disk.cpp status_t Disk::create() { // 扫描 sysfs 路径判断设备挂在什么总线上 if (mSysPath.find(usb) ! std::string::npos) { mFlags | kFlagUsb; } if (mSysPath.find(mmc) ! std::string::npos) { mFlags | kFlagMmc; } if (mSysPath.find(sd) ! std::string::npos || mSysPath.find(platform/sdhci) ! std::string::npos) { mFlags | kFlagSd; } ... }从 AOSP 的角度看这套匹配逻辑是为手机/平板类设备设计的所以它对 U 盘、SD 卡、eMMC 这些路径的覆盖已经很成熟。但关键问题是它没有专门处理 SATA 控制器和 NVMe 控制器的路径。这恰恰是 RK3568 平台最容易出情况的地方。3.2 RK3568 上 SATA、USB、eMMC 的 sysfs 路径长什么样我在板子上实测时收集了三种存储设备的实际路径很有代表性设备DEVPATH 特征Vold 是否判为 USB实测 removableU 盘插 USB 3.0 HOST 口/devices/platform/usb/xhci-hcd.0/usb1/1-1/.../block/sda是1SATA 硬盘接原生 SATA 口/devices/platform/sata0/ahci/.../block/sdb否0eMMC 板载/devices/platform/fe2b0000.mmc/mmc_host/mmc0/.../block/mmcblk0否0对比一下就能发现只要设备的 DEVPATH 里出现了 usb 字符串Vold 就会把它打上 USB 标志。原生 SATA 的路径是sata0开头不含 usb所以默认不会被打成 U 盘。这就意味着在 RK3568 的原始代码上U 盘和原生 SATA 硬盘的区分是天然成立的不需要改动 Vold 也能工作。但如果你的产品是“USB 转 SATA 硬盘盒”方案情况就不一样了。硬盘盒的内部是 SATA 盘但对外接口是 USB内核枚举出来的 DEVPATH 里一定包含 usbVold 就会把这个硬盘盒误判成 U 盘。这个下面我会单独说。3.3 framework 侧最终看到的 mFlags 数值Vold 把这些标志位收集到 Disk 对象后通过IVold::getDisks()或者磁盘事件上报framework 侧DiskInfo.java构造时直接把 int 型的 mFlags 重建出来。所以我们可以用一个小实验验证整个链路在 Java 层打印一下 DiskInfo 的 mFlags然后对比插 U 盘和插 SATA 硬盘时的值。实测下来插 U 盘时mFlags包含FLAG_USBdisk.isUsb()返回 true插 SATA 硬盘时mFlags不含FLAG_USBdisk.isUsb()返回 false链路是通的。4. 实测一把从 dumpsys 和 sysfs 反向验证 U 盘与硬盘4.1 dumpsys mount 看系统眼中的设备在 RK3568 板子上U 盘和 SATA 硬盘都插好然后执行adb shell dumpsys mount会看到类似下面的输出不同项目会有差异但结构一致Disk 0 (public:8_0): 29.7 GB, 0x08 Label: UDISK Volume: public:8_0:1 stateMOUNTED, path/mnt/media_rw/8_0:1 ... Disk 1 (public:8_16): 465.8 GB, 0x00 Label: DATA_HDD Volume: public:8_16:1 stateMOUNTED, path/mnt/media_rw/8_16:1其中0x08这个十六进制数就是mFlags的一部分8 对应FLAG_USB。所以插 U 盘的那块 Disk 是 0x08SATA 硬盘是 0x00。用这个输出可以直接验证 Vold 的分类结果不用写代码就能排查。如果你看输出不太确定再补一条命令把 sysfs 底层的属性打出来adb shell cat /sys/class/block/sda/removable adb shell cat /sys/class/block/sdb/removableU 盘大概率是 1SATA 硬盘是 0。这两个层面验证下来Vold 的判断就没跑了。4.2sm list-disks快速确认磁盘列表Android 的 shell 下还有一条很实用的命令adb shell sm list-disks输出通常是disk:public:8_0这种形式虽然没有直接显示 U 盘还是硬盘但配合dumpsys mount里的 flags 就能确认身份。调试阶段我基本都是sm list-disks先拿到 diskId再对着dumpsys mount看 flags效率很高。4.3 应用层代码怎么拿 DiskInfo 做判断如果是系统应用或者你有签名的内建 APP可以直接调用 StorageManager 的接口StorageManager sm (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); ListDiskInfo disks sm.getDisks(); for (DiskInfo disk : disks) { if (disk.isUsb()) { Log.d(TAG, 这是一个 U 盘: disk.getId()); } else { Log.d(TAG, 这不是 U 盘可能是 SATA/其他: disk.getId()); } }这里有个比较容易忽略的点getDisks()返回的列表包含了所有已经发现的磁盘包括内置 eMMC。所以你不要以为非 U 盘就是硬盘它也可能是 eMMC 或者 SD 卡。真正要定位“哪块是 SATA 硬盘”建议还是结合设备容量、挂载路径、diskId 三者一起判断。另外如果要在应用里监听设备的插拔可以注册 StorageManager 的磁盘广播IntentFilter filter new IntentFilter(); filter.addAction(StorageManager.ACTION_DISK_DETACHED); filter.addAction(StorageManager.ACTION_DISK_SCANNED);广播的 intent 里带有EXTRA_DISK_ID拿到 diskId 之后再去getDisks()里查询详细信息不要直接在广播回调里去拿 DiskInfo 对象因为广播触发时磁盘列表不一定已经同步完成实测下来偶尔会拿到 null。5. 默认逻辑不够用时如何定制区分“U 盘”和“硬盘”5.1 “USB 转 SATA 硬盘盒”这个场景必须单独处理前面说的都是原生接口的情况U 盘插 USB 口、SATA 盘插原生 SATA 口Vold 默认行为就能区分。但实际项目里有个很常见的需求用户把移动硬盘USB 转 SATA 硬盘盒插到 USB 口系统既要识别它是一块“硬盘”又希望它不像 U 盘那样被随意弹出。默认 Vold 逻辑下这块移动硬盘的 DEVPATH 里一定会出现 usb所以会被打上FLAG_USB。应用层看到isUsb() true就会把它当 U 盘处理。可它本质上是一块硬盘并不能通过设备类型来判断。这种情况要做二层区分读取设备的型号字符串或者 vendor/model 信息。我处理过的方案是在 Vold 里读取/sys/class/block/sda/device/model把常见的硬盘盒桥接芯片型号JMicron JMS578、ASMedia ASM1153 之类做成一个匹配表jmicron bridge: JMicron Generic asmedia bridge: ASMT1153然后在 Disk 对象创建阶段如果设备被标为 USB但 model 命中了硬盘桥接表就把FLAG_USB去掉换成一个自定义标志或者干脆保持无标志。这样上层拿到的isUsb()就是 false但磁盘的底层信息仍然可以通过dumpsys mount看到完整路径。5.2 用内核 devpath 里的总线控制器名称做扩展有些 RK3568 项目的 SATA 并不是挂在原生sata0上而是通过 PCIe 转 SATA 芯片比如 ASM1062接到 PCIe 上。这时候 DEVPATH 里既没有 usb也没有 sata0而是这样的/devices/platform/pcie0/.../0000:01:00.0/ata5/host5/target5:0:0/5:0:0:0/block/sdc路径里同样没有 usb所以 Vold 不会把它判成 U 盘。但如果你的应用要精确知道它是 SATA 设备还是 NVMe 设备还是得从 DEVPATH 里的ata关键字去做扩展判断。这种情况下我会建议不要只依赖 Vold 默认的 usb/mmc/sd 三个标志位而是直接在 Disk.cpp 里加自定义 flag。比如// 判断是否 PCIe/ATA 硬盘 if (mSysPath.find(ata) ! std::string::npos) { mFlags | kFlagAta; }同时把DiskInfo.java也加一个对应的FLAG_ATA。注意加了新的 flag 之后要同步检查frameworks/base/services/core/java/com/android/server/storage/DiskInfo.java和core/java/android/os/storage/DiskInfo.java两处的 Parcelable 序列化否则 Binder 传值会丢位。这个坑我踩过一次改完 Vold 和 framework 的 flags 后只改了上层isUsb()的判断没有同步改 Parcelable 的读写结果 Vold 报上来的 flag 在 Binder 传输时被截断上层拿到的 mFlags 变成 0排查了快一个下午。5.3 修改 DiskInfo 之后记得处理 fstab 里 voldmanaged 的权限Vold 上报的磁盘类型实际上和fstab里对卷的声明也是联动的。在 Android 11 上如果是外部存储卷fstab里通常有类似配置/devices/platform/usb/* auto auto defaults voldmanagedusb:auto /devices/platform/sata0/* auto auto defaults voldmanagedsata:autovoldmanagedusb:auto前面的usb是卷的 label后面的auto表示自动挂载。如果你改动了 Disk 的 flags但 fstab 里没有给对应的设备路径声明 voldmanagedVold 是不会主动挂载它的。所以定制完区分逻辑后一定要同步检查 fstab 的voldmanaged配置确保多路存储都能正常自动挂载。我们这个项目最后就是用“原始路径匹配 桥接芯片型号过滤 自定义 ATA flag”三层方案落地的。现在板子上插 U 盘、原生 SATA 硬盘、USB 硬盘盒系统都能准确分类应用层再也不用来回猜了。最后再分享一个调试小技巧改完 Vold 后可以用adb shell stop adb shell start重启 framework但 Vold 是独立守护进程如果改了system/vold下面的代码必须整机重启才会生效。调试的时候可以临时用adb root adb shell setprop vold.debug 1打开 Vold 的调试日志抓取关键字对排查非常有用。