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 看原始事件流。这比看日志更直接,能帮你快速定位问题。
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 秒就强制跳过检查,直接挂载为只读。虽然不完美,但至少用户能用了。
2.4 挂载流程:从 uevent 到 /mnt/media_rw
好了,理论说完了,咱们走一遍完整的挂载流程。我以插入一个 FAT32 格式的 OTG U 盘为例:
- 内核上报 uevent:USB 驱动检测到设备插入,创建
/dev/sda块设备,并广播 add 事件。 - Vold 接收并解析:Netlink 监听线程收到消息,解析出
ACTION=add、DEVTYPE=disk。 - 创建 Volume 对象:Vold 根据设备路径,创建一个
VolumeBase子类实例(比如DiskVolume)。 - 状态迁移:Unknown → Checking:Vold 调用
fsck_msdos检查文件系统。如果没问题,进入下一步。 - 状态迁移:Checking → Mounting:Vold 创建挂载点,比如
/mnt/media_rw/XXXX-XXXX。 - 执行 mount:调用
mount("/dev/block/sda", mount_point, "vfat", flags, "utf8")。 - 状态迁移:Mounting → Mounted:挂载成功,Vold 通知 Framework(通过
IMountServiceListener)。 - Framework 更新 UI:状态栏出现 U 盘图标,文件管理器显示新卷。
代码层面,核心逻辑在 VolumeBase.cpp 的 mount() 方法里:
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 工具检测文件系统类型,然后调用对应的挂载器。
2.5 避坑指南:我踩过的那些雷
做存储适配这些年,我总结了几条血泪教训:
- 不要假设设备节点固定:
/dev/sda可能变成/dev/sdb,取决于插入顺序。一定要用by-id或by-path软链接来识别设备。 - 处理热插拔竞态:用户可能快速插拔,导致 Vold 同时收到 add 和 remove 事件。我建议在状态机里加一个“冷却期”,比如 500ms 内不处理同一设备的重复事件。
- 注意 SELinux 策略:Vold 需要
mount权限,但 SELinux 可能会拦截。如果挂载失败,先检查avc: denied日志。 - 文件系统兼容性:exFAT 和 NTFS 需要内核模块支持。我遇到过某款平板内核没编译 exFAT 驱动,插上 NTFS 盘直接不识别。
嗯,最后说一句:Vold 的代码其实不算难,但调试起来很麻烦。因为它涉及内核、驱动、用户空间三层交互。我建议你在开发时,多开几个 adb 窗口:一个看 logcat(过滤 vold 标签),一个看内核日志,一个随时执行 mount 和 lsblk 命令。三管齐下,问题定位会快很多。