1 总体介绍

  在Android 中,当SD卡插入系统之后,系统会自动挂载。Vold 就是负责挂载SD卡的,vold 的全称是volume daemon。实际上是负责完成系统的CDROM,USB 大容量存储,MMC 卡等扩展存储的挂载任务自动完成的守护进程。它提供的主要特点是支持这些存储外设的热插拔。

1.1总体流程图

Android 源码 外置SD卡 挂载流程 sd卡挂载软件免root_构造函数

Ø         绿色箭头:表示插入SD卡后事件传递以及SD卡挂载

Ø         红色箭头:表示挂载成功后的消息传递流程

Ø         黄色箭头:表示MountService发出挂载/卸载SD卡的命令

1.2总体类图

Android 源码 外置SD卡 挂载流程 sd卡挂载软件免root_构造函数_02

n         main.cpp,vold的入口函数,系统起来会只执行vold的可执行文件,调到这个main函数中。

n         NetlinkManager.cpp位于源码位置/system/vold/NetlinkManager.cpp。该类的主要通过引用NetlinkHandler类中的onEvent()方法来接收来自内核的事件消息,NetlinkHandler位于/system/vold/NetlinkHandler.cpp。

n         VolumeManager:位于源码位置/system/vold/VolumeManager.cpp。该类的主要作用是接收经过NetlinkManager处理过后的事件消息。

n         DirectVolume:位于/system/vold/DirectVolume.cpp。该类的是一个工具类,主要负责对传入的事件进行进一步的处理,block事件又可以分为:Add,Removed,Change,Noaction这四种。

n             Volume:Volume.cpp位于/system/vold/Volume.cpp,该类是负责SD卡挂载的主要类。Volume.cpp主要负责检查SD卡格式,以及对复合要求的SD卡进行挂载,并通过Socket将消息SD卡挂载的消息传递给NativeDaemonConnector。

 

总的讲,vold程序需要分层三部分,第一部分为NetlinkManager,管理接受来自kernel的UEvent消息,第二部分为VolumeManager,主要负责处理来自NetlinkManager的消息和来自Java层的消息,之后真正的挂载卸载动作就需要volume负责了。

2 初始化流程

2.1 时序图

Android 源码 外置SD卡 挂载流程 sd卡挂载软件免root_初始化_03

2.2 代码分析

android 系统启动的时候,init进程会去解析init.rc文件,在该文件中,有如下代码:

service vold /system/bin/vold
    class core
    socket vold stream 0660 root mount
    ioprio be 2

定义了一个vold的service,去执行vold程序,并创建了一个名字为vold的socket,init进程解析完后就去执行vold程序,创建与java层通信的Socket。

      在Android 源码/system/vold路径下的main.cpp,这个就是vold程序的入口,我们看看起main函数,代码如下:


1. int
2.     VolumeManager *vm;  
3.     CommandListener *cl;  
4.     NetlinkManager *nm;  
5. if (!(vm = VolumeManager::Instance())) {//创建VolumeManager实例
6.     };  
7.    
8. if (!(nm = NetlinkManager::Instance())) {//创建NelinkManager实例
9.     };  
10. new CommandListener(); //创建与java层socket通信的接口
11.     vm->setBroadcaster((SocketListener *) cl);  
12.     nm->setBroadcaster((SocketListener *) cl);  
13. if (vm->start()) { //什么都没做
14.     }  
15. if (process_config(vm)) {//初始化fstab
16. "Error reading configuration (%s)... continuing anyways", strerror(errno));  
17.     }  
18. if (nm->start()) {//开始监听kernel上报的vold消息
19. }  
20. ……  
21. if (cl->startListener()) {//开始监听来自java层的socket消息
22. "Unable to start CommandListener (%s)", strerror(errno));  
23.         exit(1);  
24.     }  
25. while(1) {  
26.         sleep(1000);  
27.     }  
28.     exit(0);  
29. }

    首先,在 main 函数中,需要创建 VolumeManager 和 NetlinkManager 的实例,里面就做了一些初始化的动作,这里就不多说了。

