Android的存储系统(一)
看了很长时间Vold存储模块的相关知识,也死扣了一段时间的Android源码,发现Android存储系统所涉及的函数调用,以及Kernel与上层之间的Socket传输真的是让人头疼,除了需要整理整个架构的原理以外,还要反复看源码,真真的郁闷。
郁闷之余,还是打算把自己看过的经验之贴和参考资料进行整理,以帖子的形式发出来,供码神们参考,有不对的地方请指正,我们互相交流,下面就进入主题。
Android的存储系统主要由:SystemServer进程中的MountService和Vold进程中的VolumeManager组成。
它们管理着系统的存储设备,执行各种操作,如:mount、unmount、format等。
图1 Android存储系统架构图
图2 Android存储系统原理图
【重要组成分析】
1、NetlinkManager
全称是NetlinkManager.cpp位于Android 4.x 源码位置/system/vold/NetlinkManager.cpp。
该类的主要通过引用NetlinkHandler类中的onEvent()方法来接收来自内核的事件消息,NetlinkHandler位于/system/vold/NetlinkHandler.cpp。
2、VolumeManager
全称是VolumeManager.cpp位于Android 4.x源码位置/system/vold/VolumeManager.cpp。该类的主要作用是接收经过NetlinkManager处理过后的事件消息。
因为我们这里是SD的挂载,因此经过NetlinkManager处理过后的消息会分为五种,分别是:block、switch、usb_composite、battery、power_supply。
这里SD卡挂载的事件是block。
3、DirectVolume
位于/system/vold/DirectVolume.cpp。该类的是一个工具类,主要负责对传入的事件进行进一步的处理。
block事件又可以分为:Add、Removed、Change、Noaction这四种。
4、Volume
位于/system/vold/Volume.cpp,该类是负责SD卡挂载的主要类。Volume.cpp主要负责检查SD卡格式,以及对复合要求的SD卡进行挂载,并通过Socket将消息SD卡挂载的消息传递给NativeDaemonConnector。
5、CommandListener
该类位于位于/system/vold/CommandListener.cpp,通过vold socket与NativeDaemonConnector通信。
6、NativeDaemonConnector
该类位于frameworks/base/services/java/com.android.server/NativeDaemonConnector.java。该类用于接收来自Volume.cpp 发来的SD卡挂载消息并向上传递。
7、MountService
位于frameworks/base/services/java/com.android.server/MountService.java。
MountService是一个服务类,该服务是系统服务,提供对外部存储设备的管理、查询等。在外部存储设备状态发生变化的时候,该类会发出相应的通知给上层应用。在Android系统中这是一个非常重要的类。
8、StorageManaer
位于frameworks/base/core/java/andriod/os/storage/StorageManager.java。
在该类的说明中有提到,该类是系统存储服务的接口。在系统设置中,有Storage相关项,同时Setting也注册了该类的监听器。
而StorageManager又将自己的监听器注册到了MountService中,因此该类主要用于上层应用获取SD卡状态。
【SD卡挂载流程】
1、Kernel发出SD卡插入uevent消息。
2、NetlinkHandler::onEvent()接收内核发出的uevent并进行解析。
3、VolumeManager::handleBlockEvent()处理经过第二步处理后的事件。
4、接下来调用DirectVolume::handleBlockEvent()。
在该方法中需要注意亮点:
(1)程序首先会遍历mPath容器,寻找与event对应的sysfs_path是否存在于mPath容器中;
(2)针对event中的action有4种处理方式:Add、Removed、Change、Noaction。
5、经过上一步之后会调用DirectVolume::handleDiskAdded()方法,该方法中会广播disk insert消息。
6、SocketListener::runListener()会接收DirectVolume::handleDiskAdded()广播的消息。该方法主要完成对event中数据的获取,通过Socket。
7、调用FrameworkListener::onDataAvailable()方法处理接收到的消息内容。
8、FrameworkListener::dispatchCommand()该方法用于分发指令。
9、在FrameworkListener::dispatchCommand()方法中,通过runCommand()方法去调用相应的指令。
10、在/system/vold/CommandListener.cpp中有runCommand()的具体实现。在该类中可以找到这个方法:CommandListener::VolumeCmd::runCommand(),从字面意思上来看这个方法就是对Volume分发指令的解析。该方法中会执行“mount”函数:vm>mountVolume(arg[2])。
11、mountVolume(arg[2])在VolumeManager::mountVolume()中实现,在该方法中调用v>mountVol()。
12、mountVol()方法在Volume::mountVol()中实现,该函数是真正的挂载函数。(在该方法中,后续的处理都在该方法中,在Mount过程中会广播相应的消息给上层,通过setState()函数)。
13、setState(Volume::Checking);广播给上层,正在检查SD卡,为挂载做准备。
14、Fat::check();SD卡检查方法,检查SD卡是否是FAT格式。
15、Fat::doMount()挂载SD卡。
至此,SD的挂载已算初步完成,接下来应该将SD卡挂载后的消息发送给上层,在13中也提到过,在挂载以及检查的过程中其实也有发送消息给上层的。
16、MountService的构造函数中会开启监听线程,用于监听来自vold的socket信息。
Thread thread = new Thread(mConnector,VOLD_TAG); thread.start();
17、mConnector是NativeDaemonConnector的对象,NativeDaemonConnector继承了Runnable并Override了run方法。在run方法中通过一个while(true)调用ListenToSocket()方法来实现实时监听。
18、在ListenToSocket()中,首先建立与Vold通信的Socket Server端,然后调用MountService中的onDaemonConnected()方法。
19、onDaemonConnected()方法是在接口INativeDaemonConnectorCallbacks中定义的,MountService实现了该接口并Override了onDaemonConnected()方法。该方法开启一个线程用于更新外置存储设备的状态,主要更新状态的方法也在其中实现。
20、然后回到ListenToSocket中,通过inputStream来获取Vold传递来的event,并存放在队列中。
21、然后这些event会在onDaemonConnected()通过队列的”队列.take()”方法取出。并根据不同的event调用updatePublicVolumeState()方法,在该方法中调用packageManagerService中的updateExteralState()方法来更新存储设备的状态。
22、更新是通过packageHelper.getMountService().finishMediaUpdate()方法来实现的。
23、在updatePublicVolumeState()方法中,更新后会执行如下代码:
bl.mListener.onStorageStateChanged();
在Android源码/packages/apps/Settings/src/com.android.settings.deviceinfo/Memory.java代码中,实现了StorageEventListener 的匿名内部类,并Override了onStorageStateChanged()方法。因此在updatePublicVolumeState()中调用onStorageStateChanged()方法后,Memory.java中也会收到。在Memory.java中收到以后会在Setting界面进行更新,系统设置—存储中会更新SD卡的状态。从而SD卡的挂载从底层到达了上层。
在下一个帖子中我会对Vold模块的源码以及MountService服务进行分析,包括main函数、NetlinkManager、NetlinkHandler、处理block类型的uevent、处理MountService命令、VolumeManager、NativeDaemonConnector等源码,很快就会与大家见面,感谢支持,欢迎交流与指正!
Android的存储系统(二)
回顾:前贴主要分析了Android存储系统的架构和原理图,简要的介绍了整个从Kernel-->Vold-->上层MountService之间的数据传输流程,在这样的基础上,我们开始今天的源码分析!
【源码分析】
1. Vold的main函数
Vold也是通过init进程启动,它在init.rc中的定义如下:
1 service vold /system/bin/vold 2 class core 3 socket vold stream 0660 root mount 4 ioprio be 2Vold服务放到了core分组,这就意味着系统启动时,它就会被init进程启动。这里定义的一个socket,主要用语Vold和Java层的MountService通信。
Vold模块的源代码位于system/vold,我们看看入口函数main(),代码如下:
1 int main() { 2 VolumeManager *vm; 3 CommandListener *cl; 4 NetlinkManager *nm; 5 6 SLOGI("Vold 2.1 (the revenge) firing up"); 7 8 mkdir("/dev/block/vold", 0755); // 创建vold目录 9 10 klog_set_level(6); 11 12 if (!(vm = VolumeManager::Instance())) { // 创建VolumeManager对象 13 exit(1); 14 }; 15 16 if (!(nm = NetlinkManager::Instance())) { // 创建NetlinkManager对象 17 exit(1); 18 }; 19 22 cl = new CommandListener(); // 创建CommandListener对象 23 vm->setBroadcaster((SocketListener *) cl); // 建立vm和cl的联系 24 nm->setBroadcaster((SocketListener *) cl); // 建立nm和cl的联系 25 26 if (vm->start()) { // 启动VolumeManager 27 exit(1); 28 } 29 30 if (process_config(vm)) { // 创建文件/fstab.xxx中定义的Volume对象 31 SLOGE("Error reading configuration (%s)... continuing anyways", strerror(errno)); 32 } 33 34 cryptfs_pfe_boot(); 35 36 if (nm->start()) { // 启动NetlinkManager,会调用NetlinkManager的start()方法,它创建PF_NETLINK socket,并开启线程从此socket中读取数据 37 exit(1); 38 } 39 40 coldboot("/sys/block"); // 冷启动,创建/sys/block下的节点文件 41 42 if (cl->startListener()) { // 开始监听Framework的socket 43 exit(1); 44 } 45 46 while(1) { // 进入循环 47 sleep(1000); // 主线程进入休眠 48 } 4950 SLOGI("Vold exiting"); 51 exit(0); 52 }main函数的主要工作是创建3个对象:VolumeManager、NetlinkManager和CommandListener,同时将CommandListener对象分别设置到了VolumeManager对象和NetlinkManager对象中。
从前贴的架构图中可以发现,CommandListener对象用于和Java层的NativeDaemonConnector对象进行socket通信,因此,无论是VolumeManager对象还是NetlinkManager对象都需要拥有CommandListener对象的引用。
2. 监听驱动发出的消息—Vold的NetlinkManager对象
NetlinkManager对象的主要工作是监听驱动发出的uevent消息。
main()函数中调用NetlinkManager类的静态函数Instance()来创建NetlinkManager对象,代码如下:
1 NetlinkManager *NetlinkManager::Instance() { 2 if (!sInstance) 3 sInstance = new NetlinkManager(); // NetlinkManager对象通过静态变量sInstance来引用,这意味着vold进程中只有一个NetlinkManager对象。 4 return sInstance; 5 }看下NetlinkManager的构造函数,代码如下:
1 NetlinkManager::NetlinkManager() { 2 mBroadcaster = NULL; 3 }NetlinkManager的构造函数只是对mBroadcaster进行了初始化。我们可以发现main()函数中通过调用NetlinkManager的setBroadcaster()函数来给变量mBroadcaster重新赋值。
nm->setBroadcaster((SocketListener *) cl);main()函数还调用了NetlinkManager的start()函数,我们观察一下NetlinkManager中的start()方法,代码如下:
1 int NetlinkManager::start() { 2 struct sockaddr_nl nladdr; 3 int sz = 64 * 1024; 4 int on = 1; 5 6 memset(&nladdr, 0, sizeof(nladdr)); 7 nladdr.nl_family = AF_NETLINK; 8 nladdr.nl_pid = getpid(); 9 nladdr.nl_groups = 0xffffffff; 10 /*创建一个socket用于内核空间和用户空间的异步通信,监控系统的hotplug事件*/ 11 if ((mSock = socket(PF_NETLINK,SOCK_DGRAM,NETLINK_KOBJECT_UEVENT)) < 0) { 12 SLOGE("Unable to create uevent socket: %s", strerror(errno)); 13 return -1; 14 } 15 /*设置缓冲区大小为64KB*/ 16 if (setsockopt(mSock, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz)) < 0) { 17 SLOGE("Unable to set uevent socket SO_RCVBUFFORCE option: %s", strerror(errno)); 18 goto out; 19 } 20 /*设置允许 SCM_CREDENTIALS 控制消息的接收*/ 21 if (setsockopt(mSock, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on)) < 0) { 22 SLOGE("Unable to set uevent socket SO_PASSCRED option: %s", strerror(errno)); 23 goto out; 24 } 25 /*绑定 socket 地址*/ 26 if (bind(mSock, (struct sockaddr *) &nladdr, sizeof(nladdr)) < 0) { 27 SLOGE("Unable to bind uevent socket: %s", strerror(errno)); 28 goto out; 29 } 30 /*利用新创建的socket实例化一个NetlinkHandler类对象用于监听socket,NetlinkHandler继承了类NetlinkListener,NetlinkListener又继承了类SocketListener*/ 31 mHandler = new NetlinkHandler(mSock); 32 if (mHandler->start()) { // 启动NetlinkHandler,调用NetlinkHandler的start()函数 33 SLOGE("Unable to start NetlinkHandler: %s", strerror(errno)); 34 goto out; 35 } 36 37 return 0; 38 39 out: 40 close(mSock); 41 return -1; 42 }我们看一下NetlinkManager的家族关系,如下图:
上面的虚线为启动时的调用流程:
(1) class NetlinkManager(在其start函数中创建了NetlinkHandler对象,并把创建的socket作为参数)
(2)class NetlinkHandler: public NetlinkListener(实现了onEvent)
(3) class NetlinkListener : public SocketListener(实现了onDataAvailable)
(4) class SocketListener(实现了runListener,在一个线程中通过select查看哪些socket有数据,通过调用onDataAvailable来读取数据)。
总结:此贴主要分析了Vold的main()函数和NetlinkManager对象的源码,通过源码了解对象的创建时机和函数调用流程,下一贴会继续从NetlinkHandler的start()方法深入分析,继续源码的学习,很快会与大家见面,欢迎大家批评指正,我们互相学习。Android的存储系统(三)
回顾:前帖分析了Vold的main()函数和NetlinkManager的函数调用流程,截止到NetlinkHandler的创建和start()调用,本帖继续分析源码
1、处理block类型的uevent
main()函数创建了CommandListener对象,NetlinkManager的start()函数又创建了NetlinkHandler对象,如果将CommandListener类和NetlinkHandler类的继承关系图画出来,会发现它们都是从SocketListener类派生出来的,如下图所示:
图1 NetlinkHandler和CommandListener的继承关系
原理:处于最底层的SocketListener类的作用是监听socket的数据,接收到数据后分别交给FrameworkListener类和NetlinkListener类的函数,并分别对来自Framework和驱动的数据进行分析,分析后根据命令再分别调用CommandListener和NetlinkHandler中的函数。
观察NetlinkHandler类的构造方法,代码如下:
NetlinkHandler::NetlinkHandler(int listenerSocket) : NetlinkListener(listenerSocket) { }这个构造方法很简单,再看看它的start()方法,代码如下:
int NetlinkHandler::start() { return this->startListener(); }可以发现,start()方法调用了SocketListener的startListener()函数,代码如下:
1 int SocketListener::startListener(int backlog) { 2 if (!mSocketName && mSock == -1) { 3 SLOGE("Failed to start unbound listener"); 4 errno = EINVAL; 5 return -1; 6 } else if (mSocketName) { // 只有CommandListener中会设置mSocketName 7 if ((mSock = android_get_control_socket(mSocketName)) < 0) { 8 SLOGE("Obtaining file descriptor socket '%s' failed: %s",mSocketName, strerror(errno)); 9 return -1; 10 } 11 SLOGV("got mSock = %d for %s", mSock, mSocketName); 12 } 13 14 if (mListen && listen(mSock, backlog) < 0) { 15 SLOGE("Unable to listen on socket (%s)", strerror(errno)); 16 return -1; 17 } else if (!mListen) 18 mClients->push_back(new SocketClient(mSock, false, mUseCmdNum)); 19 20 if (pipe(mCtrlPipe)) { // 创建管道,用于退出监听线程 21 SLOGE("pipe failed (%s)", strerror(errno)); 22 return -1; 23 } 24 25 if (pthread_create(&mThread, NULL, SocketListener::threadStart, this)) { // 创建一个监听线程 26 SLOGE("pthread_create (%s)", strerror(errno)); 27 return -1; 28 } 29 30 return 0; 31 }startListener()函数开始监听socket,这个函数在NetlinkHandler中会被调用,在CommandListener也会被调用。
startListener()函数首先判断变量mSocketName是否有值,只有CommandListener对象会对这个变量赋值,它的值就是在init.rc中定义的socket字符串。
调用函数 android_get_control_socket()的目的是从环境变量中取得socket的值,这样CommandListener对象得到了它需要监听的socket,
而对于NetlinkHandler对象而言,它的mSocket不为NULL,前面已经创建了socket。
startListener()函数接下来会根据成员变量mListener的值来判断是否需要调用Listen()函数来监听socket。这个mListen的值在对象构造时根据参数来初始化。
对于CommandListener对象,mListener的值为ture,对于NetlinkHandler对象,mListener的值为false,这是因为CommandListener对象和SystemServer通信,需要监听socket连接,而NetlinkHandler对象则不用。
接下来startListener()函数会创建一个管道,这个管道的作用是通知线程停止监听,这个线程就是startListener()函数最后创建的监听线程,它的运行函数是threadStart(),在前贴的NetlinkManager家族图系中我们可以清晰的发现,其代码如下:
void *SocketListener::threadStart(void *obj) { SocketListener *me = reinterpret_cast<SocketListener *>(obj); me->runListener(); // 调用runListener()方法 pthread_exit(NULL); return NULL; }threadStart()中又调用了runListener()函数,代码如下:
1 void SocketListener::runListener() { 2 3 SocketClientCollection pendingList; 4 5 while(1) { // 无限循环,一直监听 6 SocketClientCollection::iterator it; 7 fd_set read_fds; 8 int rc = 0; 9 int max = -1; 10 11 FD_ZERO(&read_fds); // 清空文件描述符集read_fds 12 13 if (mListen) { // 如果需要监听 14 max = mSock; 15 FD_SET(mSock, &read_fds); // 把mSock加入到read_fds 16 } 17 18 FD_SET(mCtrlPipe[0], &read_fds); // 把管道mCtrlPipe[0]也加入到read_fds 19 if (mCtrlPipe[0] > max) 20 max = mCtrlPipe[0]; 21 22 pthread_mutex_lock(&mClientsLock); // 对容器mClients的操作需要加锁 23 for (it = mClients->begin(); it != mClients->end(); ++it) { // mClient中保存的是NetlinkHandler对象的socket,或者CommandListener接入的socket 24 int fd = (*it)->getSocket(); 25 FD_SET(fd, &read_fds); // 遍历容器mClients的所有成员,调用内联函数getSocket()获取文件描述符,并添加到文件描述符集read_fds 26 if (fd > max) { // 也加入到read_fds 27 max = fd; 28 } 29 } 30 pthread_mutex_unlock(&mClientsLock); 31 SLOGV("mListen=%d, max=%d, mSocketName=%s", mListen, max, mSocketName); 32 if ((rc = select(max + 1, &read_fds, NULL, NULL, NULL)) < 0) { // 执行select调用,开始等待socket上的数据到来 33 if (errno == EINTR) // 因为中断退出select,继续 34 continue; 35 SLOGE("select failed (%s) mListen=%d, max=%d", strerror(errno), mListen, max); 36 sleep(1); // select出错,休眠1秒后继续 37 continue; 38 } else if (!rc) 39 continue; // 如果fd上没有数据到达,继续 40 41 if (FD_ISSET(mCtrlPipe[0], &read_fds)) { 42 char c = CtrlPipe_Shutdown; 43 TEMP_FAILURE_RETRY(read(mCtrlPipe[0], &c, 1)); 44 if (c == CtrlPipe_Shutdown) { 45 break; 46 } 47 continue; 48 } 49 if (mListen && FD_ISSET(mSock, &read_fds)) { // 如果是CommandListener对象上有连接请求 50 struct sockaddr addr; 51 socklen_t alen; 52 int c; 53 54 do { 55 alen = sizeof(addr); 56 c = accept(mSock, &addr, &alen); // 接入连接请求 57 SLOGV("%s got %d from accept", mSocketName, c); 58 } while (c < 0 && errno == EINTR); // 如果是中断导致失败,重新接入 59 if (c < 0) { 60 SLOGE("accept failed (%s)", strerror(errno)); 61 sleep(1); 62 continue; // 接入发生错误,继续循环 63 } 64 pthread_mutex_lock(&mClientsLock); 65 mClients->push_back(new SocketClient(c, true, mUseCmdNum)); // 把接入的socket连接加入到mClients,这样再循环时就会监听到它的数据到达 66 pthread_mutex_unlock(&mClientsLock); 67 } 68 69 /* Add all active clients to the pending list first */ 70 pendingList.clear(); 71 pthread_mutex_lock(&mClientsLock); 72 for (it = mClients->begin(); it != mClients->end(); ++it) { 73 SocketClient* c = *it; 74 int fd = c->getSocket(); 75 if (FD_ISSET(fd, &read_fds)) { 76 pendingList.push_back(c); // 如果mClients中的某个socket上有数据了,把它加入到pendingList列表中 77 c->incRef(); 78 } 79 } 80 pthread_mutex_unlock(&mClientsLock); 81 82 /* Process the pending list, since it is owned by the thread,* there is no need to lock it */ 83 while (!pendingList.empty()) { // 处理pendingList列表 84 /* Pop the first item from the list */ 85 it = pendingList.begin(); 86 SocketClient* c = *it; 87 pendingList.erase(it); // 把处理了的socket从pendingList列表中删除 88 /* Process it, if false is returned, remove from list */ 89 if (!onDataAvailable(c)) { 90 release(c, false); // 调用release()函数-->调用onDataAvailable()方法 91 } 92 c->decRef(); 93 } 94 } 95 }SocketListener::runListener是线程真正执行的函数。
以上runListener()函数虽然比较长,但这是一段标准的处理混合socket连接的代码,对于我们编写socket的程序大有帮助,这里先做简单了解。
<--------接下来,我们继续分析......-------->
runListener()函数收到从驱动传递的数据或者MountService传递的数据后,调用onDataAvailable()函数来处理,FrameworkListener类和NetlinkListener类都会重载这个函数。
首先来分析一下NetlinkListener类的onDataAvailable()函数是如何实现的!
直接上代码:
1 bool NetlinkListener::onDataAvailable(SocketClient *cli) 2 { 3 int socket = cli->getSocket(); 4 ssize_t count; 5 uid_t uid = -1; 6 /*从socket中读取kernel发送来的uevent消息*/ 7 count = TEMP_FAILURE_RETRY(uevent_kernel_multicast_uid_recv(socket, mBuffer, sizeof(mBuffer), &uid)); 8 if (count < 0) { // 如果count<0,进行错误处理 9 if (uid > 0) 10 LOG_EVENT_INT(65537, uid); 11 return false; 12 } 13 14 NetlinkEvent *evt = new NetlinkEvent(); // 创建NetlinkEvent对象 15 if (evt->decode(mBuffer, count, mFormat)) { // 调用decode()函数 16 onEvent(evt); // 在NetlinkHandler中实现17 } else if (mFormat != NETLINK_FORMAT_BINARY) { 18 SLOGE("Error decoding NetlinkEvent"); 19 } 20 delete evt; 21 return true; 22 }NetlinkListener类的onDataAvailable()函数首先调用uevent_kernel_multicast_uid_recv()函数来接收uevent消息。
接收到消息后,会创建NetlinkEvent对象,然后调用它的decode()函数对消息进行解码,然后用得到的消息数据给NetlinkEvent对象的成员变量赋值。
最后onDataAvailable()函数调用了onEvent()函数继续处理消息,onEvent()函数的代码如下:
void NetlinkHandler::onEvent(NetlinkEvent *evt) { VolumeManager *vm = VolumeManager::Instance(); const char *subsys = evt->getSubsystem(); if (!subsys) { SLOGW("No subsystem found in netlink event"); return; } if (!strcmp(subsys, "block")) { vm->handleBlockEvent(evt); // 调用VolumeManager的handleBlockEvent()函数来处理 } }NetlinkHandler的onEvent()函数中会判断event属于哪个子系统的,如果属于“block”(SD热插拔),则调用VolumeManager的handleBlockEvent()函数来处理,代码如下:
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) { if (!(*it)->handleBlockEvent(evt)) { // 对每个DirectVolume对象,调用它handleBlockEvent来处理这个event hit = true; // 如果某个Volume对象处理了Event,则返回 break; } } ..... }总结:本帖的源码分析先到这里为止,下一贴再分析DirectVolume对象的handleBlockEvent()函数以及CommandListener对象如何处理从MountService发送的命令数据,即我们之前还没有讨论的关于FrameworkListener的onDataAvailable()函数的代码!
PS:希望对Android手机开发、IOS、以及游戏(纯兴趣,白菜)和Java EE感兴趣的码友们互粉,这样我也能及时的看到你们的大神之作和经验之贴,感谢感谢!
原文地址 http://www.cnblogs.com/pepsimaxin/p/5195842.html