1. RK3568存储需求背景分析
1.1 为什么需要区分U盘和硬盘
先说个具体的场景。我在做的这块RK3568板卡,跑的是Android 11.0系统,硬件上同时带了USB 3.0接口和一路原生SATA接口。产品定义是这样的:eMMC跑系统,SATA口挂一块2.5寸机械硬盘做本地数据中心,USB口留给用户插U盘或者移动硬盘做数据导入导出。等到我把第一版系统烧进去,插上U盘再插上硬盘,发现设置里两个设备显示出来的标签几乎一样,都是"USB存储设备"之类的泛称,应用层根本分不清哪个是板上的硬盘、哪个是用户刚插进来的U盘。
这个不是个例。市面上用RK3568做NAS、边缘计算盒、工业一体机的方案越来越多,这类产品几乎都有一个共同点:既要有USB口方便用户临时拷贝数据,又要有SATA/NVMe这种大容量存储用来做长期数据保存。两个接口在系统层面看起来到底有什么区别?默认情况下真的没有区别——它们都被Android当成"外部存储卷"来处理,设备节点都是sdX,存储框架只关心你能不能挂载、能不能格式化,根本不管你是接在USB上还是挂在SATA总线上。这个需求之所以值得单独写一篇,是因为要把"硬件层面的接口差异"翻译成"系统和应用能识别的语义差异",中间要过Vold这一大关。
1.2 Vold在Android外部存储体系里的位置
Vold的全称是Volume Daemon,翻译过来就是卷管理守护进程,它是Android系统里负责所有外部存储设备生命周期管理的核心Native服务。你在系统UI上看到的U盘图标、存储设置里的每个分区、adb shell里执行sm mount时触发的挂载动作,底层全部由Vold串起来。
它的工作方式简单概括是三步:第一步,通过NetlinkManager监听内核netlink socket上报的uevent事件;第二步,在NetlinkHandler里解析这些事件,识别出新增/移除的是磁盘还是分区;第三步,把解析结果交给VolumeManager,由它维护一个完整的设备状态模型,再通过Binder接口同步给Framework层的StorageManagerService,最终由它把变化通知给所有关注存储状态的应用。
Vold里最核心的几个数据结构是Disk、VolumeBase、PublicVolume和PrivateVolume。每个物理磁盘对应一个Disk对象,Disk下面再按分区表拆成若干个Volume对象。咱们标题里提到的DiskInfo,其实分为两层:Native层对应的是Disk这个C++类,Java层对应的是android.os.storage.DiskInfo这个Parcelable对象。StorageManagerService会把Vold上报的Disk信息包装成DiskInfo,再通过StorageManager暴露给应用。
流程不复杂,但问题就出在Disk对象创建时的那几个标志位上。
2. Vold与DiskInfo体系剖析
2.1 Disk对象的诞生过程
在Android 11的源码里,Vold由init进程通过system/vold/vold.rc启动,入口是main.cpp。标准启动流程里会创建NetlinkManager、VolumeManager,然后开启netlink监听线程。当用户插入一个U盘,内核的usb-storage驱动会注册一个新的SCSI磁盘,接着通过block层发出KOBJ_ADD事件,Vold的NetlinkHandler收到以后就会层层上报。
关键代码在VolumeManager::handleBlockEvent里。这个方法接收一个NetlinkEvent指针,先从事件里取出DEVPATH、DEVNAME这些属性,然后根据DEVTYPE判断是disk还是partition。如果是disk,就会做一系列初始化,最终new一个Disk对象出来存进内部的vector。这里有一个细节非常值得注意:
// system/vold/VolumeManager.cpp (Android 11.0) if (type == "disk") { std::string eventPath(ev->get("DEVPATH")); std::string sysPath = "/sys" + eventPath; std::string device = ev->get("DEVNAME"); ... int flags = 0; if (StringStartsWith(device, "mmcblk")) { flags |= kFlagSd; } ... auto disk = new Disk(flags, eventPath, sysPath, dev, nickname);这段代码里,Android原生逻辑判断一个磁盘是不是SD卡,只看设备节点是不是以mmcblk开头。mmcblk开头的一般是eMMC和SD/TF卡,这个判断在手机、平板上问题不大。但在RK3568这类设备上,外置的TF卡走的是SDMMC控制器,也是mmcblk节点;而板载eMMC通常也是mmcblk0。所以仅仅靠这个判断,连TF卡和eMMC都分不清,更不用说U盘和SATA硬盘了——它们俩都是sdX节点,在Vold看来就是一模一样的两块"未知磁盘"。
2.2 DiskInfo标志位的来龙去脉
Android其实早就想在系统层面区分不同种类的磁盘。在Native层的include/gui/...不对,是在system/vold/Disk.h里,定义了一组磁盘标志:
// system/vold/Disk.h enum { kFlagAdoptable = 1 << 0, // 可合并到内部存储 kFlagDefaultPrimary = 1 << 1, // 默认主存储 kFlagSd = 1 << 2, // SD卡 kFlagUsb = 1 << 3, // USB设备 };对应到Java层的android.os.storage.DiskInfo.java里,也有这么一份:
// core/java/android/os/storage/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;看到没有?系统实际上预留了FLAG_USB这个位,说明设计者已经预料到需要区分USB设备。但是问题在于,Vold的Volumemanager在创建Disk对象时,压根没有哪个分支会去设置kFlagUsb。整个Android 11 AOSP源码里,kFlagUsb几乎处于“只定义不赋值”的状态。你可以grep一下system/vold目录,搜索结果只会出现在两个地方:Disk.h里的定义和Java层的映射文件,没有任何一行业务代码会给它赋值。所以指望系统开箱就把U盘标出来,是不可能的。
2.3 现有逻辑在实际RK平台上的表现
在RK3568的Android SDK里,瑞芯微对Vold做了一些定制,但大多数改动集中在分区挂载和eMMC/固件升级相关逻辑上。对于磁盘类型的判断,基本还是沿用AOSP那套。所以实际呈现出来的效果就是:你在设置里看到一个USB存储设备,但其实它可能是一个用USB转SATA底座接的3.5寸机械硬盘;你以为是内置硬盘的设备,可能只是一个插在USB 2.0口上的老U盘。
这就给上层应用带来了三个典型问题:
第一,存储标签混乱。比如插了一块希捷硬盘,系统显示的名字可能是"USB存储设备"或者干脆是一串编号,用户感受很差。
第二,挂载策略无法区分。有些产品希望U盘插上以后自动挂载、弹出后自动卸载,而SATA硬盘要在系统启动时就挂载好,甚至要开机自检、修复文件系统。如果系统分不清两者,就无法对不同设备执行不同的生命周期策略。
第三,权限和数据安全无法精细化控制。比如NAS产品里,硬盘数据要求只有特定应用能访问,U盘则允许用户随意读写。在Framework层看到的是同样的"外部存储卷",上层只能一刀切处理。
所以结论很明确:要在RK3568 Android 11.0上区分U盘和硬盘,光靠系统现有逻辑是不够的,必须对Vold做定制开发。
3. 设备识别原理:从sysfs挖出真实身份
3.1 为什么设备节点名不靠谱
先敲一下重点:U盘和SATA硬盘在/dev/block下都叫sdX(sda、sdb、sdc这样),这个命名是内核SCSI子系统统一分配的。为什么?因为USB Mass Storage协议在Linux内核里本身就是模拟成SCSI磁盘来处理的,SATA硬盘走的又是libata驱动,libata同样向上层暴露出SCSI磁盘接口。所以两种设备在内核眼里都是SCSI磁盘,节点名撞车就避免不了。
如果产品里还有NVMe硬盘,情况稍好一点,它的节点是nvme0n1这种。但NVMe同样面临一个问题:它可能直接挂在RK3568的PCIe控制器下,也可能通过一个PCIe转USB的桥接芯片插在USB口上,节点名同样不能说明物理接口。
那什么东西能区分设备到底挂在哪个总线上?答案在sysfs里。
3.2 sysfs路径里藏着完整拓扑
sysfs是内核暴露设备模型的虚拟文件系统,挂在/sys目录。每个块设备在/sys/block/下面都能找到对应的目录,比如/sys/block/sda。在这个目录下,有一个关键的符号链接:device。
这个链接指向的是该块设备底层物理设备在sysfs里的路径,而从根节点到目标设备的整条路径,实际上就是该设备从CPU总线开始挂载的完整拓扑链。换句话说,只要把这个符号链接的内容读出来,看一眼路径里出现了哪些控制器节点的关键字,就能知道它是从USB总线过来的,还是从SATA控制器过来的。
以我手上一台RK3568设备为例,分别插入U盘和接上SATA硬盘,实际看到的链接内容如下。
U盘插在USB 3.0口上:
rk3568:/ $ ls -l /sys/block/sda/device lrwxrwxrwx 1 root root 0 1970-01-01 08:00 /sys/block/sda/device -> ../../devices/platform/soc/fe800000.usb/xhci-hcd.0/usb3/3-1/3-1:1.0/host0/target0:0:0/0:0:0:0SATA硬盘接在板载SATA口上:
rk3568:/ $ ls -l /sys/block/sdb/device lrwxrwxrwx 1 root root 0 1970-01-01 08:00 /sys/block/sdb/device -> ../../devices/platform/soc/fe800000.sata/ata1/host0/target0:0:0/0:0:0:0看到区别没有?前者的路径里出现了fe800000.usb、xhci-hcd.0、usb3/3-1这些关键字,说明这个磁盘挂在USB控制器下;后者的路径里出现的是fe800000.sata、ata1,说明它挂在SATA控制器下。判断逻辑一下就简单了:读取/sys/block/sdX/device这个符号链接的内容,如果里面含有"usb"字样,就认为它是USB设备;如果含有"sata"或者"ata",就认为是SATA硬盘。
3.3 还需要考虑的边界情况
只判断usb和sata两招,能覆盖大部分场景,但做产品要稳,还得考虑几个变种。
第一种,NVMe设备。RK3568通过PCIe接口可以扩展NVMe SSD,这种情况下的sysfs路径会包含"nvme"和"pci"字样,比如:
/sys/block/nvme0n1/device -> ../../devices/platform/soc/.../pci0000:00/0000:00:03.0/0000:01:00.0/nvme/nvme0/nvme0n1如果你的产品把NVMe也当成"硬盘",那判断条件要把"nvme"也算进去。
第二种,USB转SATA/转NVMe桥接设备。这种就是市售的硬盘盒移动硬盘,底层是SATA或NVMe盘,但外面套了个USB桥接芯片。它的sysfs路径里一定包含"usb",因为整个链路就是USB控制器到USB设备(桥接芯片),然后再到SATA/NVMe。从物理接口角度看,它确实属于USB设备;从应用需求角度看,很多时候用户希望把它当成"移动硬盘"而不是"U盘"。这个语义上没有统一答案,完全取决于产品定义。如果产品需要按物理接口分,那这类硬盘盒归入U盘一类没问题;如果需要按存储介质分,就得额外想办法读取硬盘的identify信息或者通过其他途径判断是不是转接设备,复杂度会上一截。
第三种,内置TF卡和外置SD卡。mmcblk开头的设备在sysfs路径里同样有区分,内置eMMC一般挂在平台的sdhci控制器(比如fe310000.mmc),而外置TF卡可能走另一个sdmmc控制器(比如fe2b0000.mmc)。如果你的产品还想把TF卡单独识别出来,可以用同样的方法去解析路径关键字。
有了这个base,接下来就能动手改代码了。
4. 实操:在Vold中落地U盘与硬盘的区分
4.1 方案选型:改Vold还是改上层
开始写代码之前,先想清楚改动放在哪一层。最简单的做法是完全不动Vold,只在应用层或在StorageManagerService里解析/sys/block/xxx/device,然后自己维护一个磁盘类型map。这个方案听起来省事,但有个致命问题:应用层拿到的不一定是sysfs路径,或者拿到路径之后还得自己保证权限、考虑设备热插拔导致路径变化。而且每次新插一个磁盘,上层都要重新扫描一遍sysfs,逻辑散落在各处,维护成本很高。
更好的做法是把判断逻辑下沉到Vold。因为Vold是唯一一个既能及时感知设备插拔、又有权限直接访问sysfs的系统组件,而且它管理着Disk对象和Volume对象的生命周期,在Disk创建时就把类型定下来,后续所有流程——挂载策略、标签显示、事件广播——都可以直接复用这个结果。
具体有两种实现路径:
路径A:最小改动版。沿用系统已有的FLAG_USB标志位,在VolumeManager创建Disk时,判断sysfs路径并设置kFlagUsb。上层代码通过这个标志位区分U盘和非U盘。对于此题,U盘设置FLAG_USB,SATA/NVMe硬盘不设置FLAG_USB,再配合设备节点名或径路径判断,就能达到目的。这个方案改动小、风险低,强烈推荐先用它。
路径B:完整扩展版。自定义一个新的标志位,比如kFlagHdd,并且通过Binder接口、Parcelable序列化一路传到Java层,让所有应用直接通过DiskInfo.FLAG_HDD来识别。这个方案改动面广,需要动到aidl文件和几个跨进程对象,但如果产品确实需要把硬盘作为一种独立的设备类型来管理,值得去做。
下面先讲路径A,再介绍路径B的做法。
4.2 路径A:在VolumeManager中设置FLAG_USB
改动点是system/vold/VolumeManager.cpp的handleBlockEvent。在构造Disk之前,添加一个工具函数用来解析sysfs路径。
这个工具函数在Android的C++环境里没有现成的,自己写一个即可,注意不要引入过多的库依赖:
// system/vold/VolumeManager.cpp #include <unistd.h> #include <limits.h> #include <string> static bool isDeviceNodeOnUsb(const std::string& sysPath) { char buf[PATH_MAX]; std::string deviceLinkPath = sysPath + "/device"; ssize_t len = readlink(deviceLinkPath.c_str(), buf, sizeof(buf) - 1); if (len == -1) { return false; } buf[len] = '\0'; std::string deviceLink(buf); // USB设备路径中一定会出现"usb"关键字 return deviceLink.find("usb") != std::string::npos; }然后在handleBlockEvent的disk分支里,在new Disk之前追加判断:
if (type == "disk") { ... int flags = 0; if (StringStartsWith(device, "mmcblk")) { flags |= kFlagSd; } // 新增:判断是否是USB存储设备 if (StringStartsWith(device, "sd")) { if (isDeviceNodeOnUsb(sysPath)) { flags |= kFlagUsb; } } auto disk = new Disk(flags, eventPath, sysPath, dev, nickname); ... }看到没有,这里加了一个前置条件:只有设备节点以sd开头的才去判断usb,mmcblk设备直接跳过,减少无谓的readlink操作。因为只有SCSI磁盘(sdX)才可能是USB存储或者SATA硬盘。NVMe设备虽然节点不是sdX开头,但同样可能挂在USB桥接后面;不过那种链路在sysfs路径里也会出现"usb",所以如果你需要识别USB转NVMe硬盘盒,判断条件就不要只限定sd前缀,可以把"nvme"也纳入判断范围:
if (StringStartsWith(device, "sd") || StringStartsWith(device, "nvme")) { if (isDeviceNodeOnUsb(sysPath)) { flags |= kFlagUsb; } }这样U盘会被标记为FLAG_USB,SATA硬盘和NVMe硬盘都不会被标记,天然区分。
4.3 上层读取识别结果
Vold设置了FLAG_USB之后,Data会通过Binder接口同步到StorageManagerService,StorageManagerService在创建Java层DiskInfo时会把这个flags原样带过去。应用层通过StorageManager.getDisks()拿到的每个DiskInfo对象,都可以直接检查标志:
StorageManager sm = context.getSystemService(StorageManager.class); List<DiskInfo> disks = sm.getDisks(); for (DiskInfo disk : disks) { boolean isUsb = (disk.flags & DiskInfo.FLAG_USB) != 0; boolean isSd = (disk.flags & DiskInfo.FLAG_SD) != 0; // 如果既不是USB也不是SD,而且节点是sdX/NVMe,基本可以认为是内置硬盘/其他类型 String label = isUsb ? "U盘" : "硬盘"; Log.d("StorageDebug", "Disk " + disk.getId() + " -> " + label); }框架层这一侧不需要做任何改动。StorageManagerService自己也会把DiskInfo缓存在mDisks列表里,所以设置界面直接就会用到这个flags。
如果你同时插了U盘和SATA硬盘,又想让两个盘在界面上显示不同的名字,可以顺手改一下settings或者SystemUI里读取DiskInfo标签的逻辑。比如在PackagesUtils或者Settings的StorageDashboardFragment里,原来显示disk.getDescription()的地方,改成根据flags判断,分别显示"USB存储设备"和"内部硬盘"。这个属于上层UI定制,基础已经打好了。
4.4 路径B:自定义扩展一个硬盘标志位
如果产品团队希望更明确地区分USB设备、SD卡和内置硬盘,而不是靠“非USB即硬盘”这种默认推导,那就在A的基础上继续加。
Native层在Disk.h里新增一个枚举位:
// system/vold/Disk.h enum { kFlagAdoptable = 1 << 0, kFlagDefaultPrimary = 1 << 1, kFlagSd = 1 << 2, kFlagUsb = 1 << 3, kFlagInternalDisk = 1 << 4, // 新增:内置SATA/NVMe硬盘 };然后在VolumeManager.cpp里,对非USB、非SD且节点名为sdX/nvme的磁盘设置这个位:
if ((flags & kFlagUsb) == 0 && (flags & kFlagSd) == 0) { if (StringStartsWith(device, "sd") || StringStartsWith(device, "nvme")) { flags |= kFlagInternalDisk; } }接着是Java层的DiskInfo.java,同步新增一个标志位:
public static final int FLAG_INTERNAL_DISK = 1 << 4;还需要修改DiskInfo的Parcelable序列化,确保标志位能正确跨进程传输。DiskInfo.java里flags字段已经是Parcelable的基础字段之一,只要flags值变了,Parcel里写入的int数值自然能传过去。但如果Framework层在某个地方单独筛选或映射了flags,要记得把新增的位加进去,比如StorageManagerService里可能有对这种flags的过滤逻辑,或者SystemUI里有switch判断。
修改完之后,应用层就能用更明确的语义判断:
boolean isHdd = (disk.flags & DiskInfo.FLAG_INTERNAL_DISK) != 0;注意,这个改法需要同步思考一个问题:标志位是给Native和Java共享的一个数字,跨模块定义时务必保持数值一致,否则会出现Native设置了位而Java读不到的情况。这个坑我实际踩过,当时改的是另一个平台,Native用1<<5,Java里顺手写了1<<6,调试了一个下午。
4.5 编译与部署
改完代码以后,需要编Vold模块。Android 11的源码树里,仍然推荐用mmm或者make来编单模块。我在实际开发中一般是这样操作的:
source build/envsetup.sh lunch rk3568_xxx-userdebug mmm system/vold编译完成后,产物在out/target/product/rk3568_xxx/system/bin/vold。有两种部署方式。
第一种,整包升级。执行make systemimage,刷整个system分区。这个方法稳妥,但耗时长,适合最终验证的时候用。
第二种,单独替换。开发调试阶段没必要整包刷机,直接把vold二进制push到设备上重启即可:
adb root adb remount adb push out/target/product/rk3568_xxx/system/bin/vold /system/bin/vold adb shell chmod 755 /system/bin/vold adb reboot注意,如果你改的只是vold这个二进制,且系统开启了SELinux,换掉之后可能需要重新设置文件上下文。通常adb remount之后push进去的文件会继承原文件的SELinux label,一般没问题,但如果遇到SELinux avc denied日志,就要手动restorecon一下:
adb shell restorecon /system/bin/vold还有一种情况是ro.secure级别较高时remount失败,需要先adb disable-verity。我在RK平台上就遇到过,串口提示dm-verity verification failed,解决方法是:
adb root adb disable-verity adb reboot # 重启后再执行 adb root adb remount这个流程做完再push文件,就稳妥了。
5. 验证、调试与避坑指南
5.1 快速验证命令集合
代码改完、系统跑起来以后,第一件事是验证Vold有没有正确设置FLAG_USB。推荐用下面这几条命令从底层往上排查。
先看Vold日志,确认磁盘事件有没有被正确处理:
adb logcat -s Vold你会看到类似这样的输出:
D Vold : About to add disk event path: /devices/platform/soc/fe800000.usb/xhci-hcd.0/usb3/3-1/3-1:1.0/host0/target0:0:0/0:0:0:0 D Vold : Disk added: sda再查看块设备对应的sysfs链接,确认判断依据本身是对的:
adb shell ls -l /sys/block/sda/device adb shell ls -l /sys/block/sdb/device最关键的是用dumpsys看StorageManagerService里的状态:
adb shell dumpsys mount adb shell dumpsys storagedumpsys mount命令的输出里会包含Vold上报的所有Disk和Volume信息,字段有id、flags等。flags值是十进制数字,需要自己转成二进制再对照判断标志位。比如flags = 8,转成二进制就是1000,对应位3,那就是FLAG_USB被设置了。如果查看SATA硬盘时flags = 0,说明没有设置USB,符合预期。
如果发现flags没有变化,优先检查Vold版本是不是真的更新了:
adb shell pidof vold adb shell cat /proc/<vold_pid>/maps | head -1或者直接查看设备的vold二进制编译时间:
adb shell ls -l /system/bin/vold5.2 常见问题与排查方法
问题一:修改了代码但flags没变。多半是vold没被替换成功,或者替换后进程没重启。确认adb shell pidof vold的PID发生了变化,如果没变,手动kill掉让init重新拉起。
问题二:U盘和硬盘都被标记成USB。检查sysfs路径,看硬件是否通过USB转SATA桥接的硬盘盒接的。如果确实是USB转SATA硬盘盒,从物理接口角度看它确实是USB设备,这是预期行为。如果产品必须区分,要在需求上明确这是按“物理接口”分还是按“盘体类型”分。
问题三:SATA硬盘识别出来了,但应用还是显示成USB存储。检查Framework层是否有自己的逻辑覆盖了DiskInfo的flags。在Android 11的设置里,StorageDashboardFragment对磁盘类型的判断逻辑可能不止看flags,还可能通过isSd()等辅助方法。可以在Java层打日志确认。
问题四:导致系统启动异常、开机卡住。遇到这种情况,先回退代码,确认是Vold改动引起。通常我们在handleBlockEvent里加的readlink逻辑不应该导致阻塞,但如果遇到某些异常设备(比如device链接指向不存在的目录),readlink可能失败或者卡在IO等待上。稳妥的做法是在函数入口加个超时或者提前检查路径是否存在。
问题五:NVMe硬盘被识别成USB设备。如果在sysfs路径里出现usb关键字,说明这个NVMe确实是USB桥接的(硬盘盒)。如果NVMe是直接插在PCIe上,路径里不会出现usb,判断逻辑可以放心。还有种特殊情况:RK3568平台上,如果NVMe是通过可拆卸M.2口连接,kernel有可能把整个PCIe控制器直接挂在usb枚举下面?这个不太常见,但遇到异常设备时要学会看实际sysfs输出,而不是死套一条判断规则。
5.3 扩展思路:把判断能力做成通用接口
做完了这版修改,我后来又回头想,这个判断逻辑能不能做更通用一点?比如把设备类型抽象成一个枚举,Vold直接给上层上报TYPE_USB、TYPE_SATA、TYPE_NVME、TYPE_SD这种结构化数据,而不是让上层靠flags位去猜。这种设计在对接上层各种产品需求时确实更友好,但改动量也会更大。
目前的实用建议是:如果你的产品只需要一个简单区分,先用路径A,即设置FLAG_USB。它利用了系统预留的机制,代码量小、风险低,上层判断也直观。如果产品后续真的要把“内置硬盘”作为一个独立品类来管理,再升级到路径B,给DiskInfo扩展一个FLAG_INTERNAL_DISK或者索性加一个diskType属性。
另外,如果你在做的是多个存储设备同时存在的复杂场景,建议在Vold层做一个设备类型日志,把每次插入的磁盘路径、flags值、识别结果都打出来。这样测试阶段能快速定位,避免反复插拔U盘、来回改代码。
我在调试这套逻辑时,最后还在StorageManagerService里加了一个很小的日志,把每次磁盘updates的flags变化过程记录下来。这个日志在不稳定复现问题的时候帮了大忙,因为很多时候你会发现,前几次插入flags是对的,但某一次热插拔之后,事件分发顺序变了,flags被后续事件覆盖了或者没传上来。这属于Vold事件处理的时序问题,跟判断逻辑本身没有关系。
所以如果你在实测中发现偶发性的类型识别错误,别急着怀疑判断条件,先抓日志看事件到达Vold和Framework的顺序。
这块代码改完以后,我又顺手验证了TF卡的场景,用同样的方法把SD卡从mmcblk中区分出来,也成功跑通了。识别逻辑放在Vold里,胜在一劳永逸——所有上层模块不用各搞一套,系统级识别好了,大家就都能用。 ## 5. 调试与排查技巧实录
5.1 用什么命令快速看结论
代码改完,系统跑起来之后,第一步肯定是验证Vold有没有正确设置FLAG_USB。这个阶段我习惯按“从底层到上层”的顺序来,先看sysfs里的原始路径,再看Vold日志,最后用dumpsys确认Framework层拿到的flags值。
最直接、也最不会骗人的一条命令,是查看块设备对应的device符号链接。无论你插的是U盘还是SATA硬盘,插上去先执行一下:
adb shell ls -l /sys/block/sda/device adb shell ls -l /sys/block/sdb/device手工确认路径里有无usb关键字之后,再查看Vold的日志:
adb logcat -s Vold正常执行后,日志里会出现类似下面的内容:
D Vold : About to add disk event path: /devices/platform/soc/fe800000.usb/xhci-hcd.0/usb3/3-1/3-1:1.0/host0/target0:0:0/0:0:0:0 D Vold : Disk added: sda如果这一步没看到新增日志,大概率是uevent事件没到Vold,或者被上层的netlink处理逻辑拦掉了。
Framework层的验证,直接用:
adb shell dumpsys mount adb shell dumpsys storagedumpsys mount的输出里会带出Vold上报的Disk和Volume信息,包括id、flags、sysPath这些字段。比如我在这台RK3568上同时插了U盘和SATA硬盘,dumpsys mount里会显示两个disk:一个的flags里带了FLAG_USB(十进制算下来是8),另一个flags是0。注意这儿的flags是十进制数,你需要心算或者手动转一下二进制,对照前面说的枚举值来确认。
如果你改的是StorageManagerService或者上层framework代码,建议再加一层dumpsys storage的验证,因为这个服务的缓存数据是给上层应用直接用的。有时候Vold侧已经正确识别了,但StorageManagerService内部还保留着老的数据结构,这时候就需要确认磁盘事件有没有被正确广播到Java层。
5.2 常见的坑和问题排查
问题一:改完了代码,flags还是没变化。
优先确认你push的vold二进制是不是真的生效了。Android system分区的权限检查比较严格,如果你用的userdebug版本,在执行adb remount之前要先确认dm-verity已经关闭。如果忽略了这一步,实际跑在设备上的还是原来的vold。验证方法很简单:
adb shell "ps -A | grep vold" adb shell "cat /proc/$(pidof vold)/maps | head -1"或者最干脆的,在Vold.cpp里临时加一条LOGI打点,重新编一版push进去,看日志有没有打出来。这样能100%确认你改的代码在跑。
问题二:同一个USB口插U盘没问题,插USB硬盘盒就被识别成硬盘了。
这个属于预期行为。USB硬盘盒本质上是把SATA或NVMe设备包在一个USB桥接芯片里,sysfs路径中必然出现usb字样。如果你拿到的产品定义是“凡是USB口接入的都算U盘”,那这个结论没问题;如果产品需要区分“U盘”和“移动硬盘”,那光靠sysfs路径就不够了——你还得读取存储设备的INQUIRY数据、看是不是USB Mass Storage类型,甚至去匹配VID/PID。这个复杂度就上来了,正常情况下优先级不高。
问题三:热插拔的时候,偶发性识别错误。
我遇到过一种情况:U盘拔掉再插上,Vold收到的uevent事件顺序偶发错乱,导致Disk对象重建时sysPath还没来得及更新,判断结果时对时错。排查方法是Vold和NetlinkHandler都打开调试日志,对比事件到达的先后顺序。
adb logcat -s NetlinkManager NetlinkHandler Vold定位到是事件乱序之后,有一个不算太脏的解决办法:在handleBlockEvent里不要直接用eventPath拼sysPath,而是在判断前先确认/sys/block/sda/device这个链接存在,如果暂时不存在,可以延后到下一轮或换一个策略(比如延迟20ms再解析)。这块逻辑不复杂,但需要你对热插拔的时序足够敏感,开发阶段建议多插拔几千次做压测。
问题四:SATA硬盘识别出来了,但上层应用还是把它当USB设备显示。
这个坑大概率出在Framework层。如果只改了Vold,上层StorageManagerService的DiskInfo还是按老逻辑来,或者应用层直接自己读设备节点名判断——那就是应用层没走DiskInfo.flags。遇到这类问题,先看应用代码里是怎么获取存储设备的,是否用了StorageManager.getDisks(),是否读取了flags字段。如果应用层是自己扫/sys/block,那你光改Vold(只影响系统framework的事件)对第三方应用不一定生效,因为它们不解析DiskInfo。
5.3 扩展思路:不止U盘和硬盘
这套“从sysfs路径识别设备拓扑”的思路,对RK3568上其他存储类型同样适用,原理是通用的。
例如,把eMMC、TF卡和U盘区分开。eMMC挂在platform的sdhci控制器下,TF卡也挂在sdhci下,但通常对应不同的控制器实例。比如eMMC的节点是mmcblk0,TF卡是mmcblk1。如果设备只有一个mmcblk节点,通过/sys/block/mmcblk0/device下面的路径,也能区分它到底挂在mmc0(eMMC)还是mmc1(SD/TF)。对Android来说,内置eMMC本来就是系统盘,一般不需要标记,但如果你要做特殊产品,比如“TF卡作为默认外部存储的卡片机”,这个判断就很有用了。
另外就是NVMe设备。RK3568通过PCIe可以接NVMe SSD,这种设备的节点是nvme0n1,sysfs路径里会出现pci和nvme字样。如果你想让NVMe和SATA硬盘一样被视为“内置硬盘”,判断条件里把nvme加进去就行:
static bool isSataOrNvmeDisk(const std::string& sysPath) { char buf[PATH_MAX]; std::string deviceLinkPath = sysPath + "/device"; ssize_t len = readlink(deviceLinkPath.c_str(), buf, sizeof(buf) - 1); if (len == -1) return false; buf[len] = '\0'; std::string deviceLink(buf); return deviceLink.find("sata") != std::string::npos || deviceLink.find("nvme") != std::string::npos || deviceLink.find("pci") != std::string::npos; }这个扩展思路在后续做NAS类产品时大概率能直接复用。底层把识别做好了,上层各个模块就不用各自折腾了。
我在实际调试中还发现一个规律:判断设备类型这件事,千万不要在应用层做,也不要散落在多个Java服务里做。因为每个地方判断一次,就多一份出错的风险,而且你还得考虑SELinux权限、跨进程读取sysfs的IO开销。放在Vold里,一锤定音,上层只是消费结果,这是最优解。