接着,则是初始化vold与java层的socket通信接口。创建了的CommandListener实例。在上面的类图关系中,我们知道,CommandListener继承于FrameworkListener,而FrameworkListener有继承于SocketListener。先看看CommandListener的初始化代码:


1. CommandListener::CommandListener() :  
2. "vold") {  
3. new
4. new VolumeCmd()); //处理volume事件
5. new
6. new
7. new
8. new
9. new
10. }

    在上面代码中我们看到,先以“ vold ”为参数构造 FrameworkListener 类,完成之后,则调用 FrameworkListener 类中的 registerCmd() 方法,注册一些处理方法类,而对于 sd 卡挂载的事件,我们先关注 VolumeCmd 类,它是 FrameworkListener 的内部类,用于处理 Volume 事件。接下来,看 FrameworkListener 的构造函数:


1. FrameworkListener::FrameworkListener(const char
2. true) {  
3. new
4. }

    以之前传进来的“ vold ”参数构造 SocketListener 类,然后在 FrameworkListener 构造函数中,创建 FrameworkCommandCollection 的实例,其实它就是一个容器,用于存储之前调用的 registerCmd() 注册的处理方法类。下面就看 SocketListener 的构造函数:


1. SocketListener::SocketListener(const char *socketName, bool
2.     mListen = listen;  
3.     mSocketName = socketName; /将vold字符串存储在mSocketName变量中  
4.     mSock = -1;  
5.     pthread_mutex_init(&mClientsLock, NULL);  
6. new SocketClientCollection(); //创建socket客户端容器
7. }

    其实很简单,就是做了一些变量的初始化工作,用 mSocketName 变量存储“ vold ”字符串,这个 vold 是很有讲究的,因为是 init.rc 定义的 vold Service 中,就创建了一个名字为 vold 的 socket 端口,后面将通过“ vold ”获取到该   socket 端口。

到此,CommandListener的初始化就完成了的。

我们回到main函数中,创建了CommandListener实例之后,然后调用VolumeManger的setBroadcaster方法,将CommandListener的实例存储在mBroadcaster变量中,代码如下:


void setBroadcaster(SocketListener *sl) { mBroadcaster = sl; }


其实NetlinkManager也做了同样的设置,但我还没发现它有什么用,所以就不再关注了。

接下来就开始调用了main.cpp的process_config()方法了,在介绍之前,我必须先介绍下vold.fstab配置文件,这个配置文件就是在process_config()中被解析的,而vold.fstab配置文件,就是用于描述vold的挂载动作的,其配置例子如下:


dev_mount        sdcard         /mnt/sdcard         auto     /devices/platform/goldfish_mmc.0   
挂载命令            标签           挂载点              子分区个数               挂载路径


   

我们就以上面例子来说明,意思就是将/devices/platform/goldfish_mmc.0挂载到/mnt/sdcard中,/devices/platform/goldfish_mmc.0可以认为是kernel上报上来的路径。子分区个数如果为auto则表示只有1个子分区,也可以为任何不为0的整数。如果vold.fstab解析无误,VolueManager将创建DirectVolume。

好了,下面可以看 process_config()方法了,代码如下:


1. static int
2. FILE
3. int
4. char
5. if (!(fp = fopen("/etc/vold.fstab", "r"))) {  
6. return
7.     }  
8. while(fgets(line, sizeof(line), fp)) {  
9. const char *delim = " \t";  
10. char
11. char
12. int
13.         n++;  
14. '\0';  
15.    
16. if (line[0] == '#' || line[0] == '\0')  
17. continue;  
18. if
19. goto
20.         }  
21. if
22. goto
23.         }  
24. if
25. goto
26.         }  
27. if (!strcmp(type, "dev_mount")) {  
28.             DirectVolume *dv = NULL;  
29. char
30. if
31. goto
32.             }  
33. if (strcmp(part, "auto") && atoi(part) == 0) {  
34. goto
35.             }  
36. if (!strcmp(part, "auto")) {//如果解析没有错,那么就将创建DirectVolume
37. new
38. else
39. new
40.             }  
41. while
42. if (*sysfs_path != '/') {  
43. break;  
44.                 }  
45. if
46. goto
47.                 }  
48.             }  
49. if
50.                 flags = parse_mount_flags(sysfs_path);  
51. else
52.                 flags = 0;  
53.             dv->setFlags(flags);  
54. //将创建的DirectVolume添加到VolumeManager中。
55. else if (!strcmp(type, "map_mount")) {  
56. else
57.         }  
58.     }  
59.     fclose(fp);  
60. return
61. }

    该方法,通过一个 wihle 方法,逐行进行解析,如果认为合理,那么将拿到的信息用于创建 DirectVolume 实例,然后调用 VolumeManager 的 addVolume 方法,存储在 mVolumes 变量中。

