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在安全性上还是做了一定考虑的。如果没有特殊需要,读者了解上面这些知识也就够了。
';