这章我前后翻了三遍每次读都有新收获。Vold和Rild这两个名字凡是做过Android系统定制或者ROM开发的人都不会陌生一个管存储一个管电话看起来八竿子打不着但都属于“用户空间最接近内核硬件”的那一层守护进程。手机连上电脑不弹盘、插入耳机没反应、双卡信号断断续续这些看起来八竿子打不着的现场问题查到最后大概率都会撞上它们。如果你是做应用层开发的可能一辈子不用碰这两个模块但如果你想深入系统服务、搞定制ROM、做车机方案或者只是想真正弄懂“手机到底是怎么把一块U盘挂载到用户目录的”“拨号后那句AT指令是怎么发出去的”第8章就是绕不开的一课。这一章读起来不算轻松涉及C、内核事件、Binder通信、状态机、协议栈但好东西往往都埋在这种硬核内容里。下面我结合自己在实际项目里调试这两个模块的经验把这一章拆开揉碎讲一遍。我不打算逐行念书而是把里面最有价值的设计思想、实现细节和排障思路理出来最后再给你一套能落地的源码阅读方法。1. 这一章到底在讲什么两个守护进程的分量1.1 为什么说Vold和Rild是手机系统的“神经系统”两端先说个最直观的理解。手机功能千千万但拆到骨架上就两条线一条是数据如何存一条是信号如何通。Vold管理前者Rild管理后者。比如你把一张TF卡插进手机系统怎么知道这张卡出现了卡插进去之后内核的驱动层会检测到设备变化接着Vold拿到设备节点判断分区类型解析文件系统挂载到某个目录最后再通知上层“你的存储空间多了”。这一系列动作里负责调度的人就是VoldVolume Daemon卷守护进程。没有它插入U盘、外置SD卡、甚至内部userdata分区的挂载都会乱套。Rild则是无线电接口层守护进程Radio Interface Layer Daemon它夹在Android电话框架Telephony Framework和Modem基带处理器之间。上层App调用TelephonyManager拿到SIM卡的IMSI、发短信、打电话真正把这些请求翻译成Modem能听懂的AT指令或QMI消息的就是它。没有Rild手机界面上的信号满格图标都画不出来因为在它之上根本没数据可用。两者一个控制着“接通外部世界的数据入口”一个控制着“连接运营商网络的唯一通道”所以我说它们是手机底层两条最重要的“神经”。1.2 这一章的阅读路线建议《深入理解Android》第8章的结构按我的理解是分两条主线推进的一条走存储一条走电话但两条线在架构层面是并行对照的。第一次读这章的人最容易犯的错是“一步到位”。上来就想把Vold的整个挂载流程背下来结果看到VolumeManager、NetlinkManager、DirectVolume这些类就被绕晕了。我建议分三步走第一步先只记住两个角色关系谁的请求发给谁谁的事件上报给谁。Vold听内核的ueventRild听Modem的主动上报两者最终都向Framework报告状态。第二步再深入到每个进程内部的状态机搞清楚什么条件下从“挂载中”变成“已挂载”什么条件下从“注册网络”变成“漫游”。第三步才值得去抠具体代码路径和数据结构。我读这章时还结合了一个习惯把书里的类名和AOSP源码里实际的路径对上。比如Vold源码在system/vold/目录Rild源码在hardware/ril/目录。这样书读完了代码也能顺着目录继续啃下去不会读完一遍就还给作者。2. Vold存储世界的总调度2.1 Vold到底负责哪些事很多人以为Vold就是“挂载SD卡的”其实它的职权范围比这宽得多。从Android 4.x到Android 14Vold的职责经历过好几次扩张如今至少包括这么几件事内部存储分区userdata解锁与挂载外置存储卷识别、挂载、卸载、格式化用户空间加密FBE密钥管理磁盘加密FDE的启停存储卷状态变更通知通过Binder回调给MountService处理fstab文件里声明的各分区挂载需求提供Vold命令接口供Framework层调用vold、sm命令我遇到过很多人对Vold有一种误解觉得它是文件系统的一部分。其实Vold不负责具体读写文件它更像是一个调度员检查设备、创建挂载点、发起挂载、向上层汇报结果。真正解析ext4、f2fs、vfat这些格式的是内核文件系统驱动Vold只是“叫人家干活”的人。这个定位很重要因为只有理解了Vold的“调度”属性你才不会在看到它处理create、destroy、format这些命令时发懵。它调用的其实是fs_mgr和内核syscall自己并不实现文件系统逻辑。2.2 uevent监听内核与用户态之间的桥Vold最核心的输入源是内核发出的uevent事件。每次存储设备发生变化比如U盘插入、TF卡拔掉、分区重新扫描内核都会通过netlink套接字往用户态广播一条uevent。Vold里的NetlinkManager就是专门蹲守这个套接字的组件。具体过程可以这样理解内核检测到USB存储设备插入→发出ACTIONADD的uevent→NetlinkManager从netlink socket读出来→解析出DEVNAME如/dev/sda1、DEVTYPE如partition、MAJOR/MINOR等属性→交给VolumeManager决定要不要认领这个设备。这一步是整个Vold消息驱动的基石。没有它难道让Vold每分钟轮询一次设备目录吗那既不实时也浪费资源。所以Vold和很多底层守护进程一样采用了“事件驱动被动触发”的模型——这是系统开发里的常见套路比轮询优雅得多。我在实际开发里遇到过一个问题某定制设备插入U盘后系统没有弹窗但dmesg里内核明明认到了设备。排查到Vold日志才发现NetlinkManager根本没收到事件。后来查下来是SEAndroid的netlink权限策略没有配好Vold进程没有权限绑定到netlink套接字上。这种问题不看Vold日志根本定位不到所以读这一章时NetlinkManager这一节值得多看两眼。2.3 fstab配置的逐字段解读Vold启动后要干的第二件事是读/etc/fstab或vendor分区下的fstab.设备名把启动阶段需要挂载的分区清单拉出来。这里以常见的fstab条目为例/dev/block/by-name/system /system ext4 ro,wait,slotselect first_stage_mount /dev/block/by-name/userdata /data f2fs rw,latemount,wait,check,quota,resize,fileencryptionaes-256-xts:aes-256-cts first_stage_mount第一行挂载system分区带ro和wait意思是只读挂载并且要等设备节点出现第二行挂载userdata分区latemount表示不在第一阶段挂载等Vold起来后再挂载这正好对应了FBE加密的一个设计userdata先不急着挂等Vold准备好了密钥再挂才能看到解锁后的数据。fileencryptionaes-256-xts:aes-256-cts这一串是加密算法配置。前者是文件内容的加密算法后者是文件名加密的算法。如果你在系统启动日志里看到“FBE: enabling file encryption”之类的字样多半就是从这行配置读出来的。很多人忽略wait这个flag的作用。它决定了若设备节点迟迟不出现挂载要不要卡住。对于system分区wait通常就是等下去对于userdata不光是wait还有latemount配合因为早期启动阶段还不能挂载加密数据。读懂fstab里每个flag的语义是配置Vold行为的最基本功课这一章里应该都有提到我的建议是把书中列出的每个flag抄下来对照自己机器上的fstab文件逐个验证一遍。2.4 用户空间加密与挂载状态机Android 7.0之后的FBEFile Based Encryption是个大话题Vold在其中承担的角色也很明确为每个加密用户准备密钥、挂载加密分区、在用户解锁时把密钥注入密钥管理模块。FBE这玩意设计的核心思想是不同的用户User 0、User 10、User 11……拥有不同的密钥系统启动时只能挂载Device EncryptedDE部分用户输入锁屏密码后系统才能拿到Credential EncryptedCE部分的密钥进而挂载完整的用户数据目录。这个过程的脚本化描述是这样的Vold启动→读取/data/unencrypted或metadata分区里的加密信息→初始化KeyManagement→挂载userdata的DE容器→通知Framework“存储已就绪”→用户解锁时Vold接收命令注入CE密钥→挂载CE容器→上层应用看到全部数据。我调试过一个现象某设备关机重启后个别第三方应用的数据不见了。查来查去问题出在Vold的密钥注入和挂载顺序上。App还没等CE容器挂载完成就去读了/data/user/0/xxx读到一半目录还是空的。这就是“状态机时序”问题不是你App的问题而是系统底层的挂载时序比预期慢。所以每次系统启动慢、桌面图标迟迟不出来第一反应应该去查Vold的挂载进度而不是怪Launcher卡顿。2.5 Vold常见改造与排错思路做定制ROM的人Vold最常碰面的三个改造场景是修改默认挂载类型比如默认把USB大容量存储设为可移动设备还是内部共享存储新增分区或文件系统比如加入exfat支持让Vold识别Windows格式的TF卡修改加密策略比如从FDE切到FBE或反过来排错思路方面我最常用的命令是这几个adb shell vold --help adb shell sm list-volumes adb shell dumpsys mount adb logcat -s Volddumpsys mount能看到MountService记录的各个卷状态这是一个很好的“快照”入口。logcat -s Vold能看到Vold进程自己的日志如果事件没走到Binder服务那层基本日志在这里就能看见。还有一个隐藏技巧Vold有自己的命令行工具vdcVold Command可以手动触发挂载、卸载、格式化调试时非常有用。比如adb shell vdc volume list这条命令会列出当前Vold里所有卷及其状态比看一堆封装的Java日志直观得多。遇到“U盘认到了但不想自动挂载”的情况还能用vdc volume unmount手动处理。3. Rild连接Modem的那道闸门3.1 RIL的整体分层讲Rild之前先把整个RIL的层次摆出来不然后面聊流程容易迷路。由上到下是Telephony FrameworkJava层包含TelephonyManager、Phone、GsmCdmaPhone等RILJJava层的RIL实现RIL.java负责把上层请求序列化并通过Socket发给RildRildC进程rild负责分配请求、管理响应、调度主动上报Vendor RIL厂商实现库libril和libreference-ril等或HIDL下的android.hardware.radio服务Modem基带芯片用AT指令、QMI、MBIM等方式通信Rild处在中间像一个“翻译官调度员”。它接收RILJ发来的包裹拆开看是哪个请求转交给Vendor层执行Vendor层拿到Modem的应答后Rild再把结果装回给RILJ。可能有人会问为什么不让Java层直接和Modem通信答案是稳定性和系统解耦。Modem的接口各家各不相同有的走串口有的走USB有的走共享内存如果让Framework直接面对这些差异上层就没法稳定了。Rild这层把硬件差异屏蔽掉Framework只面对一个统一的接口模型。3.2 从拨号到接通Rild处理请求的完整脉络我拿拨号来串一下整个RIL流程这样比干讲API更直观。你在拨号盘点下“呼叫”后事情是这样传的TelephonyManager发起通话请求→Phone对象解析出对应的请求码如RIL_REQUEST_DIAL→RILJ把这个请求塞到一个Message对象里并且带上一个递增的Token作为唯一标识→RILJ通过socket把请求序列化后发给Rild进程→Rild里的RIL_register注册的请求处理器查到对应功能的执行函数→Vendor层把命令翻译成具体AT指令发给Modem比如ATD10086;→Modem返回OK或ERROR→Rild把结果映射回RIL_REQUEST_DIAL的响应结构→通过socket回传给RILJ→上层通过回调拿到结果更新通话状态。这一段链路里最值得理解的是Token机制。为什么要带Token因为Rild和RILJ之间是异步的不是你发一条就等一条的。上层可能同时发出拨号、查询信号、读取SIM卡信息三个请求三条请求都堆在socket里响应返回的时间又各不相同。响应里必须带TokenRILJ拿到后才能知道“这条响应对应的是之前的哪一个请求”。这类似于HTTP请求里的RequestId是异步通信的老套路了。想抓这段日志的话可以用adb logcat -b radio这里的radio缓冲区是专门给RIL相关日志用的能看到RILJ发出什么请求、Rild返回什么响应。排查“拨号无反应”这类问题时-b radio是第一步。3.3 来电与信号上报主动事件的流转路径Rild不只是被动响应请求它还得主动向上报消息比如来电、信号强度变化、网络注册状态变化。这些不等上层问、自己先说的事件在RIL里叫URCUnsolicited Result Code。典型场景是来电Modem检测到来电信号→Vendor层解析出类似RIL_UNSOL_CALL_STATE_CHANGED的主动上报消息→Rild收到后判定这是主动事件不走请求-响应匹配逻辑而是直接在socket上发RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED给RILJ→RILJ解析出事件类型回调到PhoneNotifier→Framework把来电状态广播到系统UI锁屏上就弹出接听界面了。这个“主动上报”模型对系统稳定性至关重要。如果来电也要上层轮询Modem才知道那电话灵敏度会差到没法用而且Modem资源也会被浪费。所以Rild的设计天然就是“被请求时响应有事件时通知”的双通道模式。我调试过一个灵异问题手机有信号但别人打不进来。抓-b radio日志发现Modem早把CALL_STATE_CHANGED发出来了Rild也收到了但RILJ上报到Framework那层就断了。后来排查发现是某个系统服务把TelephonyRegistry的Binder回调卡住了。这类问题根本不在于Rild而在于上层链路所以每次在Rild日志里看到URC正常到达就该把矛头转向Framework内部。3.4 双卡与SIM热插拔中的状态处理现在的手机大多双卡双待这对Rild的架构提出了一个很大的挑战双卡是跑两个Rild进程还是一个答案是两种方案在Android历史中都存在过。早期双卡方案会起两个Rild进程各管一张卡上面再套一个多卡代理后来演进到单Rild进程内双栈也就是一个Rild管理多个Radio实例。AOSP里用RIL_Init传入instance ID来区分卡槽每一路都有独立的Socket、独立的请求队列和独立的事件回调。我看第8章的时候觉得作者在SIM热插拔这块讲得尤其值得琢磨。SIM卡被拔出时Modem会上报SIM状态变化Rild会把RIL_UNSOL_SIM_REFRESH或RIL_UNSOL_RESPONSE_SIM_STATUS_CHANGED发给上层。Framework接到后会经历“SIM移除→注销网络→清空联系人缓存→界面提示无卡”等一系列状态变化。这个过程中最怕的是状态回不到SIM_STATE_READY。我在某双卡方案项目上遇到过卡槽2插入新卡后系统一直显示“正在检查SIM卡…”底层的getSimStatus链路上Rild返回的状态机一直停在RIL_SIM_ABSENT。后来定位到是Vendor RIL的缓存数据没刷新新卡的ICCID没读上来。这类问题往往要同时抓-b radio和-b telephony两个日志缓冲区对照才能看出状态机断在哪一环。双卡的UID唯一标识管理也是重点。每张卡对应一个Subscription IDFramework通过它区分卡槽。Rild在初始化时通过RIL_REQUEST_GET_SIM_STATUS拿到卡状态再配合RIL_REQUEST_SET_MUTE之类的设置才能做到不同卡槽独立收发。3.5 Rild的调试与常见“老大难”问题Rild的调试工具和方式跟Vold不太一样因为Rild通信链路长横跨Java、C和Modem每一层都可能有坑。最常用的排查链路是adb logcat -b radio -v time adb logcat -b telephony atrace --atrace-categoriesradio-b radio看RILJ和Rild之间的消息流-b telephony看Framework电话状态的流转。两段日志一对基本能判断问题在上层调度还是下层通信。这里提几个我遇到过的“老大难”问题第一个是IMEI消失。Modem上报正常但*#06#看不到IMEI。查Rild日志发现RIL_REQUEST_DEVICE_IDENTITY返回了空字段。这类问题多半在Vendor RILRild只是中间搬运它不产生IMEI数据遇到空数据也只能原样返回。第二个是信号满格但无法注册网络。Rild反复上报NETWORK_REG_STATE_SEARCHINGModem那边信号强度很高却一直搜不到网。这一般不是Rild代码逻辑问题而是Modem的NV非易失性配置错乱或运营商参数配置不符需要写入正确的NV项才能恢复。第三个是打电话有声音但对方听不到或对方听得到但你听不到。这类问题很容易被怀疑是音频路由其实和Rild也有关系。Rild在上层语音通话建立时会根据RIL_REQUEST_SET_SUPP_SVC之类的指令把音频通路切到正确的设备上。如果Rild的音频路由逻辑有问题传出去的语音数据就可能在错误通路上变成“单边静音”。Rild调试的一个基本心得是抓日志时必须带上具体时间点并且最好在问题复现前就启动抓取。因为是长链路通信事后回溯日志定位效率会高很多。4. 两个进程的共性设计哲学4.1 独立守护进程 Binder暴露能力Vold和Rild虽然功能不同但在架构设计上有着惊人的一致性它们都是独立的守护进程跑在用户态不从属于任何Android应用也不直接挂在某个系统服务进程里。为什么这么设计我理解有两点考虑。一是稳定隔离。Vold崩溃了不能让整个system_server跟着挂掉Rild被Modem的数据冲暴了也不能让整个系统服务一起遭殃。把它们独立成进程出问题后可以由init重新拉起系统核心服务还能继续跑这是Android底层设计的一个基本取舍。二是权限隔离。Vold要直接操作设备节点、挂载文件系统这些操作涉及非常大的权限。Rild需要访问串口、USB设备同样涉及硬件权限。如果这些权限落在system_server进程里风险面太大了。独立守护进程配合SEAndroid策略可以精确限制每个进程能碰什么设备做到最小权限。对外暴露能力方面Vold通过Binder接口IVolumeManager向MountService提供命令Rild则不是走Binder而是走Socketrildsocket和上层通信。这里有个历史原因RIL是原电话栈时代继承下来的架构时间长了就一直保留Socket方案。第8章如果能把这两者的“通信方式差异”和“为什么差异”读明白底层架构的理解会深一个层次。4.2 事件驱动的三条腿轮询、监听、回调无论是Vold还是Rild它们的运行模型本质上都是事件驱动。它们不会没事干就扫描一遍设备列表或轮询Modem状态而是靠三种机制协同工作监听内核事件Vold的netlink uevent监听硬件上报Rild从Vendor层拿Modem的异步通知接收上层命令Vold接收Binder调用/命令Rild接收Socket请求这三种机制的共同点是“等别人找上门”而不是“主动去问”。这跟应用层开发完全不同。应用层为了保证UI刷新可能每16毫秒画一帧底层守护进程则是“闲时挂起、忙时忙碌”基本不占用CPU。这一点在阅读第8章时不需要刻意去记什么代码只要在做系统性能分析时能想到这两个进程的CPU占用高不是活跃事件出了问题就是某个循环卡死了。我用systrace看系统性能时观察Vold和Rild的线程状态通常它们应该长期处于Sleep状态。如果看到它们的某个线程一直在Running尤其是不带sleep的忙等循环基本就是代码逻辑出了问题得去看具体在跑什么。4.3 状态机系统底层的稳定器Vold里有卷状态如VolumeState、挂载状态Mounted/UnmountedRild里有网络状态NET_REG_STATE_HOME/ROAMING/SEARCHING、SIM状态SIM_STATE_READY/ABSENT/PIN_REQUIRED。这些状态转换往往都画成了状态图第8章应该也花了不小篇幅。状态机的意义在于让系统在复杂事件面前保持稳定。试想一个U盘挂载到一半被拔掉Vold该怎么处理如果没有状态机上层可能还以为存储可用App写入数据时直接失败。有了状态机Vold能明确知道当前处于“挂载中”拔掉设备后会把状态从MOUNTING转到UNMOUNTING或IDLE再通知上层“设备取消了”。整个过程是可控的、可追踪的。对Rild也是一样。SIM卡热插拔、飞行模式切换、网络搜索这些操作如果能用一个状态机规范地约束就能避免“卡在中间态”的问题。我排查过很多“飞行模式关不彻底”“SIM卡状态不刷新”的案件最后原因都是某个状态迁移分支没有处理好导致底层和上层感知不一致。所以看第8章时与其逐行扣代码不如把状态机画一遍、讲一遍。状态图看通了比记住几十个函数名有用得多。5. 结合源码阅读的实操建议5.1 如何高效阅读这一章很多人买了这本书读到第8章就没耐心了因为太多类、太多方法。我推荐的阅读方法不是从头看到尾而是“场景驱动式阅读”。选一个你熟悉的使用场景比如“插入U盘”然后带着问题去找答案U盘插上后第一个感知到的人是谁内核怎么通知VoldVold怎么确定这个分区要不要挂挂到哪里通知上层时带了什么数据带着这一串问题去书里找对应章节你会发现每个问题都能落到具体的类和方法上。这比单纯从头翻到尾效率高太多而且印象更深。读Rild那部分就用“拨一个电话”来串从拨号键按下去到锁屏显示“正在呼叫”中间每一段消息的流向、每一个关键类的职责都能串成一个完整的故事。5.2 推荐的工具链与抓取日志技巧源码阅读之外我强烈建议配合真机来学。纸上得来终觉浅Vold和Rild这种模块光看书记不住得实际跑过才记得牢。工欲善其事必先利其器。我一般准备这么几类工具日志抓取adb logcat -b all是加大分量的方式但真机调试时建议有针对性地抓Vold、MountService、RILJ、RILC这几个tag系统状态查看adb shell dumpsys mount、adb shell dumpsys telephony.registry分别是Vold和Rild体系里非常关键的快照命令内核事件观察adb shell cat /proc/kmsg结合dmesg能看到netlink uevent的前身源码交叉搜索读AOSP时grep -rn class Vold或者grep -rn RIL_REQUEST_DIAL能找到关键类再配合书里的类图事半功倍抓日志这件事有几个细节第一时间戳必须带。Rild和Vold的问题很多时候和“先后顺序”强相关不带时间戳根本没法判断谁先谁后。用adb logcat -v threadtime能打印出时间和线程ID。第二日志过滤要精准。不加过滤的日志量非常庞大真机跑一会儿就几百MB。我开发者选项里通常只保留必要buffer再配合-s参数过滤关键tag。第三底层事件的线索别只盯着Android日志。dmesg里藏了很多内核底层的设备变化信息比如U盘插拔时内核有没有认到设备节点、SCSI层有没有报错。Vold的日志能告诉你“Vold处理得对不对”dmesg能告诉你“内核到底遇到什么情况”。还有一个小技巧修改源码后要验证Vold或Rild改动是否生效可以先adb root再adb remount然后手动重启对应进程adb shell stop adb shell start这等价于软重启framework和守护进程比整个机器重启省时间。但如果改了fstab或SEAndroid策略就必须完整重启因为这些是在早期启动阶段生效的软重启不起作用。最后分享一个我在实际项目中反复用到的排障顺序先看内核有没有事件dmesg再看守护进程有没有处理logcat -s Vold/RILC最后看Framework状态有没有更新dumpsys。按照这个顺序排查90%的“底层不生效”类问题都能在两三轮内定位到根因。Vold和Rild虽然分工不同但这条排查思路是通用的。第8章读完了如果你能把这两个进程各自的一条完整事件链路讲给别人听并且能说出状态机的几个关键状态那就算真的读进去了。剩下的就是带着源码和日志到真机上折腾踩几回坑自然就能把书里的内容变成自己的手感和直觉。