好了,下面就开始看注册监听kernel的sockect端口了。就是NetLinkManager的start方法,代码如下:


int NetlinkManager::start() {
    struct sockaddr_nl nladdr;
    int sz = 64 * 1024;
    int on = 1;
    memset(&nladdr, 0, sizeof(nladdr));
    nladdr.nl_family = AF_NETLINK;
    nladdr.nl_pid = getpid();
    nladdr.nl_groups = 0xffffffff;
mSock = socket(PF_NETLINK,//创建socket,返回文件描述符
                        SOCK_DGRAM,NETLINK_KOBJECT_UEVENT)) < 0) {
        SLOGE("Unable to create uevent socket: %s", strerror(errno));
        return -1;
    }
setsockopt(mSock, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz)) < 0) {
        SLOGE("Unable to set uevent socket SO_RECBUFFORCE option: %s", strerror(errno));
        return -1;
    }
setsockopt(mSock, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on)) < 0) {
        SLOGE("Unable to set uevent socket SO_PASSCRED option: %s", strerror(errno));
        return -1;
    }
    if (bind(mSock, (struct sockaddr *) &nladdr, sizeof(nladdr)) < 0) {
        SLOGE("Unable to bind uevent socket: %s", strerror(errno));
        return -1;
    }
 mHandler = new NetlinkHandler(mSock);
    if (mHandler->start()) {
        return -1;
    }
    return 0;
}


其实就是调用socket()创建socket端口,返回描述符,经过一些设置,然后就描述符作为参数,创建的NetlinkHandler实例,然后就直接调用起start方法。看NetLinkHandler构造函数:

 


NetlinkHandler::NetlinkHandler(int listenerSocket) :
                NetlinkListener(listenerSocket) {
}


构造函数里什么都没做,NetlinkHandler继承于NetlinkListener,然后讲socket端口的描述符传进去。


NetlinkListener::NetlinkListener(int socket) :
                            SocketListener(socket, false) {
    mFormat = NETLINK_FORMAT_ASCII;
}


又是这么几句代码,NetlinkListener也是继承于SocketListener,所以还将socket描述符传进去,再次创建了SocketListener的实例,所以,在vold系统中,有两个SocketListener的实例。看其构造函数,这里的构造函数与之前的是不一样的,代码如下:


SocketListener::SocketListener(int socketFd, bool listen) {
    mListen = listen;
    mSocketName = NULL;
mSock = socketFd;
    pthread_mutex_init(&mClientsLock, NULL);
   mClients = new SocketClientCollection();
}


其实,与上面的构造函数,还是差不多的,只是传进来的参数不一样而已,之前的是一个“vold”字符串,而这里是一个socket的描述符。

好了,构造函数创建好了,那么接着看NetlinkHandler->start()方法:

int NetlinkHandler::start() {
return this->startListener();//指到了socketListener中了
}

这里的startListener方法是SocketListener中的,代码如下:

int SocketListener::startListener() {
    if (!mSocketName && mSock == -1) {
        return -1;
    } else if (mSocketName) {
        if ((mSock = android_get_control_socket(mSocketName)) < 0) {
            return -1;
        }
    }
    if (mListen && listen(mSock, 4) < 0) {
        return -1;
    } else if (!mListen)
 mClients->push_back(new SocketClient(mSock, false));//创建socket客户端,并添加到mClients容器中。
    if (pipe(mCtrlPipe)) {
        return -1;
    }
pthread_create(&mThread, NULL, SocketListener::threadStart, this)) {//创建新线程
        return -1;
    }
    return 0;
}

