ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Vold与sysfs的RK3568 U盘/硬盘区分方案

基于Vold与sysfs的RK3568 U盘/硬盘区分方案 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下都叫sdXsda、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_USBSATA/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开头的才去判断usbmmcblk设备直接跳过减少无谓的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_USBSATA硬盘和NVMe硬盘都不会被标记天然区分。4.3 上层读取识别结果Vold设置了FLAG_USB之后Data会通过Binder接口同步到StorageManagerServiceStorageManagerService在创建Java层DiskInfo时会把这个flags原样带过去。应用层通过StorageManager.getDisks()拿到的每个DiskInfo对象都可以直接检查标志StorageManager sm context.getSystemService(StorageManager.class); ListDiskInfo 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用15Java里顺手写了16调试了一个下午。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的节点是mmcblk0TF卡是mmcblk1。如果设备只有一个mmcblk节点通过/sys/block/mmcblk0/device下面的路径也能区分它到底挂在mmc0eMMC还是mmc1SD/TF。对Android来说内置eMMC本来就是系统盘一般不需要标记但如果你要做特殊产品比如“TF卡作为默认外部存储的卡片机”这个判断就很有用了。另外就是NVMe设备。RK3568通过PCIe可以接NVMe SSD这种设备的节点是nvme0n1sysfs路径里会出现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里一锤定音上层只是消费结果这是最优解。
返回列表