• Android Zygote进程启动分析


    dvm,app进程,linux进程三者关系

    DVM指 dalivk 的虚拟机。每一个 Android 应用程序都在它自己的进程中运行,都拥有一个独立的 Dalvik 虚拟机实例。而每一个 DVM 都是在 Linux 中的一个进程,所以说可以认为是同一个概念

    Zygote进程与app进程关系

    Zygote是java层的进程即它也拥有一个独立的Dalvik 虚拟机实例,它是被linux层的第一个用户空间Init进程所启动的,它的主要作用就是用来孵化app进程系统进程
    fork一个app进程,是通过ActivityManagerService类向Zygote发出fork命令,ActivityManagerService是在系统进程,但是Zygote处于自己的进程中,它们之间的通信没有采用binder机制,而是采用了socket机制,因此我们可以把Zygote称为一个孵化server,ActivityMamagerService称为一个client

    下面的图描述了上面的过程

    涉及到的类

    我们先来梳理这个过程中使用到的类,并且这些类是做什么的

     

    以server和client2个维度来归纳这些类

    Zygote进程启动分析

    大家都知道android系统的Zygote进程是所有的android进程的父进程,包括SystemServer和各种应用进程都是通过Zygote进程fork出来的。Zygote(孵化)进程相当于是android系统的根进程,后面所有的进程都是通过这个进程fork出来的,而Zygote进程则是通过linux系统的init进程启动的,也就是说,android系统中各种进程的启动方式

    init进程 –> Zygote进程 –> SystemServer进程 –>各种应用进程

    init进程:linux的根进程,android系统是基于linux系统的,因此可以算作是整个android操作系统的第一个进程;

    Zygote进程:android系统的根进程,主要作用:可以作用Zygote进程fork出SystemServer进程和各种应用进程;

    SystemService进程:主要是在这个进程中启动系统的各项服务,比如ActivityManagerService,PackageManagerService,WindowManagerService服务等等;

    各种应用进程:启动自己编写的客户端应用时,一般都是重新启动一个应用进程,有自己的虚拟机与运行环境;

    Zygote就是进程init启动起来的。Android中所有应用程序进程,以及运行系统关键服务的System进程都是由Zygote创建的。它通过复制自身的形式创建其它进程。Zygote在启动时会在内部创建一个虚拟机实例,因此,通过复制Zygote得到的其它应用程序进程和System进程都可以快速地在内部获得一个虚拟机地拷贝。Zygote启动完成后就立即将System进程启动,以便各种关键服务被启动运行 

    service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
        class main
        socket zygote stream 660 root system
        onrestart write /sys/android_power/request_state wake
        onrestart write /sys/power/state on
        onrestart restart media
        onrestart restart netd

    写到zygote是由init进程解析init.rc(以init.zygote32.rc为例)文件启动的,启动的过程传入了四个参数。分别是-Xzygote/system/bin--zygote--start-system-server

    • 虚拟机参数以"-"开头,上边的"-Xzygote"即为虚拟机参数,在启动虚拟机时传递给虚拟机
    • 运行目录即app_process可执行程序所在的目录,一般是在/system/bin
    • 参数以"--"开头,"--zygote"表示启动zygote进程,"-start-system-server-"表示启动system server"--application"表示以普通进程方式执行java代码。
    • java类,将要执行的java类,必须有一个静态方法。但是如果参数中有"--zygote"时将会忽略该参数,固定的执行zygoteInit类。

    Zygote进程能够重启的地方:

    • servicemanager进程被杀; (onresart)
    • surfaceflinger进程被杀; (onresart)
    • Zygote进程自己被杀; (oneshot=false)
    • system_server进程被杀; (waitpid)

    Zygote是由init进程通过解析init.zygote.rc文件而创建的,zygote所对应的可执行程序app_process,所对应的源文件是App_main.cpp,进程名为zygote。

     前边分析中可以知道zygote要执行的程序是system/bin/app_process,它的源代码位于frameworks/base/cmds/app_process/App_main.cpp文件中

    从App_main()开始,Zygote启动过程的函数调用类大致流程如下:

    App_main.main

    http://androidxref.com/6.0.1_r10/xref/frameworks/base/cmds/app_process/app_main.cpp

    int main(int argc, char* const argv[])
    {
        //传到的参数argv为“-Xzygote /system/bin --zygote --start-system-server”
        AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
        argc--; argv++; //忽略第一个参数
    
        int i;
        for (i = 0; i < argc; i++) {
            if (argv[i][0] != '-') {
                break;
            }
            if (argv[i][1] == '-' && argv[i][2] == 0) {
                ++i;
                break;
            }
            runtime.addOption(strdup(argv[i]));
        }
        //参数解析
        bool zygote = false;
        bool startSystemServer = false;
        bool application = false;
        String8 niceName;
        String8 className;
        ++i;
        while (i < argc) {
            const char* arg = argv[i++];
            if (strcmp(arg, "--zygote") == 0) {
                zygote = true;
                //对于64位系统nice_name为zygote64; 32位系统为zygote
                niceName = ZYGOTE_NICE_NAME;
            } else if (strcmp(arg, "--start-system-server") == 0) {
                startSystemServer = true;
            } else if (strcmp(arg, "--application") == 0) {
                application = true;
            } else if (strncmp(arg, "--nice-name=", 12) == 0) {
                niceName.setTo(arg + 12);
            } else if (strncmp(arg, "--", 2) != 0) {
                className.setTo(arg);
                break;
            } else {
                --i;
                break;
            }
        }
        Vector<String8> args;
        if (!className.isEmpty()) {
            // 运行application或tool程序
            args.add(application ? String8("application") : String8("tool"));
            runtime.setClassNameAndArgs(className, argc - i, argv + i);
        } else {
            //进入zygote模式,创建 /data/dalvik-cache路径
            maybeCreateDalvikCache();
            if (startSystemServer) {
                args.add(String8("start-system-server"));
            }
            char prop[PROP_VALUE_MAX];
            if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {
                return 11;
            }
            String8 abiFlag("--abi-list=");
            abiFlag.append(prop);
            args.add(abiFlag);
    
            for (; i < argc; ++i) {
                args.add(String8(argv[i]));
            }
        }
    
        //设置进程名
        if (!niceName.isEmpty()) {
            runtime.setArgv0(niceName.string());
            set_process_name(niceName.string());
        }
        if (zygote) {
            // 启动AppRuntime
            runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
        } else if (className) {
            runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
        } else {
            //没有指定类名或zygote,参数错误
            return 10;
        }
    }

     AndroidRuntime.start

    http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/AndroidRuntime.cpp

    void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
    {
        static const String8 startSystemServer("start-system-server");
    
        for (size_t i = 0; i < options.size(); ++i) {
            if (options[i] == startSystemServer) {
               const int LOG_BOOT_PROGRESS_START = 3000;
            }
        }
        const char* rootDir = getenv("ANDROID_ROOT");
        if (rootDir == NULL) {
            rootDir = "/system";
            if (!hasDir("/system")) {
                return;
            }
            setenv("ANDROID_ROOT", rootDir, 1);
         //主要是用来完成环境变量ANDROID_ROOT的设置,调用了getenv()方法和setenv()方法。
         //通过这段代码,环境变量ANDROID_ROOT的值被设置成了/system
        }
        JniInvocation jni_invocation;
        jni_invocation.Init(NULL);//通过jni_invocation.Init(NULL)完成jni接口的初始化
        JNIEnv* env;
        //调用startVm启动虚拟机,在startVm方法中,定义了虚拟机的一系列参数
        //通过property_get()方法来进行参数的设置。
        if (startVm(&mJavaVM, &env, zygote) != 0) {
            return;
        }
        onVmCreated(env);
        // JNI方法注册
        if (startReg(env) < 0) {
            return;
        }
        //定义了三个变量,定义参数的目的是将AndroidRuntime::start的两个参数传入到ZygoteInit的main方法
        jclass stringClass;
        jobjectArray strArray;
        jstring classNameStr;
    
        //对三个变量分别进行了赋值
        
        //等价 strArray= new String[options.size() + 1];
        stringClass = env->FindClass("java/lang/String");
        strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
    
        //等价 strArray[0] = "com.android.internal.os.ZygoteInit"
        classNameStr = env->NewStringUTF(className);
        env->SetObjectArrayElement(strArray, 0, classNameStr);
    
        //等价 strArray[1] = "start-system-server";
        // strArray[2] = "--abi-list=xxx";
        //其中xxx为系统响应的cpu架构类型,比如arm64-v8a.
        for (size_t i = 0; i < options.size(); ++i) {
            jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
            env->SetObjectArrayElement(strArray, i + 1, optionsStr);
        }
    
        //将"com.android.internal.os.ZygoteInit"转换为"com/android/internal/os/ZygoteInit"
        char* slashClassName = toSlashClassName(className);
        jclass startClass = env->FindClass(slashClassName);
        if (startClass == NULL) {
            ...
        } else {
            jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
                "([Ljava/lang/String;)V");
            // 调用ZygoteInit.main()方法
            env->CallStaticVoidMethod(startClass, startMeth, strArray);
        }
        //释放相应对象的内存空间
        free(slashClassName);
        mJavaVM->DetachCurrentThread();
        mJavaVM->DestroyJavaVM();
    }

    start()是通过JNI回调java层的方法,它主要的目的是执行"com/android/internal/os/ZygoteInit"中main()方法,即frameworks/base/core/java/com/android/internal/os/ZygoteInit.java中的main()函数。

    创建虚拟机

    进程内创建一个虚拟机实例,并注册一系列JNI方法。
    http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/AndroidRuntime.cpp

    /* start the virtual machine. */
    startVM(&mJavaVM, &env);
    /* Register android functions. */
    startReg(env);

    创建Java虚拟机方法的主要篇幅是关于虚拟机参数的设置,下面只列举部分在调试优化过程中常用参数。

    int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
    {
        // JNI检测功能,用于native层调用jni函数时进行常规检测,比较弱字符串格式是否符合要求,资源是否正确释放。该功能一般用于早期系统调试或手机Eng版,对于User版往往不会开启,引用该功能比较消耗系统CPU资源,降低系统性能。
        bool checkJni = false;
        property_get("dalvik.vm.checkjni", propBuf, "");
        if (strcmp(propBuf, "true") == 0) {
            checkJni = true;
        } else if (strcmp(propBuf, "false") != 0) {
            property_get("ro.kernel.android.checkjni", propBuf, "");
            if (propBuf[0] == '1') {
                checkJni = true;
            }
        }
        if (checkJni) {
            addOption("-Xcheck:jni");
        }
    
        //虚拟机产生的trace文件,主要用于分析系统问题,路径默认为/data/anr/traces.txt
        parseRuntimeOption("dalvik.vm.stack-trace-file", stackTraceFileBuf, "-Xstacktracefile:");
    
        //对于不同的软硬件环境,这些参数往往需要调整、优化,从而使系统达到最佳性能
        parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
        parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");
        parseRuntimeOption("dalvik.vm.heapgrowthlimit", heapgrowthlimitOptsBuf, "-XX:HeapGrowthLimit=");
        parseRuntimeOption("dalvik.vm.heapminfree", heapminfreeOptsBuf, "-XX:HeapMinFree=");
        parseRuntimeOption("dalvik.vm.heapmaxfree", heapmaxfreeOptsBuf, "-XX:HeapMaxFree=");
        parseRuntimeOption("dalvik.vm.heaptargetutilization",
                           heaptargetutilizationOptsBuf, "-XX:HeapTargetUtilization=");
        ...
    
        //preloaded-classes文件内容是由WritePreloadedClassFile.java生成的,
        //在ZygoteInit类中会预加载工作将其中的classes提前加载到内存,以提高系统性能
        if (!hasFile("/system/etc/preloaded-classes")) {
            return -1;
        }
    
        //初始化虚拟机
        if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
            ALOGE("JNI_CreateJavaVM failed
    ");
            return -1;
        }
    }

    在startVm方法中,定义了虚拟机的一系列参数。通过property_get()方法来进行参数的设置。

    startReg

    http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/AndroidRuntime.cpp

    int AndroidRuntime::startReg(JNIEnv* env)
    {
        //设置线程创建方法为javaCreateThreadEt
        androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
    
        env->PushLocalFrame(200);
        //进程NI方法的注册
        if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
            env->PopLocalFrame(NULL);
            return -1;
        }
        env->PopLocalFrame(NULL);
        return 0;
    }
    startReg注册JNI的代码

    androidSetCreateThreadFunc虚拟机启动后startReg()过程,会设置线程创建函数指针gCreateThreadFn指向javaCreateThreadEtc.

    ZygoteInit.main

    接下来执行“com.android.internal.os.ZygoteInit”Java类的main方法继续执行启动。

    Zygote进程启动后,ZygoteInit类的main方法会被执行

    http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

    public static void main(String argv[]) {
        try {
            // Start profiling the zygote initialization.
            SamplingProfilerIntegration.start();
    
            boolean startSystemServer = false;
            String socketName = "zygote";
            String abiList = null;
            for (int i = 1; i < argv.length; i++) {
                if ("start-system-server".equals(argv[i])) {
                    startSystemServer = true;
                } else if (argv[i].startsWith(ABI_LIST_ARG)) {
                    abiList = argv[i].substring(ABI_LIST_ARG.length());
                } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
                    socketName = argv[i].substring(SOCKET_NAME_ARG.length());
                } else {
                    throw new RuntimeException("Unknown command line argument: " + argv[i]);
                }
            }
    
            if (abiList == null) {
                throw new RuntimeException("No ABI list supplied.");
            }
            /*启动servier socket*/
            registerZygoteSocket(socketName);
            EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
                SystemClock.uptimeMillis());
        //预加载资源,预加载耗时的类
            preload();
            EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
                SystemClock.uptimeMillis());
    
            // Finish profiling the zygote initialization.
            SamplingProfilerIntegration.writeZygoteSnapshot();
    
            // Do an initial gc to clean up after startup
            gc();
    
            // Disable tracing so that forked processes do not inherit stale tracing tags from
            // Zygote.
            Trace.setTracingEnabled(false);
    
            if (startSystemServer) {
                /*启动系统服务*/
                startSystemServer(abiList, socketName);
            }
    
            Log.i(TAG, "Accepting command socket connections");
            runSelectLoop(abiList);
    
            closeServerSocket();
        } catch (MethodAndArgsCaller caller) {
            //这行代码很重要
            caller.run();
        } catch (RuntimeException ex) {
            Log.e(TAG, "Zygote died with exception", ex);
            closeServerSocket();
            throw ex;
        }
    }

    上面代码主要做了下面的事情:

    • registerZygoteSocket(socketName)启动一个ServerSocket
    • preload()预加载资源,预加载耗时的类
    • startSystemServer(abiList, socketName)启动系统服务,并且fork系统进程
    • runSelectLoop(abiList)监听client socket的连接

    来看下registerZygoteSocket(socketName)方法

    private static void registerZygoteSocket(String socketName) {
        if (sServerSocket == null) {
            int fileDesc;
            final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;
            try {
                String env = System.getenv(fullSocketName);
                fileDesc = Integer.parseInt(env);
            } catch (RuntimeException ex) {
                throw new RuntimeException(fullSocketName + " unset or invalid", ex);
            }
    
            try {
                sServerSocket = new LocalServerSocket(
                        createFileDescriptor(fileDesc));
            } catch (IOException ex) {
                throw new RuntimeException(
                        "Error binding to local socket '" + fileDesc + "'", ex);
            }
        }
    }

    代码很简单,再来看下preload()方法

      static void preload() {
        Log.d(TAG, "begin preload");
        preloadClasses();
        preloadResources();
        preloadOpenGL();
        preloadSharedLibraries();
        // Ask the WebViewFactory to do any initialization that must run in the zygote process,
        // for memory sharing purposes.
        WebViewFactory.prepareWebViewInZygote();
        Log.d(TAG, "end preload");
    }

    这其中:
    preloadClasses()用于初始化Zygote中需要的class类;
    preloadResources()用于初始化系统资源;
    preloadOpenGL()用于初始化OpenGL;
    preloadSharedLibraries()用于初始化系统libraries;
    preloadTextResources()用于初始化文字资源;
    prepareWebViewInZygote()用于初始化webview;

    我们简单看下preloadClasses()preloadResources()所做的事情

    private static void preloadClasses() {
            //.......省略代码
            is = new FileInputStream(PRELOADED_CLASSES);
            // ......省略代码
            BufferedReader br
                = new BufferedReader(new InputStreamReader(is), 256);
    
            int count = 0;
            String line;
            while ((line = br.readLine()) != null) {
                // Skip comments and blank lines.
                line = line.trim();
                if (line.startsWith("#") || line.equals("")) {
                    continue;
                }
    
                try {
                    if (false) {
                        Log.v(TAG, "Preloading " + line + "...");
                    }
                    Class.forName(line);
                    if (Debug.getGlobalAllocSize() > PRELOAD_GC_THRESHOLD) {
                        if (false) {
                            Log.v(TAG,
                                " GC at " + Debug.getGlobalAllocSize());
                        }
                        System.gc();
                        runtime.runFinalizationSync();
                        Debug.resetGlobalAllocSize();
                    }
                    count++;
                    //......省略代码
    
                }
    
             //......省略代码
    }
    
    private static void preloadResources() {
        final VMRuntime runtime = VMRuntime.getRuntime();
    
        Debug.startAllocCounting();
        try {
            System.gc();
            runtime.runFinalizationSync();
            mResources = Resources.getSystem();
            mResources.startPreloading();
            if (PRELOAD_RESOURCES) {
                Log.i(TAG, "Preloading resources...");
    
                long startTime = SystemClock.uptimeMillis();
                TypedArray ar = mResources.obtainTypedArray(
                        com.android.internal.R.array.preloaded_drawables);
                int N = preloadDrawables(runtime, ar);
                ar.recycle();
                Log.i(TAG, "...preloaded " + N + " resources in "
                        + (SystemClock.uptimeMillis()-startTime) + "ms.");
    
                startTime = SystemClock.uptimeMillis();
                ar = mResources.obtainTypedArray(
                        com.android.internal.R.array.preloaded_color_state_lists);
                N = preloadColorStateLists(runtime, ar);
                ar.recycle();
                Log.i(TAG, "...preloaded " + N + " resources in "
                        + (SystemClock.uptimeMillis()-startTime) + "ms.");
            }
            mResources.finishPreloading();
        } catch (RuntimeException e) {
            Log.w(TAG, "Failure preloading resources", e);
        } finally {
            Debug.stopAllocCounting();
        }
    }

    preloadClasses方法所做的事情是从"/system/etc/preloaded-classes"文件种把预加载的类加载到虚拟机中

    然后调用startSystemServer(abiList, socket)

    private static boolean startSystemServer(String abiList, String socketName)
                throws MethodAndArgsCaller, RuntimeException {
            long capabilities = posixCapabilitiesAsBits(
                OsConstants.CAP_BLOCK_SUSPEND,
                OsConstants.CAP_KILL,
                OsConstants.CAP_NET_ADMIN,
                OsConstants.CAP_NET_BIND_SERVICE,
                OsConstants.CAP_NET_BROADCAST,
                OsConstants.CAP_NET_RAW,
                OsConstants.CAP_SYS_MODULE,
                OsConstants.CAP_SYS_NICE,
                OsConstants.CAP_SYS_RESOURCE,
                OsConstants.CAP_SYS_TIME,
                OsConstants.CAP_SYS_TTY_CONFIG
            );
            /* Hardcoded command line to start the system server */
            String args[] = {
                "--setuid=1000",
                "--setgid=1000",
                "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1032,3001,3002,3003,3006,3007",
                "--capabilities=" + capabilities + "," + capabilities,
                "--nice-name=system_server",
                "--runtime-args",
                "com.android.server.SystemServer",
            };
            ZygoteConnection.Arguments parsedArgs = null;
    
            int pid;
    
            try {
                parsedArgs = new ZygoteConnection.Arguments(args);
                ZygoteConnection.applyDebuggerSystemProperty(parsedArgs);
                ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs);
    
                /* Request to fork the system server process */
                pid = Zygote.forkSystemServer(
                        parsedArgs.uid, parsedArgs.gid,
                        parsedArgs.gids,
                        parsedArgs.debugFlags,
                        null,
                        parsedArgs.permittedCapabilities,
                        parsedArgs.effectiveCapabilities);
            } catch (IllegalArgumentException ex) {
                throw new RuntimeException(ex);
            }
    
            /* For child process */
            if (pid == 0) {
                if (hasSecondZygote(abiList)) {
                    waitForSecondaryZygote(socketName);
                }
    
                handleSystemServerProcess(parsedArgs);
            }
    
            return true;
        }

    可以看到这段逻辑的执行逻辑就是通过Zygote fork出SystemServer进程。

    在来看runSelectLoop方法

     private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {
        ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
        ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
        FileDescriptor[] fdArray = new FileDescriptor[4];
    
        fds.add(sServerSocket.getFileDescriptor());
        peers.add(null);
    
        int loopCount = GC_LOOP_COUNT;
        while (true) {
            int index;
    
            /*
             * Call gc() before we block in select().
             * It's work that has to be done anyway, and it's better
             * to avoid making every child do it.  It will also
             * madvise() any free memory as a side-effect.
             *
             * Don't call it every time, because walking the entire
             * heap is a lot of overhead to free a few hundred bytes.
             */
            if (loopCount <= 0) {
                gc();
                loopCount = GC_LOOP_COUNT;
            } else {
                loopCount--;
            }
    
    
            try {
                fdArray = fds.toArray(fdArray);
                index = selectReadable(fdArray);
            } catch (IOException ex) {
                throw new RuntimeException("Error in select()", ex);
            }
    
            if (index < 0) {
                throw new RuntimeException("Error in select()");
            } else if (index == 0) {
                ZygoteConnection newPeer = acceptCommandPeer(abiList);
                peers.add(newPeer);
                fds.add(newPeer.getFileDescriptor());
            } else {
                boolean done;
                /*开始读取client发出的命令*/
                done = peers.get(index).runOnce();
    
                if (done) {
                    peers.remove(index);
                    fds.remove(index);
                }
            }
        }
    }

    它所做的事情是:
    - 监听client的socket连接
    - 发现有连接则建立一个ZygoteConnection对象
    - client发送命令,则找到相应的ZygoteConnection对象,并且调用该对象的runOnce方法,来处理client发送的命令
    - ZygoteConnection对象处理完毕,则从列表中移除

    总结:
    Zygote进程mian方法主要执行逻辑:

    初始化DDMS;

    注册Zygote进程的socket通讯;

    初始化Zygote中的各种类,资源文件,OpenGL,类库,Text资源等等;

    初始化完成之后fork出SystemServer进程;fork出SystemServer进程之后,关闭socket连接;

     

  • 相关阅读:
    MySQL严格模式总结
    python筛选关键字---error
    将pem证书转换成p12格式证书
    ## 游戏网关源码赏析
    pid获取
    顺序io_磁盘队列
    nsq源码阅读3_编译nsqd
    nsq源码阅读2_核心数据结构
    nsq源码阅读1_目录结构
    如何设计Mqtt的Retain?
  • 原文地址:https://www.cnblogs.com/mingfeng002/p/10384613.html
Copyright © 2020-2023  润新知