9.2.6 Vold实例分析

最后更新于:2022-04-02 05:53:24

这一节将分析一个实际案例,即插入一张SD卡引发的事件及其处理过程。在分析之前,还是应先介绍MountService。 1. MountService的介绍 有些应用程序需要检测外部存储卡的插入/拔出事件,这些事件是由MountService通过Intent广播发出的,例如外部存储卡插入后,MountService就会发送ACTION_MEDIA_MOUNTED消息。从某种意义上说,可把MountService看成是Java世界的Vold。来简单认识一下这个MountService,它的代码如下所示: **MountService.java** ~~~ class MountService extends IMountService.Stub implements INativeDaemonConnectorCallbacks { //MountService实现了INativeDaemonConnectorCallbacks接口 ...... public MountService(Context context) { mContext = context; ...... //创建一个HandlerThread,在第5章中曾介绍过。 mHandlerThread = new HandlerThread("MountService"); mHandlerThread.start(); /* 创建一个Handler,这个Handler使用HandlerThread的Looper,也就是说,派发给该Handler 的消息将在另外一个线程中处理。可回顾第5章的内容,以加深印象。 */ mHandler = new MountServiceHandler(mHandlerThread.getLooper()); ...... /* NativeDaemonConnector用于Socket通信,第二个参数“vold”表示将和Vold通信,也就是 和CL模块中的那个socket建立通信连接。第一个参数为INativeDaemonConnectorCallbacks 接口。它提供两个回调函数: onDaemonConnected:当NativeDaemonConnector连接上Vold后回调。 onEvent:当NativeDaemonConnector收到来自Vold的数据后回调。 */ mConnector = new NativeDaemonConnector(this, "vold", 10, "VoldConnector"); mReady= false; //再启动一个线程用于和Vold通信。 Threadthread = new Thread(mConnector, NativeDaemonConnector.class.getName()); thread.start(); } ...... } ~~~ MountService通过NativeDaemonConnector和Vold的CL模块建立通信连接,这部分内容比较简单,读者可自行研究。下面来分析SD卡插入后所引发的一连串处理。 2. 设备插入事件的处理 (1)Vold处理Uevent事件 在插入SD卡后,Vold的NM模块接收到Uevent消息,假设此消息的内容是前面介绍Uevent知识时使用的add消息,它的内容如下所示: **SD卡插入的Uevent消息** ~~~ add@/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0 ACTION=add //add表示设备插入动作,另外还有remove和change等动作 //DEVPATH表示该设备位于/sys目录中的设备路径 DEVPATH=/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0 /* SUBSYSTEM表示该设备属于哪一类设备,block为块设备,磁盘也属于这一类设备,另外还有 character(字符)设备等类型。 */ SUBSYSTEM=block MAJOR=179//MAJOR和MINOR分别表示该设备的主次设备号,二者联合起来可以标识一个设备 MINOR=0 DEVNAME=mmcblk0 DEVTYPE=disk//设备Type为disk NPARTS=3 //表示该SD卡上的分区,我的SD卡上有三块分区 SEQNUM=1357//序号 ~~~ 根据前文分析可知,NM模块中的NetlinkHandler会处理此消息,请回顾一下相关代码: **NetlinkHandler.cpp** ~~~ void NetlinkHandler::onEvent(NetlinkEvent *evt){ VolumeManager *vm = VolumeManager::Instance(); constchar *subsys = evt->getSubsystem(); ...... //根据上面Uevent消息的内容可知,它的subsystem对应为block,所以我们会走下面这个if分支 if (!strcmp(subsys, "block")) { vm->handleBlockEvent(evt); //调用VM的handleBlockEvent } ...... } ~~~ **VolumeManager.cpp** ~~~ voidVolumeManager::handleBlockEvent(NetlinkEvent *evt) { constchar *devpath = evt->findParam("DEVPATH"); VolumeCollection::iterator it; boolhit = false; for(it = mVolumes->begin(); it != mVolumes->end(); ++it) { //调用各个Volume的handleBlockEvent if(!(*it)->handleBlockEvent(evt)) { hit = true; break; } } ...... } ~~~ 我的G7手机只有一个Volume,其实际类型就是之前介绍过的DirectVolume。请看它是怎么对待这个Uevent消息的,代码如下所示: **DirectVolume.cpp** ~~~ int DirectVolume::handleBlockEvent(NetlinkEvent*evt) { constchar *dp = evt->findParam("DEVPATH"); PathCollection::iterator it; for(it = mPaths->begin(); it != mPaths->end(); ++it) { if(!strncmp(dp, *it, strlen(*it))) { int action = evt->getAction(); const char *devtype =evt->findParam("DEVTYPE"); if (action == NetlinkEvent::NlActionAdd) { int major = atoi(evt->findParam("MAJOR")); int minor = atoi(evt->findParam("MINOR")); char nodepath[255]; snprintf(nodepath, sizeof(nodepath),"/dev/block/vold/%d:%d", major, minor); //内部调用mknod函数创建设备节点 if (createDeviceNode(nodepath, major, minor)) { SLOGE("Error makingdevice node '%s' (%s)", nodepath, strerror(errno)); } if (!strcmp(devtype, "disk")) { //对应Uevent消息的DEVTYPE值为disk,所以走这个分支 handleDiskAdded(dp, evt); } else { //处理DEVTYPE为Partition的情况 handlePartitionAdded(dp,evt); } } else if (action == NetlinkEvent::NlActionRemove) { //对应Uevent的ACTION值为remove ...... } else if (action == NetlinkEvent::NlActionChange) { //对应Uevent的ACTION值为change ...... } ...... return 0; } } errno= ENODEV; return-1; } ~~~ 插入SD卡,首先收到的Uevent消息中DEVTYPE的值是“disk”,这将导致DirectVolume的handleDiskInserted被调用。下面来看它的工作。 **DirectVolume.cpp** ~~~ void DirectVolume::handleDiskAdded(const char*devpath, NetlinkEvent *evt) { mDiskMajor = atoi(evt->findParam("MAJOR")); mDiskMinor = atoi(evt->findParam("MINOR")); constchar *tmp = evt->findParam("NPARTS"); if(tmp) { mDiskNumParts = atoi(tmp);//这个disk上的分区个数。 } else{ ...... mDiskNumParts = 1; } charmsg[255]; intpartmask = 0; inti; /* Partmask会记录这个Disk上分区加载的情况,前面曾介绍过,如果一个Disk有多个分区, 它后续则会收到多个分区的Uevent消息。 */ for (i= 1; i <= mDiskNumParts; i++) { partmask |= (1 << i); } mPendingPartMap = partmask; if(mDiskNumParts == 0) { ......//如果没有分区,则设置Volume的状态为Idle。 setState(Volume::State_Idle); }else { ......//如果还有分区未加载,则设置Volume状态为Pending setState(Volume::State_Pending); } /* 设置通知内容,snprintf调用完毕后msg的值为: “Volume sdcard/mnt/sdcard disk inserted (179:0)” */ snprintf(msg, sizeof(msg), "Volume %s %s disk inserted(%d:%d)", getLabel(), getMountpoint(), mDiskMajor, mDiskMinor); /* getBroadcaster函数返回setBroadcaster函数设置的那个Broadcaster,也就是CL对象。 然后调用CL对象的sendBroadcast给MountService发送消息,注意它的第一个参数是ResponseCode::VolumeDiskInserted。 */ mVm->getBroadcaster()->sendBroadcast(ResponseCode::VolumeDiskInserted, msg, false); } ~~~ handleDiskAdded把Uevent消息做一些转换后发送给了MountService,实际上可认为CL模块和MountService通信使用的是另外一套协议。那么,MountService会做什么处理呢? (2)MountService的处理 **MountService.javaonEvent函数** ~~~ publicboolean onEvent(int code, String raw, String[] cooked) { Intent in = null; //关于onEvent函数,MountService的介绍中曾提过,当NativeDaemonConnector收到 //来自vold的数据后都会调用这个onEvent函数。 ...... if(code == VoldResponseCode.VolumeStateChange) { ...... }else if (code == VoldResponseCode.ShareAvailabilityChange) { ...... }else if ((code == VoldResponseCode.VolumeDiskInserted) || (code ==VoldResponseCode.VolumeDiskRemoved) || (code ==VoldResponseCode.VolumeBadRemoval)) { final String label = cooked[2]; //label值为”sdcard” final String path = cooked[3]; //path值为”/mnt/sdcard” int major = -1; int minor = -1; try { String devComp = cooked[6].substring(1, cooked[6].length() -1); String[] devTok = devComp.split(":"); major = Integer.parseInt(devTok[0]); minor = Integer.parseInt(devTok[1]); } catch (Exception ex) { ...... } if (code == VoldResponseCode.VolumeDiskInserted) { //收到handleDiskAdded发送的VolumeDiskInserted消息了 //然后单独启动一个线程来处理这个消息。 new Thread() { public void run() { try { int rc; //调用doMountVolume处理。 if ((rc =doMountVolume(path)) != StorageResultCode.OperationSucceeded) { } } catch (Exception ex){ ...... } } }.start(); } ~~~ doMountVolume函数的代码如下所示: **MountService.java** ~~~ private int doMountVolume(String path) { int rc = StorageResultCode.OperationSucceeded; try { //通过NativeDaemonConnector给Vold发送请求,请求内容为: //volume mount /mnt/sdcard mConnector.doCommand(String.format("volume mount %s", path)); }catch (NativeDaemonConnectorException e) { ......//异常处理 } ~~~ 走了一大圈,最后又回到Vold了。CL模块将收到这个来自MountService的请求,请求的内容为字符串“volume mount/mnt/sdcard”,其中的volume表示命令的名字,CL会根据这个名字找到VolumeCmd对象,并交给它处理这个命令。 (3)Vold处理MountService的命令 Vold处理MountService命令的代码如下所示: **CommandListener.cppVolumeCmd类** ~~~ intCommandListener::VolumeCmd::runCommand(SocketClient *cli, int argc, char **argv) { ...... VolumeManager *vm = VolumeManager::Instance(); int rc= 0; if(!strcmp(argv[1], "list")) { return vm->listVolumes(cli); } elseif (!strcmp(argv[1], "debug")) { ...... } elseif (!strcmp(argv[1], "mount")) { ......//调用VM模块的mountVolume来处理mount命令,参数是“/mnt/sdcard” rc= vm->mountVolume(argv[2]); } elseif (!strcmp(argv[1], "unmount")) { ...... rc= vm->unmountVolume(argv[2], force); } elseif (!strcmp(argv[1], "format")) { ...... rc = vm->formatVolume(argv[2]); } elseif (!strcmp(argv[1], "share")) { ...... rc= vm->shareVolume(argv[2], argv[3]); } elseif (!strcmp(argv[1], "unshare")) { ...... rc= vm->unshareVolume(argv[2], argv[3]); } ...... if(!rc) { //发送处理结果给MountService cli->sendMsg(ResponseCode::CommandOkay, "volume operationsucceeded", false); } ...... return0; } ~~~ 看mountVolume函数: **VolumeManager.cpp** ~~~ int VolumeManager::mountVolume(const char*label) { /* 根据label找到对应的Volume。label这个参数的名字含义上有些歧义,根据loopupVolume 的实现来看,它其实比较的是Volume的挂载路径,也就是vold.fstab中指定的那个 /mnt/sdcard。而vold.fstab中指定的label叫sdcard。 */ Volume*v = lookupVolume(label); ...... returnv->mountVol();//mountVol由Volume类实现。 } ~~~ 找到对应的DirectVolume后,也就找到了代表真实存储卡的对象。它是如何处理这个命令的呢?代码如下所示: **Volume.cpp** ~~~ int Volume::mountVol() { dev_tdeviceNodes[4]; int n,i, rc = 0; charerrmsg[255]; ...... //getMountpoint返回挂载路径,即/mnt/sdcard //isMountpointMounted判断这个路径是不是已经被mount了 if(isMountpointMounted(getMountpoint())) { setState(Volume::State_Mounted);//设置状态为State_Mounted return 0;//如果已经被mount了,则直接返回 } n =getDeviceNodes((dev_t *) &deviceNodes, 4); ...... for (i= 0; i < n; i++) { char devicePath[255]; sprintf(devicePath, "/dev/block/vold/%d:%d",MAJOR(deviceNodes[i]), MINOR(deviceNodes[i])); ...... errno = 0; setState(Volume::State_Checking); //默认SD卡为FAT分区,只有这样,当加载为磁盘的时候才能被Windows识别。 if(Fat::check(devicePath)) { ...... return -1; } /* 先把设备mount到/mnt/secure/staging,这样/mnt/secure/staging下的内容 就是该设备的存储内容了 */ errno = 0; if(Fat::doMount(devicePath, "/mnt/secure/staging", false, false, 1000, 1015, 0702,true)) { ...... continue; } /* 下面这个函数会把存储卡中的autorun.inf文件找出来并删掉,这个文件就是“臭名昭著”的 自动运行文件,在Windows系统上,把SD卡挂载为磁盘后,双击这个磁盘就会自动运行 这个文件,很多病毒和木马都是通过它传播的。为了安全起见,要把这个文件删掉。 */ protectFromAutorunStupidity(); //①下面这个函数比较有意思,需要看看: if(createBindMounts()) { ...... return -1; } //将存储卡mount路径从/mnt/secure/staging移到/mnt/sdcard if(doMoveMount("/mnt/secure/staging", getMountpoint(), false)) { ...... return -1; } //②设置状态为State_Mounted,这个函数将发送状态信息给MountService setState(Volume::State_Mounted); mCurrentlyMountedKdev = deviceNodes[i]; return 0; } ...... setState(Volume::State_Idle); return-1; } ~~~ 上面代码中有个比较有意思的函数,就是createBindMounts,其代码如下所示: **Volume.cpp** ~~~ int Volume::createBindMounts() { unsigned long flags; /*‘ 将/mnt/secure/staging/android_secure目录名改成 /mnt/secure/staging/.android_secure,SEC_STG_SECIMGDIR的值就是 /mnt/secure/staging/.android_secure,也就是把它变成Linux平台上的隐藏目录 */ if(!access("/mnt/secure/staging/android_secure", R_OK | X_OK)&& access(SEC_STG_SECIMGDIR, R_OK | X_OK)) { if(rename("/mnt/secure/staging/android_secure", SEC_STG_SECIMGDIR)) { SLOGE("Failed to rename legacy asec dir (%s)",strerror(errno)); } } ...... /* 使用mount命令的bind选项,可将/mnt/secure/staging/.android_secure这个目录 挂载到/mnt/secure/asec目录下。/mnt/secure/asec目录是一个secure container, 目前主要用来保存一些安装在SD卡上的APP信息。APP2SD是Android 2.2引入的新机制,它 支持将APP安装在SD卡上,这样可以节约内部的存储空间。 mount的bind选项允许将文件系统的一个目录挂载到另外一个目录下。读者可以通过man mount 查询具体信息。 */ if(mount(SEC_STG_SECIMGDIR, SEC_ASECDIR, "", MS_BIND, NULL)) { ...... return -1; } ...... /* 将tempfs设备挂载到/mnt/secure/staging/.android_secure目录,这样之前 .android_secure目录中的内容就只能通过/mnt/secure/asec访问了。由于那个目录只能 由root访问,所以可以起到安全和保护的作用。 */ if(mount("tmpfs", SEC_STG_SECIMGDIR, "tmpfs", MS_RDONLY,"size=0,mode=000,uid=0,gid=0")){ ...... umount("/mnt/asec_secure"); return -1; } return0; } ~~~ createBindMounts的作用就是将存储卡上的.android_secure目录挂载到/mnt/secure/asec目录下,同时对.android_secure进行一些特殊处理,这样,没有权限的用户就不能更改或破坏.android_secure目录中的内容了,因此它起到了一定的保护作用。 在手机上,受保护的目录内容,只能在用adb shell登录后,进入/mnt/secure/asec目录来查看。注意,这个asec目录的内容就是.android_secure未挂载tmpfs时的内容(亦即它保存着那些安装在存储卡上的应用程序的信息)。另外,可把SD卡拔出来,通过读卡器直接插到台式机上,此时,这些信息就能在.android_secure目录中被直接看到了。 (4)MountService处理状态通知 volume的mountVol完成相关工作后,就通过下面的函数,发送信息给MountService: ~~~ setState(Volume::State_Mounted); //感兴趣的读者可自行分析此函数的实现。 ~~~ MountService依然会在onEvent函数中收到这个消息。 **MountService.java** ~~~ public boolean onEvent(int code, String raw,String[] cooked) { Intent in = null; ...... if(code == VoldResponseCode.VolumeStateChange) { /* 状态变化由notifyVolumeStateChange函数处理,由于Volume的状态 被置成Mounted,所以下面这个notifyVolumeStateChange会发送 ACTION_MEDIA_MOUNTED这个广播。我们就不再分析这个函数了,读者 可自行研究。 */ notifyVolumeStateChange( cooked[2], cooked[3],Integer.parseInt(cooked[7]), Integer.parseInt(cooked[10])); } ~~~ 实例分析就到这里。中间略去了一些处理内容,例如对分区的处理等,读者可自行研读,相信已没有太大难度了。另外,在上述处理过程中,稍微难懂的是mountVol这个函数在挂载方面的处理过程。用图9-6来总结一下这个处理过程: :-: ![](http://img.blog.csdn.net/20150802164428372?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) 图9-6 SD卡插入事件处理流程图 由上图可知,Vold在安全性上还是做了一定考虑的。如果没有特殊需要,读者了解上面这些知识也就够了。
';