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-6 SD卡插入事件处理流程图
由上图可知,Vold在安全性上还是进行了一定考虑的。如果没有特殊需要,读者了解上面这些知识也就够了。