9.2.6 Vold实例分析

这一节将分析一个实际案例,即插入一张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通信。

Thread thread=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();

const char*subsys=evt->getSubsystem();

……

//根据上面Uevent消息的内容可知,它的subsystem对应为block,所以我们会走下面这个if分支。

if(!strcmp(subsys,"block")){

vm->handleBlockEvent(evt);//调用VM的handleBlockEvent。

}

……

}

[—>VolumeManager.cpp]

void VolumeManager:handleBlockEvent(NetlinkEvent*evt){

const char*devpath=evt->findParam("DEVPATH");

VolumeCollection:iterator it;

bool hit=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){

const char*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 making device 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 chardevpath,NetlinkEventevt){

mDiskMajor=atoi(evt->findParam("MAJOR"));

mDiskMinor=atoi(evt->findParam("MINOR"));

const char*tmp=evt->findParam("NPARTS");

if(tmp){

mDiskNumParts=atoi(tmp);//这个disk上的分区个数。

}else{

……

mDiskNumParts=1;

}

char msg[255];

int partmask=0;

int i;

/*

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发送消息,注意它的第一个参数是ResponseC ode:VolumeDiskInserted。

*/

mVm->getBroadcaster()->sendBroadcast(ResponseCode:VolumeDiskInserted,

msg,false);

}


handleDiskAdded把Uevent消息做一些转换后发送给了MountService,实际上可认为CL模块和MountService通信使用的是另外一套协议。那么,MountService会做什么处理呢?

(2)MountService的处理


[—>MountService.java onEvent函数]

public boolean 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("volumemount%s",path));

}catch(NativeDaemonConnectorException e){

……//异常处理

}


走了一大圈,最后又回到Vold了。CL模块将收到这个来自MountService的请求,请求的内容为字符串“volume mount/mnt/sdcard”,其中的volume表示命令的名字,CL会根据这个名字找到VolumeCmd对象,并交给它处理这个命令。

(3)Vold处理MountService的命令

Vold处理MountService命令的代码如下所示:


[—>CommandListener.cpp VolumeCmd类]

int CommandListener:VolumeCmd:runCommand(SocketClientcli,int argc,char*argv)

{

……

VolumeManager*vm=VolumeManager:Instance();

int rc=0;

if(!strcmp(argv[1],"list")){

return vm->listVolumes(cli);

}else if(!strcmp(argv[1],"debug")){

……

}else if(!strcmp(argv[1],"mount")){

……//调用VM模块的mountVolume来处理mount命令,参数是"/mnt/sdcard"。

rc=vm->mountVolume(argv[2]);

}else if(!strcmp(argv[1],"unmount")){

……

rc=vm->unmountVolume(argv[2],force);

}else if(!strcmp(argv[1],"format")){

……

rc=vm->formatVolume(argv[2]);

}else if(!strcmp(argv[1],"share")){

……

rc=vm->shareVolume(argv[2],argv[3]);

}else if(!strcmp(argv[1],"unshare")){

……

rc=vm->unshareVolume(argv[2],argv[3]);

}

……

if(!rc){

//发送处理结果给MountService。

cli->sendMsg(ResponseCode:CommandOkay,"volume operation succeeded",false);

}

……

return 0;

}


来看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);

……

return v->mountVol();//mountVol由Volume类实现。

}


找到对应的DirectVolume后,也就找到了代表真实存储卡的对象。它是如何处理这个命令的呢?代码如下所示:


[—>Volume.cpp]

int Volume:mountVol(){

dev_t deviceNodes[4];

intn,i,r c=0;

char errmsg[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;

}

return 0;

}


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来总结一下这个处理过程:

9.2.6 Vold实例分析 - 图1

图 9-6 SD卡插入事件处理流程图

由上图可知,Vold在安全性上还是进行了一定考虑的。如果没有特殊需要,读者了解上面这些知识也就够了。