2、Vold与存储卷管理:Vold守护进程架构、磁盘事件监听与处理、卷状态机与挂载流程

2.1 Vold 守护进程:存储世界的“交通警察”

说到 Android 的存储管理,Vold(Volume Daemon)绝对是绕不开的核心。我刚开始接触 AOSP 时,总觉得它是个黑盒子——插个 U 盘,系统就自动弹出来了,背后到底发生了什么?

说白了,Vold 就是存储卷的守护进程。它负责监听硬件事件、管理卷的状态、执行挂载与卸载。你可以把它想象成存储世界的“交通警察”——谁来了、该走哪条路、什么时候该停,都由它说了算。

Vold 的架构其实不复杂,核心就三个部分:

  • 事件监听层:通过 Netlink 机制监听内核发出的 uevent。比如你插个 OTG 盘,内核会发一个“我来了”的消息。
  • 卷管理层:维护一个卷状态机。每个存储设备从“出现”到“可用”,要经历好几个状态转换。
  • 挂载执行层:真正执行 mount 命令、创建挂载点、设置权限等操作。

我记得第一次看 Vold 源码时,发现它其实是个单线程的事件驱动模型。嗯,这里要注意——虽然是单线程,但通过异步 I/O 和回调机制,它处理并发事件的能力并不差。

2.2 磁盘事件监听:Netlink 与 uevent

Vold 怎么知道用户插了个 U 盘?答案就是 Netlink。

Netlink 是 Linux 内核与用户空间通信的一种机制。Vold 会创建一个 Netlink 套接字,监听 KOBJECT_UEVENT 消息。每当内核检测到设备变化(插入、移除、分区变更),就会广播一个 uevent。

一个典型的 uevent 长这样:

ACTION=add
DEVPATH=/devices/platform/soc/xxx/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda
SUBSYSTEM=block
DEVTYPE=disk
MAJOR=8
MINOR=0
DEVNAME=sda
DEVTYPE=disk
SEQNUM=12345

Vold 收到后,会解析这些字段。关键信息是 ACTION(add/remove/change)和 DEVPATH(设备路径)。

我个人习惯在调试时,直接 adb shell cat /proc/kmsg | grep uevent 看原始事件流。这比看日志更直接,能帮你快速定位问题。

小技巧: 如果你发现 U 盘插上后没反应,先别急着怀疑 Vold。用 adb shell udevadm monitor --kernel --property 看看内核有没有发出 uevent。有时候是硬件驱动层就没上报。

2.3 卷状态机:从“未知”到“已挂载”

每个存储卷在 Vold 内部都有一个状态机。我把它总结为五个核心状态:

状态 含义 触发条件
Unknown 初始状态,卷刚被发现 收到 add uevent
Checking 正在检查文件系统完整性 Vold 发起 fsck 检查
Mounting 正在执行挂载操作 检查通过后自动进入
Mounted 挂载成功,用户可访问 mount 命令执行成功
Unmounting 正在卸载 用户弹出或系统休眠

你想想看,如果状态机设计得不好,会出现什么情况?比如 U 盘还在检查文件系统,用户就急着去访问——那肯定报错。所以 Vold 在挂载前,一定会先走一遍 Checking 状态。

我曾经遇到过一个坑:某款劣质 U 盘,文件系统有坏块,fsck 检查卡住了。结果 Vold 一直卡在 Checking 状态,用户等半天没反应。后来我加了个超时机制,超过 30 秒就强制跳过检查,直接挂载为只读。虽然不完美,但至少用户能用了。

注意: 不要在生产环境中跳过 fsck 检查。文件系统损坏可能导致数据丢失。我那个方案只是临时应急,后续还是要引导用户格式化。

2.4 挂载流程:从 uevent 到 /mnt/media_rw

好了,理论说完了,咱们走一遍完整的挂载流程。我以插入一个 FAT32 格式的 OTG U 盘为例:

  1. 内核上报 uevent:USB 驱动检测到设备插入,创建 /dev/sda 块设备,并广播 add 事件。
  2. Vold 接收并解析:Netlink 监听线程收到消息,解析出 ACTION=addDEVTYPE=disk
  3. 创建 Volume 对象:Vold 根据设备路径,创建一个 VolumeBase 子类实例(比如 DiskVolume)。
  4. 状态迁移:Unknown → Checking:Vold 调用 fsck_msdos 检查文件系统。如果没问题,进入下一步。
  5. 状态迁移:Checking → Mounting:Vold 创建挂载点,比如 /mnt/media_rw/XXXX-XXXX
  6. 执行 mount:调用 mount("/dev/block/sda", mount_point, "vfat", flags, "utf8")
  7. 状态迁移:Mounting → Mounted:挂载成功,Vold 通知 Framework(通过 IMountServiceListener)。
  8. Framework 更新 UI:状态栏出现 U 盘图标,文件管理器显示新卷。

代码层面,核心逻辑在 VolumeBase.cppmount() 方法里:

status_t VolumeBase::mount() {
    // 1. 检查状态
    if (mState != State::kChecking) {
        SLOGW("Unexpected state %d", mState);
        return -EINVAL;
    }

    // 2. 创建挂载点
    std::string mountPoint = getMountPoint();
    if (mkdir(mountPoint.c_str(), 0755) != 0 && errno != EEXIST) {
        SLOGE("Failed to create mount point %s: %s", mountPoint.c_str(), strerror(errno));
        return -errno;
    }

    // 3. 执行挂载
    if (doMount(mountPoint) != OK) {
        rmdir(mountPoint.c_str());
        return -EIO;
    }

    // 4. 更新状态
    setState(State::kMounted);
    return OK;
}

这里有个细节:doMount() 是虚函数,不同文件系统(vfat、exfat、ntfs)有不同的实现。Vold 通过 blkid 工具检测文件系统类型,然后调用对应的挂载器。

核心要点: 整个挂载流程是同步的,但 Vold 通过工作队列(WorkQueue)来避免阻塞主线程。每个挂载请求会投递到工作线程中执行,主线程继续监听新事件。

2.5 避坑指南:我踩过的那些雷

做存储适配这些年,我总结了几条血泪教训:

  • 不要假设设备节点固定/dev/sda 可能变成 /dev/sdb,取决于插入顺序。一定要用 by-idby-path 软链接来识别设备。
  • 处理热插拔竞态:用户可能快速插拔,导致 Vold 同时收到 add 和 remove 事件。我建议在状态机里加一个“冷却期”,比如 500ms 内不处理同一设备的重复事件。
  • 注意 SELinux 策略:Vold 需要 mount 权限,但 SELinux 可能会拦截。如果挂载失败,先检查 avc: denied 日志。
  • 文件系统兼容性:exFAT 和 NTFS 需要内核模块支持。我遇到过某款平板内核没编译 exFAT 驱动,插上 NTFS 盘直接不识别。

嗯,最后说一句:Vold 的代码其实不算难,但调试起来很麻烦。因为它涉及内核、驱动、用户空间三层交互。我建议你在开发时,多开几个 adb 窗口:一个看 logcat(过滤 vold 标签),一个看内核日志,一个随时执行 mountlsblk 命令。三管齐下,问题定位会快很多。