此时的条件下,mSocketName=null,mSock!=0,继续往下看,创建了SocketClient实例,并添加到mClients容器中,用于接收客户端发过来的消息。

接着创建新的一个线程,用于读取socket客户端发过来的消息,线程执行的方法如下:


void *SocketListener::threadStart(void *obj) {
    SocketListener *me = reinterpret_cast<SocketListener *>(obj);
  me->runListener();
    pthread_exit(NULL);
    return NULL;
}


看runListener()方法:


void SocketListener::runListener() {
    SocketClientCollection *pendingList = new SocketClientCollection();
    while(1) {
        ……
        for (it = mClients->begin(); it != mClients->end(); ++it) {
            int fd = (*it)->getSocket();
            if (FD_ISSET(fd, &read_fds)) {
                pendingList->push_back(*it);
            }
        }
        pthread_mutex_unlock(&mClientsLock);
        while (!pendingList->empty()) {//客户端有消息
            it = pendingList->begin();
            SocketClient* c = *it;
            pendingList->erase(it);
onDataAvailable(c) && mListen) {//  处理消息          
            }
        }
    }
    delete pendingList;
}


在该方法中,一个while循环,不断读取socket消息,如果发现有socket消息,那么就调用方法onDataAvailable处理,该方法是在NetlinkListener方法实现的,其代码如下:


bool NetlinkListener::onDataAvailable(SocketClient *cli)
{
    int socket = cli->getSocket();
    ssize_t count;
    count = TEMP_FAILURE_RETRY(uevent_kernel_multicast_recv(socket, mBuffer, sizeof(mBuffer)));
    if (count < 0) {
        SLOGE("recvmsg failed (%s)", strerror(errno));
        return false;
    }
    NetlinkEvent *evt = new NetlinkEvent();
    if (!evt->decode(mBuffer, count, mFormat)) {
        SLOGE("Error decoding NetlinkEvent");
    } else {
onEvent(evt);//在NetlinkHandler被实现
    }
    return true;


就是经过了处理,跳转到了NetlinkHandler的onEvent()方法处理。好了,注册kernel监听就到此先搞一段落了。

回到了main函数中,最后,看到调用了CommandListener->startListener(),其实就是调用了SocketListener中的startListener方法。代码就不再次贴出来了,同样也是创建了一个新的线程读取socket消息,只是,发现有消息后,调用的是FrameworkListener中的onDataAvailable方法处理。

好了,到此,vold的初始化已经完成了。下面看看sd的mount流程吧。

3 SD卡mount流程

3.1时序图

Android 源码 外置SD卡 挂载流程 sd卡挂载软件免root_初始化_04

3.2 流程图

Android 源码 外置SD卡 挂载流程 sd卡挂载软件免root_描述符_05

3.3 代码分析

经过前面的介绍,我们知道了,在NetlinkHandler的onEvent方法中,收到了kernel的消息。其代码如下:


void NetlinkHandler::onEvent(NetlinkEvent *evt) {
    VolumeManager *vm = VolumeManager::Instance();
    const char *subsys = evt->getSubsystem();
    if (!subsys) {
        return;
    }
    if (!strcmp(subsys, "block")) {
 vm->handleBlockEvent(evt);//进一步处理
    }
}


这里就只处理block消息了,看看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) {
!(*it)->handleBlockEvent(evt)) {//到DirectVolume处理
            hit = true;
            break;
        }
    }
}


这里的for循环遍历mVolumes,它其实是DirectVolume实例列表,在解析vold.fstab中,创建的DirectVolume实例并添加到mVolumes列表中。然后再调用DirectVolume的handleBlockEvent方法尝试处理该消息,看是否能匹配,起代码如下:


