• Android研究Android的init启动到launcher启动主要分析zygote服务


    本文引用:http://blog.csdn.net/lizhiguo0532/article/details/7028910

    ------------------------------------------------------------------------------------------------------

    Android的启动过程是从进程init开始的,所以它是后续所有进程的祖先进程。

    一、init进程

    源码位于system/core/init目录。主要做了以下事情:

    1.     重新设置子进程终止时信号SIGCHLD的处理函数。

    act.sa_handler = sigchld_handler;  //调用了wait函数等待子进程退出。

    act.sa_flags = SA_NOCLDSTOP;

    act.sa_mask = 0;

    act.sa_restorer = NULL;

    sigaction(SIGCHLD, &act, 0);

    2. 将kernel启动过程中建立好的文件系统框架mount到相应目录。

        mount("tmpfs", "/dev", "tmpfs", 0, "mode=0755");

       …

        mount("devpts", "/dev/pts", "devpts", 0, NULL);

        mount("proc", "/proc", "proc", 0, NULL);

    mount("sysfs", "/sys", "sysfs", 0, NULL);

       

    3.     open_devnull_stdio(),将init进程的标准输入、输出、出错设备设置为新建的设备节点/dev/__null__。

    4.     log_init(),创建并打开设备节点/dev/__kmsg__。

    5.     读取并解析rc配置文件。

    5.1 先从文件/sys/class/BOOT/BOOT/boot/boot_mode读出启动方式:Factory Mode, '4';ATE Factory Mode, '6'。看是否是facatory模式。

    5.2 如果是的话,需要读取并解析两个文件:init.factory.rc和init.rc。

    5.3 如果是正常启动,则暂时先读取init.rc。

    这里在读取解析文件的时候,是以行为最小可执行单位在解析。关于书写init.rc文件的初始化脚本语言的规则,可以上网查找。解析之后并不会马上执行,而是在init进入服务循环之前统一根据其命令本身所带的条件来执行。

       

    6.     导入kernel的cmdline,也就是u-boot传递给kernel的参数,查看其中是否具有 androidboot.xxx(androidboot.mode、androidboot.hardware等)参数,如果有,将其保存在同名字的 xxx(mode、hardware)全局变量中。这里特别说明的是hardware这个参数,从kernel中导出一部分之后,又要从/proc /cpuinfo中导出一部分来组合成完整的hardware参数,因为后面接下来会读取并解析以特定平台的rc文件。

    7.     读取特定平台相关的initrc文件,如:init.mt6516.rc。

    需要注意的是:对于service,这里会给每个服务建立一个struct service的结构体,全部挂入链表service_list之中,在init最后才启动。

       

    8.     检查解析出来的所有命令行当中是否有属于early-init的,如果有,将其提出来加入到链表action_queue之中,马上将其执行掉。

    9.     device_init()函数将会打开uevent的netlink socket,遍历/sys/class、/sys/block、/sys/devices目录,检查各级目录的uevent文件,处理在vold服务起 来之前由kernel所发出来的device add, remove等事件。

    10.  property_init(), 顾名思义,是属性初始化。首先创建一个名字为system_properties的匿名共享内存区域,对并本init进程做mmap读写映射,其余共享它 的进程只有读的权限。然后将这个prop_area结构体通过全局变量__system_property_area__传递给property services。

    接着调用函数load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT)从/default.prop文件中加载编译时生成的属性。

           

    11.  如果在root目录下有initlogo.rle文件存在,这个是两张android字样的缕空图片,将其读入fb中显示到LCD上。同时也要往串口上输出"             A N D R O I D "。如果图片不存在,就没有这两项的输出。

    12.  设置相应的属性:

    property_set("ro.factorytest", "0")

    property_set("ro.serialno", serialno[0] ? serialno : "");

    property_set("ro.bootmode", bootmode[0] ? bootmode : "unknown");

    property_set("ro.baseband", baseband[0] ? baseband : "unknown");

    property_set("ro.carrier", carrier[0] ? carrier : "unknown");

    property_set("ro.bootloader", bootloader[0] ? bootloader : "unknown");

    property_set("ro.hardware", hardware);

    snprintf(tmp, PROP_VALUE_MAX, "%d", revision);

    property_set("ro.revision", tmp);

    13.  开始执行以init为trigger的命令行:

    action_for_each_trigger("init", action_add_queue_tail);

    drain_action_queue();

            前面有执行过eraly-init的。

           

    14.  启动属性服务:property_set_fd = start_property_service();

    先读取剩余三个文件中的属性:/system/build.prop、/system/default.prop、/system /default.prop,然后用函数load_persistent_properties()加载persist.开始的属性,这种属性都是保存在 目录/data/property下的以属性名为文件名的中。

    接下来创建一个名为property_service的socket接口(SOCK_STREAM),然后进入监听状态,等待属性事件到来。

       

    15.  创建一对socket,用来做信号方面的处理。

    socketpair(AF_UNIX, SOCK_STREAM, 0, s),signal_fd = s[0],signal_recv_fd = s[1]。

        16.执行eraly-boot和boot为trigger的命令

            action_for_each_trigger("early-boot", action_add_queue_tail);

            action_for_each_trigger("boot", action_add_queue_tail);

            drain_action_queue();

           

    17.执行init.rc中以property:开头的属性设置语句,同时使能属性触发方式。

    queue_all_property_triggers();

    drain_action_queue();

           

            property_triggers_enabled = 1;  //  可以执行那些以属性为条件的init语句。

           

    1.     接下来就是利用poll机制监听前面创建的几个fd的动态。

    struct pollfd ufds[4];

    ufds[0].fd = device_fd;

    ufds[0].events = POLLIN;

    ufds[1].fd = property_set_fd;

    ufds[1].events = POLLIN;

    ufds[2].fd = signal_recv_fd;

    ufds[2].events = POLLIN;

    for(;;) {

         int nr, i, timeout = -1;

               

        for (i = 0; i < fd_count; i++)

            ufds[i].revents = 0;

        

         drain_action_queue(); //执行action_queue链表中后来新出现的command。

         restart_processes();  // 第一次启动所有服务,也包括后来restart这些

    服务。restart_service_if_needed() à service_start(svc, NULL) à fork()

        

         …

         nr = poll(ufds, fd_count, timeout);

         if (nr <= 0)

                continue;

        

         if (ufds[2].revents == POLLIN) {

                /* we got a SIGCHLD - reap and restart as needed */

                read(signal_recv_fd, tmp, sizeof(tmp));

                while (!wait_for_one_process(0))

                    ;

                continue;

         }

        

         if (ufds[0].revents == POLLIN)

              handle_device_fd(device_fd);  // Vold的netlink类型的socket

        

         if (ufds[1].revents == POLLIN)

              handle_property_set_fd(property_set_fd);//属性服务的socket

         if (ufds[3].revents == POLLIN)

                handle_keychord(keychord_fd);

    }

    到这里init就进入了死循环中一直在监听ufds中的4个文件描述符的动静,如果有POLLIN的事件,就做相应的处理,所以init并没有退出 或者进入idle,而是被当做一个服务在运行。第4个文件描述符是keychord_fd,暂时不清楚这个怎么用,不过通过它也可以启动服务,可参考源 码。

    下面是init.rc的例子,见附件init.rc

           

    二、init中启动的各种服务

    在init中启动起来的服务按照init.rc中的先后顺序,大致有:

    console: start a shell,code path: system/bin/sh,其源码中包含常用的shell命令,如ls,cd等。

    adbd: start adb daemon,通常带有disabled的选项,表明需要按名字启动,code path:system/bin/adb。

    servicemanager:这个服务管理着系统内所有binder services。code path: frameworks/base/cmds/servicemanager。

    Vold: android 的udev,code path: system/vold。

    Netd: start ntd daemon, code path: system/netd。

    Debuggerd: start debug system, code path: system/core/debuggerd。

    zygote: ['zaigəut]这是一个非常重要的服务,稍后详解。start Android  Java Runtime  and start systemserver。code path:frameworks/base/cmds/app_process。

    media: add AudioFlinger,AudioPolicyService,MediaPlayerService and CameraService to servicemanager,同时启动管理binder通讯的机制,依靠这两个类来完成binder机制在android中间层所体现的功 能:ProcessState 和IPCThreadState。Code path:frameworks/base/media/mediaserver。

    bootanim: 开机动画和铃声,code path:frameworks/base/cmds/bootanimation。

    接下来就是关于modem的服务,如:ccci_fsd、ccci_mdinit、pppd_gprs、pppd、gsm0710muxd、 muxtestapp、sockcli、socksrv、muxreport、ril-daemon等,除了前面2个,后面的都带有disabled的参 数,需要按名启动。

    Installd: start install package daemon, code path:

    frameworks/base/cmds/installd。

    后面还有很多关于其他硬件的服务,比如BT、WIFI等。

    2.1 servicemanager

        这个服务进程代码比较简单,功能也简单,c实现的,用来管理系统中所有的binder service,不管是本地的c++实现的还是java语言实现的都需要这个进程来统一管理,最主要的管理就是,注册添加服务,获取服务。

        这些binder服务在对外公开之前都必须将自身拥有的binder实体注册到SMgr中,而其他进程如果需要使用binder service的功能,也必须先经过SMgr取得 binder service中的binder实体在SMgr中对应的引用号(binder的引用号和进程中文件描述符概念类似,有效范围只限于本进程)。

    #define BINDER_SERVICE_MANAGER ((void*) 0)  

    int main(int argc, char **argv) 

        struct binder_state *bs; 

        void *svcmgr = BINDER_SERVICE_MANAGER;  

        bs = binder_open(128*1024);   

        // 打开binder设备,mmap映射当前进程的binder接收缓冲区,返回一个binder_state结构体  。

        if (binder_become_context_manager(bs)) {     

        // 通过这个函数将当前进程设置成服务管理进程MSgr,整个系统就这一个。 

           …

        } 

        svcmgr_handle = svcmgr;    

        binder_loop(bs, svcmgr_handler);  

     /*svcmgr_handler作为处理函数,所能完成的操作有:获取service,查看service是否存在,添加service ,列出service清单。其中用的最多的就是获取、添加。*/ 

        return 0; 

    }

       

        2.2 zygote

            zygote服务进程也叫做孵化进程,在linux的用户空间,进程app_process会做

    一些zygote进程启动的前期工作,如,启动runtime运行时环境(实例),参数分解,设置startSystemServer标志,接着用 runtime.start()来执行zygote服务的代码,其实说简单点,就是zygote抢了app_process这个进程的躯壳,改了名字,将 后面的代码换成zygote的main函数,这样顺利地过度到了zygote服务进程。这样我们在控制台用ps看系统所有进程,就不会看到 app_process,取而代之的是zygote。

           

            而前面runtime.start()这个函数实际上是类函数AndroidRuntime::start(),在

    这个函数中,会新建并启动一个虚拟机实例来执行com.android.internal.os.ZygoteInit这个包的main函数。这个 main函数中会fork一个子进程来启动systemserver,父进程就作为真正的孵化进程存在了,每当系统要求执行一个 Android应用程序,Zygote就会收到socket消息FORK出一个子进程来执行该应用程序。因为Zygote进程是在系统启动时产生的,它会 完成虚拟机的初始化,库的加载,预置类库的加载和初始化等操作,而在系统需要一个新的虚拟机实例时可以快速地制造出一个虚拟机出来。

       

        每一个Android应用都运行在一个Dalvik虚拟机实例里,而每一个虚拟机实例都是一个独立的进程空间。虚拟机的线程机制,内存分配和管 理,Mutex等等都是依赖底层linux实现的。所以android应用程序中创建了线程将会实际调用到linux的线程创建接口,和虚拟机所在进程共 享一个虚拟机实例对java代码执行。

       

    2.2.1 app_process

    下面重点讨论zygote服务,源码位于frameworks/base/cmds/app_process。

    service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server

    参数:/system/bin/app_process -Xzygote /system/bin --zygote --start-system-server

    int main(int argc, const char* const argv[])

    {

        …

        AppRuntime runtime; // 这里启动runtime运行时环境(实例),AppRuntime是AndroidRuntime的子类,在创建这个对象的时候会依次调用基类和子类的构造函数。

        const char *arg;

        const char *argv0;

        …

        argv0 = argv[0];

        // ignore argv[0]

        argc--;

    argv++;

    /*

        argc = 4;

    argv[0] = “-Xzygote”;

    argv[1] = “/system/bin”;

    argv[2] = “--zygote”;

    argv[3] = “–start-system-server”;

    */

        int i = runtime.addVmArguments(argc, argv); // 找到参数中第一个不是以单个-开始的参数,这里很明显是第二个参数:/system/bin

        if (i < argc) {

            runtime.mParentDir = argv[i++];  // 将命令目录保存在mParentDir中,之后i = 2。

        }

        if (i < argc) {

            arg = argv[i++];

            if (0 == strcmp("--zygote", arg)) { // 通常这个分支是成立的

                bool startSystemServer = (i < argc) ?

                        strcmp(argv[i], "--start-system-server") == 0 : false;

    // startSystemServer = true ,这个bool变量决定着后面执行runtime.start时是否启动systemserver。

                setArgv0(argv0, "zygote");

                set_process_name("zygote");

                runtime.start("com.android.internal.os.ZygoteInit",

                    startSystemServer);

            } else {

                … // 只启动AndroidRuntime

            }

        }else{

            … // 错误处理

        }

    } // main()

       

        2.2.2 AndroidRuntime

        下面进一步分析

        runtime.start("com.android.internal.os.ZygoteInit",startSystemServer);

        位于文件frameworks/base/core/jni/ AndroidRuntime.cpp,类定义于:

        frameworks/base/include/android_runtime/AndroidRuntime.h

        void AndroidRuntime::start(const char* className,

    const bool startSystemServer){

    LOGD("\n>>>>>>>>>>>>>> AndroidRuntime START <<<<<<<<<<<<<<\n");

        JNIEnv* env;

        …

        /* start the virtual machine , mJavaVM是类AndroidRuntime的私有变量,env该函数中局部变量 */

        if (startVm(&mJavaVM, &env) != 0)

            goto bail;

        /* Register android functions.向刚刚新建的虚拟机注册JNI本地接口。frameworks/base/core/jni/这个目录下的所有jni接口。*/

        if (startReg(env) < 0) {

            …

    goto bail;

    }

    //启动虚拟机之前需要构造java形式的参数数组,如下:

    jclass stringClass;

    jobjectArray strArray;

        jstring classNameStr;

        jstring startSystemServerStr;

    stringClass = env->FindClass("java/lang/String");

    strArray = env->NewObjectArray(2, stringClass, NULL);

    classNameStr = env->NewStringUTF(className);

    env->SetObjectArrayElement(strArray, 0, classNameStr);

    startSystemServerStr = env->NewStringUTF(startSystemServer ?

                                                     "true" : "false");

    env->SetObjectArrayElement(strArray, 1, startSystemServerStr);

    /*  strArray[0] = “com.android.internal.os.ZygoteInit”

        strArry[1] = “true”*/

    /*

     * Start VM.  This thread becomes the main thread of the VM, and will

     * not return until the VM exits.

     */

    jclass startClass;

    jmethodID startMeth;

    slashClassName = strdup(className);

    for (cp = slashClassName; *cp != '\0'; cp++)

        if (*cp == '.')

            *cp = '/';  // 将包名换成路径

    startClass = env->FindClass(slashClassName); // 根据路径找到这个包

                // com.android.internal.os.ZygoteInit,这个类位于文件

                // com/ndroid/nternal/os/ZygoteInit.java

    if (startClass == NULL) {

         LOGE("JavaVM unable to locate class '%s'\n", slashClassName);

         /* keep going */

     } else {

            /*这个参数决定了我们接下来执行的是zygoteInit.java的main函数。*/

            startMeth = env->GetStaticMethodID(startClass, "main",

                "([Ljava/lang/String;)V");

            if (startMeth == NULL) {

                LOGE("JavaVM unable to find main() in '%s'\n", className);

                /* keep going */

            } else {

                env->CallStaticVoidMethod(startClass, startMeth, strArray);

                    // 虚拟机启动,将会调用到com.android.internal.os.ZygoteInit包的main函数。

    }

    }

    LOGD("Shutting down VM\n");

        if (mJavaVM->DetachCurrentThread() != JNI_OK)

            LOGW("Warning: unable to detach main thread\n");

        if (mJavaVM->DestroyJavaVM() != 0)

            LOGW("Warning: VM did not shut down cleanly\n");

    bail:

        free(slashClassName);

    }

    } // start()

       

        2.2.3 ZygoteInit

        下面进入com.android.internal.os.ZygoteInit 包的main函数:

        frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

        public static void main(String argv[]) {

            try {

                …

                registerZygoteSocket(); // Registers a server socket for zygote command

                // load classs and resources

        preloadClasses();

        preloadResources();

        …

        // Do an initial gc to clean up after startup

    gc();/*初始化GC垃圾回收机制*/

    /* 通过从前面传递过来的第二个参数startsystemserver=”true” 启动systemserver, 在startSystemServer()中会fork一个新的进程命名为system_server, 执行的是com.android.server包中的SystemServer.java文件中的main函数, 源码位置:frameworks/base/services/java/com/android/server/ SystemServer.java。*/

    if (argv[1].equals("true")) {

                    startSystemServer(); ///*************

    } else if(…)

    if (ZYGOTE_FORK_MODE) {

        runForkMode();      /* ZYGOTE_FORK_MODE 永远为flase */

    } else {

        runSelectLoopMode();/* Zygote进程进入无限循环,不再返回。接下来的zygote将会作为一个孵化服务进程来运行。*/

    }

    closeServerSocket();

    }

            …

        } // end main()

       

    从这里开始android启动分为两条线走,分别是:

    startSystemServer(); ---------- Zygote的子进程

    runSelectLoopMode(); /* Zygote进程进入无限循环,不再返回,执行孵化工作。*/

    2.2.4 systemserver

        上面的startSystemServer()函数中,做了如下的调用:

        startSystemServer()

        à Zygote.forkSystemServer()

        à 父进程直接返回true,子进程执行handleSystemServerProcess()

            à RuntimeInit.zygoteInit(parsedArgs.remainingArgs);

            /*

             * Pass the remaining arguments to SystemServer.

             * "--nice-name=system_server com.android.server.SystemServer"

             */

    /*上面这个RuntimeInit包定于于文件frameworks\base\core\java\com\android\internal\os\RuntimeInit.java */

    à invokeStaticMain(startClass, startArgs)

    /* 通过该函数调用执行startClass类的main函数,带参数 system_server 。*/

    à ZygoteInit.MethodAndArgsCaller(m, argv)

    /* 得到包com.android.server.SystemServer的main()函数。然后执行。*/

       

        下面就开始调用到com.android.server.SystemServer类的main函数,源码位于:

        frameworks/base/services/java/com/android/server/SystemServer.java 

       

    This method is called from Zygote to initialize the system. This will cause the nativeservices (SurfaceFlinger, AudioFlinger, etc..) to be started. After that it will call backup into init2() to start the Android services.

        // main--->init1(system_init)--->init2(systemThread)

    native public static void init1(String[] args);

           

    public static void main(String[] args) {

            ...

    System.loadLibrary("android_servers");// libandroid_servers.so是由目录frameworks/base/services/jni下的源码编译所得

    init1(args); // init1实际上是一个jni本地接口,位于文件frameworks\base\services\jni\com_android_server_SystemServer.cpp文件中system_init()函数

    }

       

    init1接口位于com_android_server_SystemServer.cpp中:

    extern "C" int system_init();

    static void android_server_SystemServer_init1(JNIEnv* env, jobject clazz){

        system_init();

    }

    static JNINativeMethod gMethods[] = {

        /* name, signature, funcPtr */

    {"init1","([Ljava/lang/String;)V",(void*)

    android_server_SystemServer_init1 },

    };

    而函数system_init()位于文件

    frameworks\base\cmds\system_server\library\System_init.cpp

    extern "C" status_t system_init()

    {

        …

        char propBuf[PROPERTY_VALUE_MAX];

        property_get("system_init.startsurfaceflinger", propBuf, "1");

        if (strcmp(propBuf, "1") == 0) {

            // Start the SurfaceFlinger

            SurfaceFlinger::instantiate();

        }

        if (!proc->supportsProcesses()) {

           

            // Start the AudioFlinger

            AudioFlinger::instantiate();

           

            // Start the media playback service

            MediaPlayerService::instantiate();

           

            // Start the camera service

            CameraService::instantiate();

           

            // Start the audio policy service

            AudioPolicyService::instantiate();

           

            //start appc service

            APPCService::instantiate();

        }

        …

        AndroidRuntime* runtime = AndroidRuntime::getRuntime();

        runtime->callStatic("com/android/server/SystemServer", "init2");

        // 执行com.android.server.SystemServer类的init2函数

    }

    com.android.server.SystemServer包的init2函数开启一个ServerThread线程:

    public static final void init2() {

        Thread thr = new ServerThread();

        thr.setName("android.server.ServerThread");

        thr.start();

    }

    ServerThread线程的run函数会启动系统中绝大部分的android service,并最后进入Loop.loop(),,,,(SystemServer.java)

    public void run() {

        …

        // Critical services...

        try {

            …

            Slog.i(TAG, "Power Manager");

            power = new PowerManagerService();

            ServiceManager.addService(Context.POWER_SERVICE, power);

           

            Slog.i(TAG, "Activity Manager");

            context = ActivityManagerService.main(factoryTest);

           

            …

            Slog.i(TAG, "Package Manager");

            pm = PackageManagerService.main(context,

                    factoryTest != SystemServer.FACTORY_TEST_OFF);

           

            …

            Slog.i(TAG, "Content Manager");

            ContentService.main(context,

                 factoryTest == SystemServer.FACTORY_TEST_LOW_LEVEL);

           

            …

            Slog.i(TAG, "Battery Service");

            battery = new BatteryService(context);

            ServiceManager.addService("battery", battery);

            …

            其余addservice的过程类似,只是启动的不同服务罢了,后面还启动了很多

    服务,如:Lights、Vibrator、Alarm、Sensor、Bluetooth、Input Method、NetStat、NetworkManagement、Connectivity、Mount、Notification、Audio等。 在这些服务都启动完了之后。

            …

            …

            … // run()函数的后半部分

            // It is now time to start up the app processes...

            使用xxx.systemReady()通知各个服务,系统已经就绪。

            …

            ((ActivityManagerService)ActivityManagerNative.getDefault())

                    .systemReady(new Runnable() {

             public void run() {

                    …

                    if (batteryF != null) batteryF.systemReady();

                    if (connectivityF != null) connectivityF.systemReady();

                    if (dockF != null) dockF.systemReady();

                    if (uiModeF != null) uiModeF.systemReady();

                    if (recognitionF != null) recognitionF.systemReady();

                    Watchdog.getInstance().start();

                    …

                }

            });

            …

            Looper.loop(); // Run the message queue in this thread。

            …

    }

    2.2.5 home界面启动

        Home在

    ((ActivityManagerService)ActivityManagerNative.getDefault()).systemReady(.)

    函数调用的过程中启动,其中systemReady()的参数是一段callback代码,如上面灰色显示的部分。

    这个函数的实现部分在文件:ActivityManagerService.java中。

    public void systemReady(final Runnable goingCallback) {

        …

       

        if (mSystemReady) {

            if (goingCallback != null) goingCallback.run();

            return; // 执行回调

        }

        …

        resumeTopActivityLocked(null);

    }

    private final boolean resumeTopActivityLocked(HistoryRecord prev) {

        …

        if (next == null) {

            // There are no more activities!  Let's just start up the

            // Launcher...

            return startHomeActivityLocked();

        }

        …

    }

    private boolean startHomeActivityLocked() {

        …

        if (aInfo != null) {

            …

            if (app == null || app.instrumentationClass == null) {

                    intent.setFlags(intent.getFlags() |

                                    Intent.FLAG_ACTIVITY_NEW_TASK);

                    startActivityLocked(null, intent, null, null, 0, aInfo,

                            null, null, 0, 0, 0, false, false); // 这里启动home

             }

         }

        …

    }

    三、android启动图示

    参考网址:

    1.        http://blog.csdn.net/cuijianzhongswust/article/details/6612624

    2.        http://blog.csdn.net/sustzombie/article/details/6659622

    3.        http://www.docin.com/p-191202348.html

    4.        http://blog.csdn.net/maxleng/article/details/5508372

    本文完。

  • 相关阅读:
    docker相关资源监控采集方法整理
    三节课MINI计划第四周
    三节课MINI计划第五周
    三节课MINI计划第四周
    三节课MINI计划第三周
    三节课MINI计划第二周
    Bilibili用户需求分析报告
    三节课MINI计划第一周
    《产品思维30讲 》学习笔记
    PHP-FPM未授权访问漏洞
  • 原文地址:https://www.cnblogs.com/linucos/p/2513760.html
Copyright © 2020-2023  润新知