int DirectVolume::handleBlockEvent(NetlinkEvent *evt) {
    const char *dp = evt->findParam("DEVPATH");
 
    PathCollection::iterator  it;
 for (it = mPaths->begin(); it != mPaths->end(); ++it) {//遍历vold.fstab定义的路径
        if (!strncmp(dp, *it, strlen(*it))) {//kernel上报上来的路径与vold.fstab中定义的匹配
           
            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);
                if (createDeviceNode(nodepath, major, minor)) {
                }
                if (!strcmp(devtype, "disk")) {//插入设备消息
 handleDiskAdded(dp, evt);//上报一个物理分区
                } else {
 handlePartitionAdded(dp, evt);//上报一个逻辑分区
                }
            } else if (action == NetlinkEvent::NlActionRemove) {//拔出设备消息
                if (!strcmp(devtype, "disk")) {
                    handleDiskRemoved(dp, evt);
                } else {
                    handlePartitionRemoved(dp, evt);
                }
            } else if (action == NetlinkEvent::NlActionChange) {//设备状态改变消息
                if (!strcmp(devtype, "disk")) {
                    handleDiskChanged(dp, evt);
                } else {
                    handlePartitionChanged(dp, evt);
                }
            } else {
                    SLOGW("Ignoring non add/remove/change event");
            }
            return 0;
        }
    }
    errno = ENODEV;



Kernel上报上来的消息中,有一个路径的消息,将与vold.fstab中定义的路径进行匹配,如果匹配,那么说明这个消息是有效的,那么就继续处理。

那么,kernel上报的消息也分为三类,分别是设备插入、拔出、状态改变。我们这里就先关注插入的消息吧。

那么,插入的消息,又分是物理分区还是一个逻辑分区。假如插入一个sd卡,它只有一个分区,那么上报的就是Disk消息。假如插入一个sd卡,该卡有内部又被分成多个分区,那么就先上报的是一个Dist消息,用于描述这个sd卡,后面还会上报多个消息,每个消息对应sd卡中的一个分区,也就是partition消息。

在这里,我们关注Dist消息吧,看看handleDiskAdded()方法,代码如下;


void DirectVolume::handleDiskAdded(const char *devpath, NetlinkEvent *evt) {
    mDiskMajor = atoi(evt->findParam("MAJOR"));
    mDiskMinor = atoi(evt->findParam("MINOR"));
    const char *tmp = evt->findParam("NPARTS");
    if (tmp) {
        mDiskNumParts = atoi(tmp);//如果上报的是只有一个分区的sd,该变量为0
    } else {
        mDiskNumParts = 1;
    }
       mPartsEventCnt = 0;
    char msg[255];
    int partmask = 0;
    int i;
    for (i = 1; i <= mDiskNumParts; i++) {
        partmask |= (1 << i);
    }
    mPendingPartMap = partmask;
    if (mDiskNumParts == 0) {
 setState(Volume::State_Idle);//设置初始状态
    } else {
        setState(Volume::State_Pending);
    }
    snprintf(msg, sizeof(msg), "Volume %s %s disk inserted (%d:%d)",
             getLabel(), getMountpoint(), mDiskMajor, mDiskMinor);//构造消息
  mVm->getBroadcaster()->sendBroadcast(ResponseCode::VolumeDiskInserted,
                                             msg, false);//发socket消息到java层
}


如果是Disk消息,那么上报的sd卡只有一个分区,所以上面的mDiskNumParts=0。看下面,调用snprintf()构造msg消息,然后调用mVm->getBroadcaster()->sendBroadcast发送到java层。其实mVm->getBroadcaster()就是放回CommandListener的实例变量,sendBroadcast就是在SocketListener中,代码如下:


void SocketListener::sendBroadcast(const char *msg) {
    pthread_mutex_lock(&mClientsLock);
    SocketClientCollection::iterator i;
    for (i = mClients->begin(); i != mClients->end(); ++i) {
sendMsg(msg))发送socket消息
        }
    }
    pthread_mutex_unlock(&mClientsLock);
}


Ok,看到了吧,这里就发送了一个VolumeDiskInserted的消息到java层。但如果是系统改启动的话,kernel早早就发来了消息,但是java层还没起来呢。所以,等到mountService起来之后,就收到了socket消息了。

我们直接看mountService的onEvent()方法吧代码如下:


public boolean onEvent(int code, String raw, String[] cooked) {
   …….
VolumeDiskInserted) {
   new Thread() {
    public void run() {
           try {
             int rc;
doMountVolume(path)) != StorageResultCode.OperationSucceeded) {
                      Slog.w(TAG, String.format("Insertion mount failed (%d)", rc));
                            }
               } catch (Exception ex) {
                      }
                    }
                }.start();
            } else if (code == VoldResponseCode.VolumeDiskRemoved) {
             }
}


这里我们只看onEvent的处理VoldResponseCode.VolumeDiskInserted消息,我们看到,对于VolumeDiskInserted消息,mountService立刻调用了方法doMountVolume(path),其实就是通过socket对vold发送了一个条mount的命令。

所以对与java层来讲,可以发mount、unmount消息到vold中。那么现在,就看vold处理吧。

前面也介绍过,java层发送的socket消息,vold层在SocketListener中读取到,然后会在FrameworkListener的onDataAvailable()方法中处理,代码如下:


bool FrameworkListener::onDataAvailable(SocketClient *c) {
    char buffer[255];
    int len;
    len = TEMP_FAILURE_RETRY(read(c->getSocket(), buffer, sizeof(buffer)));
    if (len < 0) {
        return false;
    } else if (!len)
        return false;
    int offset = 0;
    int i;
    for (i = 0; i < len; i++) {
        if (buffer[i] == '\0') {
 dispatchCommand(c, buffer + offset);//开始派发消息
            offset = i + 1;
        }
    }
    return true;
}


调用dispatchCommand()派发消息了,代码如下:


void FrameworkListener::dispatchCommand(SocketClient *cli, char *data) {
    ……
    for (i = mCommands->begin(); i != mCommands->end(); ++i) {
        FrameworkCommand *c = *i;
        if (!strcmp(argv[0], c->getCommand())) {
  if (c->runCommand(cli, argc, argv)) {
                SLOGW("Handler '%s' error (%s)", c->getCommand(), strerror(errno));
            }
    }
}


一堆的处理,代码也就不贴出来了,直接看关键的部分吧。记得在CommandListener的构造函数中吗,里面调用了FrameworkListener的registerCmd()方法,注册了一些处理方法类,其实就是添加到了mCommands容器中了,这里当然需要遍历咯,找到其合适的处理方法类,然后调用其runComand()方法,看看其代码吧:


int CommandListener::VolumeCmd::runCommand(SocketClient *cli,
                                                      int argc, char **argv) {
    dumpArgs(argc, argv, -1);
    …….
    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")) {//处理mount消息
 rc = vm->mountVolume(argv[2]);
    } else if (!strcmp(argv[1], "unmount")) {
        rc = vm->unmountVolume(argv[2], force, revert);
} else if (!strcmp(argv[1], "format")) {  //处理格式化消息
        rc = vm->formatVolume(argv[2]);
    } else if (!strcmp(argv[1], "share")) { //处理挂载到pc消息
        rc = vm->shareVolume(argv[2], argv[3]);
    } else if (!strcmp(argv[1], "unshare")) {
        rc = vm->unshareVolume(argv[2], argv[3]);
    } else if (!strcmp(argv[1], "shared")) {
        bool enabled = false;
        if (vm->shareEnabled(argv[2], argv[3], &enabled)) {
    }
    return 0;
}


在这里处理Volume消息,我们就只看mount消息吧,就调用了VolumeManager的mountVolume方法,代码如下:


int VolumeManager::mountVolume(const char *label) {
    Volume *v = lookupVolume(label);//找到该挂载点的Volume的实例
    if (!v) {
        return -1;
    }
 return v->mountVol();//去挂载啦
}


到Volume的mountVol()中挂载,代码如下:


int Volume::mountVol() {
  …..
}


这个方法代码量比较大,就不贴出来了,但是完成mount的动作就是在该方法中,然后呢,Volume中还包含了其他的功能方法,比如unmount、share、unshare。

好了,花了一个下午的时间整理出来,vold的初始化即sd卡的挂载流程就讲解到这吧,我这里讲流程的比较多,很多细节问题也没有讲,其实我写的文档,还是比较注册流程,消息是怎么传递的,至于细节,用到的时候,再详细看!