Android-Framework(三)-开机启动Zygote进程

文章内容

一、Zygote 要回答的三个问题

在 Android 设备上执行 adb shell ps -A -o USER,PID,PPID,NAME | grep -E 'zygote|system_server',你会看到 zygote 通常只有一个实例,PPID 是 1;而任何一个第三方应用进程的 PPID 都是这个 zygote。再看进程名,zygote 和 system_server 都不是用户空间自己 fork 出来的普通进程。

这三个现象说明了三个问题:

  1. app_process 本来是个 native 程序,它凭什么能执行 Java 代码?也就是 init.rc 里那条 service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server 被 init 用 fork+execve 拉起之后,这个纯 C++ 可执行文件是怎么跨进 Java 世界的?
  2. Zygote 为什么要一直活着、一直等?它靠什么机制「类进程」一样持续接收请求而不是自己退出?
  3. 它孵化一个应用进程为什么那么快?为什么不把每个应用都让 init 重新从头启动一遍?

Android 6.0 的答案可以压成一条主链:app_process 的 main() 创建 AndroidRuntime 并调 start("com.android.internal.os.ZygoteInit") → startVm() 启动 Java 虚拟机、startReg() 注册 JNI → CallStaticVoidMethod 进入 ZygoteInit.main() → 注册 zygote socket → preload() 预加载类/资源/so → runSelectLoop() 进入死循环,用 Os.poll 监听 socket,收到命令就 acceptCommandPeer 新建 ZygoteConnection,再 runOnce() 读参数并 forkAndSpecialize 出一个新进程。

Zygote 启动主链:app_process main() → AndroidRuntime::start → startVm/startReg → CallStaticVoidMethod 进入 ZygoteInit.main() → registerZygoteSocket → preload → runSelectLoop(Os.poll) → acceptCommandPeer 新建 ZygoteConnection → runOnce 读参数 → forkAndSpecialize 孵出应用进程

应用启动快的原因不是「启动快」,而是「fork 时继承原有进程资源」。

二、全局概览:init → Zygote → 应用进程的边界

先把参与者搞清楚。Zygote 不是一个孤立的进程:左边是 native 世界(C++、AndroidRuntime、JVM 的启动),右边是 Java 世界(ZygoteInit 和它的子进程);
上面是它的父进程 init(PID 1),下面是它 fork 出来的 system_server 和所有应用进程。

开机到 Zygote 到应用进程的全局地图:init→fork+execve→app_process(Zygote)→内部(AndroidRuntime/JavaVM→ZygoteInit→registerSocket→preload→runSelectLoop)→fork 出 system_server 与各应用进程

这条边界决定了 Zygote 的三种身份:

  • ** init 的子进程**、又是绝大多数应用进程(Java 应用与 system_server)的祖先。init 用 fork+execve 把它拉起来,此后它不再退出。(init 自己还会用 fork+execve 直接拉 native 服务,那些并不是 zygote 的后代。)
  • ** Java 虚拟机宿主**。app_process 启动时 startVm 把 ART 装进自己的进程(Android 6.0 已无 Dalvik 可选,DVM 只是历史上的说法),之后每一个 fork 出来的应用进程都直接继承这份已经跑起来的虚拟机实例,不用再启动一遍。
  • 守护进程。它靠一个 zygote socket 接收来自 AMS 的请求,除此之外不再干别的活。

三、Zygote 创建:app_process 的 main() 方法

app_process 的入口 main() 第一件事不是启动虚拟机,而是解析自己的命令行参数,并且判断自己该以哪种模式运行:

c
// 代码位置 /frameworks/base/cmds/app_process/app_main.cpp 第 186—316 行// 当前应用的可执行文件入口int main(int argc, char* const argv[]){// 禁止子进程获得新的特权位,防止 setuid 提权    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {// 旧内核会返回 EINVAL,不属于致命错误,忽略        if (errno != EINVAL) {// 其余错误直接终止            LOG_ALWAYS_FATAL("PR_SET_NO_NEW_PRIVS failed: %s", strerror(errno));            return 12;        }    }// 创建 AppRuntime 子类,参数块从 argv[0] 到最后一个参数结尾    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));// 忽略 argv[0](argv[0] 是可执行文件自身,不是业务参数)    argc--;    argv++;// 逐个读参数,遇到非 '-' 开头或 '--' 就停,之前的是给虚拟机的选项    int i;    for (i = 0; i < argc; i++) {        if (argv[i][0] != '-') {            break;        }        if (argv[i][1] == '-' && argv[i][2] == 0) {            ++i; // Skip --.            break;        }        runtime.addOption(strdup(argv[i]));    }// 解析业务参数:是否 zygote 模式、是否启动 system_server 等    bool zygote = false;    bool startSystemServer = false;    bool application = false;    String8 niceName;    String8 className;// 跳过未使用的「父目录」参数    ++i;    while (i < argc) {        const char* arg = argv[i++];// 命中 --zygote,进入 zygote 模式并设置进程名        if (strcmp(arg, "--zygote") == 0) {            zygote = true;            niceName = ZYGOTE_NICE_NAME;// 命中 --start-system-server,则 zygote 起来后要继续启动 system_server        } else if (strcmp(arg, "--start-system-server") == 0) {            startSystemServer = true;// 命中 --application,是独立应用模式        } else if (strcmp(arg, "--application") == 0) {            application = true;// 命中 --nice-name=,自定义进程名        } 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;        }    }// 组装要传给 Java 入口 main() 的参数表(对应 ZygoteInit.main 收到的 argv)    Vector<String8> args;    if (!className.isEmpty()) {// 非 zygote 模式:只告诉 RuntimeInit 是 application 还是 tool        args.add(application ? String8("application") : String8("tool"));        runtime.setClassNameAndArgs(className, argc - i, argv + i);    } else {// zygote 模式:先创建 dalvik-cache 目录        maybeCreateDalvikCache();// 需要启动 system_server 就把这个标志收进参数表        if (startSystemServer) {            args.add(String8("start-system-server"));        }// 关键:从系统属性读出本 zygote 支持的 ABI 列表,拼成 --abi-list=// 这就是 ZygoteInit.main() 里 abiList 变量的真正来源        char prop[PROP_VALUE_MAX];        if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {            LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",                ABI_LIST_PROPERTY);            return 11;        }        String8 abiFlag("--abi-list=");        abiFlag.append(prop);        args.add(abiFlag);// zygote 模式下,剩余参数原样透传给 ZygoteInit.main()        for (; i < argc; ++i) {            args.add(String8(argv[i]));        }    }// 设置进程名,方便 ps 里看到    if (!niceName.isEmpty()) {        runtime.setArgv0(niceName.string());        set_process_name(niceName.string());    }// zygote 模式:把 Java 入口类固定为 ZygoteInit    if (zygote) {        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);// 普通工具/应用模式:入口是 RuntimeInit    } else if (className) {        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);// 两者都没有则报错退出    } else {        fprintf(stderr, "Error: no class name or --zygote supplied.\n");        app_usage();        LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");        return 10;    }}

这里有三点要说清楚。

  • 第一,argv[0] 是程序自身(即 /system/bin/app_process64),真正的业务参数从 argv[1] 起,所以代码里先 argc--; argv++;。
  • 第二,runtime.start 的第三个参数 zygote(布尔)决定了 AndroidRuntime 是作为「zygote 模式」启动,还是普通应用模式启动——zygote 参数主要在 startVm 内部有 if (zygote) {...} 专属分支;而 onVmCreated 走哪条路是靠 AppRuntime 自己记录的 mClassName 是否为空来判断的(zygote 模式下 mClassName 恒为空,所以效果上等价,但判断依据是 mClassName 而不是 zygote 本身)。
  • 第三,computeArgBlockSize 估算的是从 argv[0] 到最后一个参数结尾的字节数,这个参数块随后会被 setArgv0 覆写进程名,所以不能随便缩短。

还有开头那句 init.rc 命令里的 -Xzygote:它以 - 开头但不是 --zygote,所以在 app_process 的第一个参数解析循环里会被当成「给虚拟机的选项」,用 runtime.addOption 收进 VM 参数列表,而不是被后面判断 --zygote/--start-system-server 的那个循环处理。也就是说,-Xzygote 从头到尾都是一个 JVM 启动参数,startVm() 内部专门有一段 if (zygote) {...} 分支来处理这个模式——它和 --zygote(决定 native 层走 ZygoteInit 还是 RuntimeInit)是两套独立的开关,只是名字很像。

AppRuntime 继承自 AndroidRuntime,覆写了 onVmCreated(预缓存要启动的类)和 onZygoteInit(起 binder 线程池)等回调。但真正的启动动作发生在 AndroidRuntime::start。

四、native → Java:AndroidRuntime::start 启动虚拟机

runtime.start("com.android.internal.os.ZygoteInit", args, true) 是跨进 Java 世界的门。AndroidRuntime::start 做四件事:启动虚拟机、触发 onVmCreated、注册 JNI,最后用反射调用 ZygoteInit.main。

cpp
// 代码位置 /frameworks/base/core/jni/AndroidRuntime.cpp 第 1007—1110 行// Android 运行时启动入口,className 是要进入的 Java 入口类void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote){// 环境里取出根目录,默认 /system    const char* rootDir = getenv("ANDROID_ROOT");    if (rootDir == NULL) {        rootDir = "/system";        ...    }// 初始化 JNI 调用层    JniInvocation jni_invocation;    jni_invocation.Init(NULL);    JNIEnv* env;// 关键第一步:启动 Java 虚拟机,之后才有 JNIEnv    if (startVm(&mJavaVM, &env, zygote) != 0) {        return;    }// 触发 onVmCreated 回调,AppRuntime 在这里预缓存入口类    onVmCreated(env);// 关键第二步:注册 Android 的 native 函数表    if (startReg(env) < 0) {        ALOGE("Unable to register all android natives\n");        return;    }// 准备一个 String 数组:第 0 项放类名,后面放业务参数    jclass stringClass;    jobjectArray strArray;    jstring classNameStr;    stringClass = env->FindClass("java/lang/String");    strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);    classNameStr = env->NewStringUTF(className);    env->SetObjectArrayElement(strArray, 0, classNameStr);// 把剩余参数依次放进数组    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" 的 '.' 换成 '/' 再 FindClass    char* slashClassName = toSlashClassName(className);    jclass startClass = env->FindClass(slashClassName);    if (startClass == NULL) {        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);    } else {// 找到静态 main 方法        jmethodID startMeth = env->GetStaticMethodID(startClass, "main",            "([Ljava/lang/String;)V");        if (startMeth == NULL) {            ALOGE("JavaVM unable to find main() in '%s'\n", className);        } else {// 关键第三步:调用 main(),控制权从这里交到 Java 线程            env->CallStaticVoidMethod(startClass, startMeth, strArray);        }    }    free(slashClassName);// main 返回后虚拟机退出(zygote 模式下 main 不会返回)    if (mJavaVM->DetachCurrentThread() != JNI_OK)        ALOGW("Warning: unable to detach main thread\n");    if (mJavaVM->DestroyJavaVM() != 0)        ALOGW("Warning: VM did not shut down cleanly\n");}

这段代码的关键是 strArray 的构造:第 0 项放的是类名 ClassName,后面才放业务参数。视频里讲师反复强调「类名放在第一个位置、后面放的是参数」,就是这个数组。ZygoteInit.main 收到的 argv[0] 其实是类名「com.android.internal.os.ZygoteInit」,真正的 --abi-list=.../--start-system-server 等从 argv[1] 开始。

startVm 才是真正「把虚拟机跑起来」的地方,它设置堆大小、执行模式、GC 等一堆参数,这里只用签名示意,因为它和本文主线关系不大:

cpp
// 代码位置 /frameworks/base/core/jni/AndroidRuntime.cpp 第 569—972 行// 启动 Java 虚拟机,zygote=true 时走 zygote 模式的不同参数int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote){    // 设置 -Xms/-Xmx、执行模式、GC 等虚拟机参数后创建 VM    ...}

再往下 startReg 注册 Android 的 native 函数。Java 里声明成 native 的方法(例如稍后要讲到的 Zygote.nativeForkAndSpecialize)都靠这张注册表,Java 层的 native 关键字的函数才能落到 C++ 实现上——简单说就是「Java 调 native 前必须先把函数注册好,没注册就跑不了」。

启动虚拟机

视频里反复提到:Java 的方法一定要跑在 Java 虚拟机上,没有虚拟机代码就跑不起来。Zygote 是在自己的进程里先把虚拟机 startVm 起来,之后 fork 出来的应用进程和 system_server,都继承了这个虚拟机,因此不需要再 startVm 一次。这正是「应用启动为什么快」的原因:不是启动动作本身快,而是把「启动虚拟机」这一步从每次启动里省掉了。

app_process → AndroidRuntime::start → startVm/startReg → FindClass+CallStaticVoidMethod → ZygoteInit.main 的 native→Java 过渡

五、ZygoteInit.main:进入 Java 后的三步

控制权交到 ZygoteInit.main 后,剩下的就是「注册 socket → 预加载 → 进入死循环」三步,外加一个可选的「先 fork system_server」:

java
// 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 565—622 行// Zygote 进程的 Java 入口public static void main(String argv[]) {    try {        RuntimeInit.enableDdms();// 记录解析出的参数        boolean startSystemServer = false;        String socketName = "zygote";        String abiList = null;// 解析 argv[0] 之后的参数        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.");        }// 第一步:注册 zygote socket,等待应用请求连上来        registerZygoteSocket(socketName);// 第二步:预加载类和资源到当前进程        EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START, SystemClock.uptimeMillis());        preload();        EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END, SystemClock.uptimeMillis());// 预加载后做一次 GC,给 fork 前腾出干净的环境        gcAndFinalize();// 若带 --start-system-server,先 fork 出 system_server        if (startSystemServer) {            startSystemServer(abiList, socketName);        }// 第三步:进入 runSelectLoop 死循环,等待并处理命令        Log.i(TAG, "Accepting command socket connections");        runSelectLoop(abiList);        closeServerSocket();    } catch (MethodAndArgsCaller caller) {// 子进程通过抛这个异常来『跳到』自己的 main()        caller.run();    } catch (RuntimeException ex) {        Log.e(TAG, "Zygote died with exception", ex);        closeServerSocket();        throw ex;    }}

argv[0] 是类名(在 native 层被填进 strArray 第 0 项),所以参数循环从 i = 1 开始。解析出一个 --abi-list= 是必须的,否则直接抛异常没有后面的事。接下来 registerZygoteSocket、preload、runSelectLoop 就是 Zygote 生命的三个支柱,下面三节依次展开。

这里可以先画一张 ZygoteInit 的结构与三步的关系,把「参数解析 → socket/预加载/循环」的脉络固定下来:

ZygoteInit.main 的参数与三步:--abi-list/--socket-name/start-system-server → registerZygoteSocket → preload → (forkSystemServer) → runSelectLoop

六、预加载:把 Java 环境先装好(为 fork 的写时复制铺垫)

Zygote 是所有应用进程的孵化母体,所以它选择在启动时把「大多数应用都会用到的东西」一次装进自己的进程。preload() 顺序执行五个动作:

java
// 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 180—191 行// 预加载入口:按顺序装类、资源、OpenGL、动态库、文本资源static void preload() {    Log.d(TAG, "begin preload");    preloadClasses();    preloadResources();    preloadOpenGL();    preloadSharedLibraries();    preloadTextResources();    // 让 WebView 服务在 zygote 进程里初始化,便于内存共享    WebViewFactory.prepareWebViewInZygote();    Log.d(TAG, "end preload");}

每个动作装什么,看它的实现:

java
// 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 217—317 行// 加载常用 Java 类:读取 /system/etc/preloaded-classes,逐个 Class.forNameprivate static void preloadClasses() {    final VMRuntime runtime = VMRuntime.getRuntime();    InputStream is;    try {        is = new FileInputStream(PRELOADED_CLASSES);    } catch (FileNotFoundException e) {        Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");        return;    }    // 预加载期间临时放弃 root,避免静态初始化器以 root 身份运行    ...    BufferedReader br = new BufferedReader(new InputStreamReader(is), 256);    int count = 0;    String line;    while ((line = br.readLine()) != null) {        line = line.trim();        if (line.startsWith("#") || line.equals("")) {            continue;        }        try {            // 用 true 强制触发类初始化            Class.forName(line, true, null);            count++;        } catch (ClassNotFoundException e) {            Log.w(TAG, "Class not found for preloading: " + line);        }    }    Log.i(TAG, "...preloaded " + count + " classes in "            + (SystemClock.uptimeMillis() - startTime) + "ms.");} // 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 326—355 行// 加载常用系统资源:drawable 与 color state listprivate static void preloadResources() {    final VMRuntime runtime = VMRuntime.getRuntime();    try {        mResources = Resources.getSystem();        mResources.startPreloading();        if (PRELOAD_RESOURCES) {            // 读取 preloaded_drawables 数组并逐个加载            TypedArray ar = mResources.obtainTypedArray(                    com.android.internal.R.array.preloaded_drawables);            int N = preloadDrawables(runtime, ar);            ar.recycle();            // 再读 preloaded_color_state_lists            ar = mResources.obtainTypedArray(                    com.android.internal.R.array.preloaded_color_state_lists);            N = preloadColorStateLists(runtime, ar);            ar.recycle();        }        mResources.finishPreloading();    } catch (RuntimeException e) {        Log.w(TAG, "Failure preloading resources", e);    }} // 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 200—204 行// 预加载 OpenGL:把 EGL display 提前实例化private static void preloadOpenGL() {    if (!SystemProperties.getBoolean(PROPERTY_DISABLE_OPENGL_PRELOADING, false)) {        EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);    }} // 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 193—198 行// 预加载常用 native 动态库:android、compiler_rt、jnigraphicsprivate static void preloadSharedLibraries() {    Log.i(TAG, "Preloading shared libraries...");    System.loadLibrary("android");    System.loadLibrary("compiler_rt");    System.loadLibrary("jnigraphics");} // 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 206—208 行// 预加载文本分字器private static void preloadTextResources() {    Hyphenator.init();}

到这里,Zygote 进程里已经安装:一台 Java 虚拟机、一批 Framework 常用类、一套系统资源、几个几乎所有应用都会用到的 native 库(android、compiler_rt、jnigraphics)、OpenGL 的 display 实例、文本分字器,外加一个 zygote socket。

七、runSelectLoop:Zygote 的孵化循环

Zygote 启动后主线程不会退出——它进入 runSelectLoop,一个 while(true) 的死循环。课程反复强调「这个 while true 看不到出口」「Zygote 一直在这等待」。靠的是 I/O 多路复用,而不是每来一个连接就开一个线程去接收。

java
// 代码位置 /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 第 662—698 行// Zygote 主线程的 select 循环:接受新连接,并逐条处理命令private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {// fds 保存所有被监听的 fd,peers 保存与之一一对应的连接对象    ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();    ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();// 第 0 个 fd 是 server socket 本身,没有对应的连接,用 null 占位    fds.add(sServerSocket.getFileDescriptor());    peers.add(null);// 死循环,永远等待    while (true) {// 每次先把 fds 转成 poll 需要的 StructPollfd[] 数组        StructPollfd[] pollFds = new StructPollfd[fds.size()];        for (int i = 0; i < pollFds.length; ++i) {            pollFds[i] = new StructPollfd();            pollFds[i].fd = fds.get(i);// 只关心「可读」事件            pollFds[i].events = (short) POLLIN;        }// 阻塞等待:任何 fd 可读才会返回(-1 表示无限等待)        try {            Os.poll(pollFds, -1);        } catch (ErrnoException ex) {            throw new RuntimeException("poll failed", ex);        }// 倒序遍历所有 fd        for (int i = pollFds.length - 1; i >= 0; --i) {// 该 fd 没有可读事件就跳过            if ((pollFds[i].revents & POLLIN) == 0) {                continue;            }// 第 0 个 fd:server socket 接受了新连接,建一个 ZygoteConnection            if (i == 0) {                ZygoteConnection newPeer = acceptCommandPeer(abiList);                peers.add(newPeer);                fds.add(newPeer.getFileDesciptor());// 其他 fd:是已建立连接上有数据,进入 runOnce 处理一次请求            } else {                boolean done = peers.get(i).runOnce();                if (done) {                    peers.remove(i);                    fds.remove(i);                }            }        }    }}

Zygote 主线程把「server socket 的 fd」放进 fds[0],然后 Os.poll 阻塞。一旦有进程(通常来自 AMS)通过 socket 连进来,fds[0] 变为可读,循环走到 i == 0 的分支,acceptCommandPeer 接受连接、新建一个 ZygoteConnection,把它新的 fd 追加进 fds,于是下一次 poll 它也会被监听。等这个连接上真的送来了请求数据(代表 AMS 要创建进程),对应 fd 变为可读,循环走到 i != 0 分支,peers.get(i).runOnce() 完成一次孵化;runOnce 返回 true 表示连接关闭,就把这个 peer 和 fd 从列表里移除,腾出监听位。

runSelectLoop 孵化循环:Zygote 主线程 while(true) + Os.poll 监听第 0 个 fd(zygote server socket) → acceptCommandPeer 新建 ZygoteConnection → 其它连接 runOnce → forkAndSpecialize 出应用进程

I/O 复用:不是每个请求开线程,而是用一个阻塞的 poll 集中监听若干个 fd,谁有事处理谁。Zygote 主线程**几乎永远阻塞在 Os.poll。对比 6.0 之前(4.0/5.0)是用 native 方法做监听,6.0 改成了 Os.poll` 这种多路复用方式。

安全性问题

视频里专门提到 zygote socket「没什么安全机制可判断」「通过地址就能访问」。要理解这一点,看 registerZygoteSocket 的实现就明白了——它并不是某个进程自己 bind 出来的新 socket,而是把 init 早已通过 socket zygote stream 660 root system 创建、并把 fd 传下来的那条 socket 包装起来:

CODE7
socket 是 init 以 root system 权限、660 权限创建在 /dev/socket/zygote 的 UNIX socket,Zygote 只是把 fd 从环境变量捞出来叠了一层 LocalServerSocket。所以它的「保护」主要靠文件系统权限和 uid 校验——任何能连上这个 socket 的进程都能发「创建进程」的请求,这也解释了为什么要靠 ZygoteConnection 里的安全策略(applyUidSecurityPolicy 等)二次拦截。

八、ZygoteConnection.runOnce:读参数并 fork 一个应用进程

AMS 想要创建一个应用进程时,会通过 Process.start 往 zygote socket 写一个参数序列。ZygoteConnection 每个连接对应一次会话,runOnce 把这次会话处理完:

CODE8
runOnce 的核心:读参数 → 解析 → 安全校验 → Zygote.forkAndSpecialize。fork 返回后父子分开:pid == 0 是子进程(新应用进程),它清理掉不该继承的 fd 后调用 handleChildProc 进入自己的 ActivityThread.main;pid > 0 是 Zygote 本体,它把这次请求处理完,回到 runSelectLoop 继续 poll 等待下一次。

参数传递:

CODE9
任何一端在 Process.start 里调用 zygoteSendArgsAndGetResult 时,都会先写一个参数个数,再逐行写每个参数。一个典型的请求参数形如 --runtime-args、--setuid=10123、--setgid=10123、--target-sdk-version=23、com.example.app 和若干应用自身参数。ZygoteConnection 拿到后解析出 uid/gid/gids,再交给 forkAndSpecialize。

forkAndSpecialize 是怎么 fork 的

forkAndSpecialize 在 Zygote.java 里是 Java 层的封装,真正干活的是 native:

CODE10
native 层(com_android_internal_os_Zygote.cpp)在这个 nativeForkAndSpecialize 里完成真正的 fork,随后做 setresgid/setgroups/setresuid(同时设置 real/effective/saved 三组 ID,防止子进程用 saved-uid 再提权回 root)、SELinux 设置、关闭 fdsToClose 等,最终把 pid 返回给 Java。本文不深入 native 实现,Java 层的边界已经足够说明「fork 出一个继承 Zygote 环境的应用进程」这件事。

至于什么参数对应哪个字段,ZygoteConnection.Arguments 里把 --setuid、--setgid、--setgroups、--nice-name、--runtime-args、--target-sdk-version 等一一映射成 uid/gid/gids/niceName 字段,供 forkAndSpecialize 调用。

九、forkSystemServer:孵化 system_server

main() 中,如果带 --start-system-server,Zygote 不会直接进入 runSelectLoop,而是先 fork 出 system_server,然后自己才进入等待循环。这一步走的是另一条专门路径:

CODE11
这里出现了几个和普通应用不同的地方。

  • 一是 pid == 0 时走 handleSystemServerProcess,它 closeServerSocket(system_server 不再需要 zygote socket)、把进程名设为 system_server、对 SYSTEMSERVERCLASSPATH 做 dexopt,最后经 RuntimeInit.zygoteInit 进入 com.android.server.SystemServer.main。
  • 二是有个 waitForSecondaryZygote(当设备存在第二个 zygote 时)。

Zygote.forkSystemServer 同样是 Java 封装 + native 实现:

CODE12
到这里,Zygote 的启动与孵化主线已经闭环:自己的 Java 世界被 AndroidRuntime::start 搭起来,socket 被 registerZygoteSocket 接好,常用资源被 preload 装好,system_server 被 forkSystemServer 先拉起来,最后 Zygote 主线程死在 runSelectLoop 里,通过 acceptCommandPeer 接连接、runOnce 孵化一个个应用进程。

9.1 system_server 挂了,Zygote 为什么也要跟着死

「Zygote 不能被杀死」只是硬币的一面。反过来还有一个源码可验证的机制:Zygote fork 出 system_server 后,就一直通过 SIGCHLD 信号处理器盯着这个 pid(gSystemServerPid)。一旦 system_server 意外退出,Zygote 不会若无其事地继续 runSelectLoop,而是把自己也杀掉——让 init 按 service zygote 的规则重新拉起整条链路。

CODE13
gSystemServerPid 在 nativeForkSystemServer 的父进程分支里被赋值:

CODE14
这条规则回答了一个和「Zygote 不能被杀死」对称的问题:如果不是 Zygote 挂,而是它唯一特殊看护的 system_server 挂了,会怎样?答案是 Zygote 主动 kill(getpid(), SIGKILL) 自杀,交给 init 重新拉起整条链。这也是为什么 system_server 崩溃在真机上表现为「黑屏、重新走一遍开机动画」,而不是安静地少了一个进程。

顺带一提,第九节 hasSecondZygote/waitForSecondaryZygote 里的「第二个 zygote」不是巧合命名:app_process 与 app_process64 是两个独立可执行文件,分别以 zygote、zygote64 为 nice name(见 app_main.cpp 里按 __LP64__ 区分的 ZYGOTE_NICE_NAME),各自维护一份 --abi-list=(对应 ro.product.cpu.abilist32/64 属性)。hasSecondZygote 拿设备总的 ro.product.cpu.abilist 与自己这份对比,不相等就说明还有另一位 zygote 要一起启动、一起等——这也是很多支持 32/64 位混跑的设备会同时常驻 zygote 和 zygote64 两个进程的原因。

十、进程 fork

Zygote 能「免费分发环境」,靠的是 Linux 的 fork 写时复制(copy-on-write,COW)。但这份「免费」不是免费的——它有一个明确的内存代价。要理解这个代价,先把内存模型摆出来。

10.1 虚拟地址: 0-3G 是用户空间

一个进程的地址空间被虚拟地址管理。以 32 位 Linux 内核常见的 3G/1G 默认配置来说,地址空间是 0 到 4G:0-3G 归用户空间,3G-4G 归内核空间(内核可以配置成其它划分,64 位地址空间的划分方式完全不同)。代码、so 库、预加载的 class、资源,全部落在用户空间这段虚拟地址里。但虚拟地址只是「映射表」,真正的字节在物理内存(内存条)里,二者通过页(page)机制建立映射:虚拟地址被切成一页页,CPU 通过页表把虚拟页映射到物理页;当某页还没被加载进物理内存、而代码又恰好要去访问它时,就触发一次缺页异常,操作系统这才把该页真正读进物理内存。

所以「加载一个 so」分两步:第一步只是在虚拟地址空间里登记了一段地址;第二步是代码执行到那里、访问触发缺页时才真正占物理内存。这也解释了为什么一个进程虚拟地址能到 3G,但实际物理内存占用只有几百 MB——大部分映射页根本没被触碰。

内存布局:虚拟地址 0-3G 用户空间 / 3-4G 内核空间,代码/so/class/资源落在虚拟地址,经页机制映射到物理内存,缺页时加载

10.2 fork:只拷贝 mm_struct,页只读共享

Zygote fork 一个应用进程时,Linux 做的事比想象中「轻」。它不复制 3G 的虚拟内容,而是为子进程新建一份进程控制块(task_struct)与内存管理描述符 mm_struct,新页表的表项指向和父进程相同的物理页,并被标记成只读(read-only)。为了让「子进程改动不影响父进程、也不影响其它应用」,这些页保持只读共享;于是:

  • 读:父子都直接访问同一份已加载的物理页,不复制。
  • 写:触发「写时复制」,内核把该页复制一份,子进程改的是自己的副本。

这就是「读时共享、写时复制」。之所以必须把用户空间标成只读,是因为用户空间的资源(类、资源、图片)如果在父子进程间可写共享,那么一个应用改掉某张预加载图片,所有 fork 出来的应用都会跟着变,系统资源就乱套了。

CODE15
fork 写时复制:Zygote 的 mm_struct/页表被新建给子进程,二者指向同一批物理页并标为只读;任一进程写某页时内核复制一份(读时共享、写时复制)

这一下把两件事讲通了。

  • 第一,Zygote 不能被杀死:如果 Zygote 挂了,它占用的那批物理内存被释放,所有已 fork 出来、还引用着这批只读页的应用进程就会访问到失效的内存,整个系统瘫痪。所以 Zygote 由 init 守护、优先级极高。
  • 第二,应用进程一出生内存就偏大:因为 Copy-on-Write 的前提是只读共享,应用进程不能「按需清零」,只能老老实实继承这一整套预加载环境——哪怕它只做后台服务、完全用不到 UI 资源。

10.3 进程fork收益

好处和坏处是对立的:

  • 好处:应用进程启动时不用再加载 JVM、类、资源和 so,这些动作 Zygote 已经替它做过一次,fork 直接继承。这就是「应用启动快」的真正来源——快在省掉了 preload,而不是启动动作本身。
  • 坏处:每个应用进程(无论是否需要)都带上了整套预加载资源的「虚拟大小」,物理上通过只读共享省了大部分,但要写的那部分要复制,且进程的 RSS / 虚拟地址都被这套环境撑大。对不碰 UI 的后台服务进程来说,这份「家底」绝大部分是浪费。

素材里用 QQ 音乐举例:它有 9 个进程,其中之一是 qqplayerservice(播放器),这种服务进程完全不和界面打交道,但依然带着 Zygote 全套预加载资源,于是内存偏大。

十一、怎么优化:多进程、服务进程内存、预加载并行

既然应用进程一出生就带着偏大的内存,而系统又按「进程优先级 + 内存大小」来杀进程,那么优化就有两条路:让该有的进程更快见到开屏页,以及让可以被杀的服务进程尽量少占内存。

11.1 多进程:各司其职,别互相拖累

应用开发层面,多进程的一个典型动机是把「重」的初始化挪出主进程。Application.onCreate 在每个进程都会执行一次(N 个进程执行 N 次),所以如果你在 onCreate 里做了大量初始化——比如动态修复播放器 so、加载解码库——那么主进程一启动就会卡在这里,开屏页迟迟不出来。

一个常见做法是把这些重活放进一个独立进程(比如 qqplayerservice),让它的 Application 去做了。主进程的 Application 只做轻量初始化,于是主进程能最快到达开屏页,用户先看到画面,播放器相关的重初始化交给后台进程完成。多进程还带来隔离:每个进程有独立内存上限,进程之间崩溃互不影响,也不容易触发系统的「枪打出头鸟」,整体体验更流畅。

11.2 服务进程内存:越小越不容易被杀

系统杀进程按优先级从低到高:空进程最容易杀,后台其次,服务进程居中,前台进程最难被杀。同时还有一层「内存大小」——Android 的 Low Memory Killer(LMK)在内存紧张时偏向挑「占内存大」的进程开刀。一个后台播放服务(退到后台、锁屏后)容易成为目标,一旦被杀,音乐停、消息丢。

所以对服务进程的优化目标是把内存压到尽可能小。M受限于「fork 继承」的机制,服务进程天然背着 Zygote 的全套环境,能省的是它自己后申请的、与业务无关的一部分,哪怕是优化掉 1-3MB 也有着良好的收益——因为它发生在「一个可能被反复创建/杀掉的进程」上。

11.3 系统侧:预加载并行化

前面看到 Zygote 的 preload() 是单进程、单线程顺序执行的:preloadClasses 大约 1-2 秒,preloadResources 大约 2-3 秒。如果 CPU 是多核,这一长串串行加载完全可以用多个线程并行。让 preloadClasses 和 preloadResources 各占一个线程,总时间就取决于其中较长的那个(2-3 秒),能省出 1-2 秒——对 20 秒左右的开机来说,就是 10% 左右的优化。若是单核 CPU,开多线程反而抢时间片,就不该并行。

CODE16
要点是:这种并行化属于系统 developer 的优化,普通应用开发者无法直接控制 Zygote 的 preload,但它解释了「Zygote 这一段开机时间花在哪、为什么系统能再快一点」。

十二、总结与验证

  1. app_process 怎么执行 Java 代码:main() 创建 AppRuntime,调 AndroidRuntime::start("com.android.internal.os.ZygoteInit"),startVm 启动 JVM、startReg 注册 JNI,最后 CallStaticVoidMethod 调 ZygoteInit.main,native 世界由此跨进 Java。
  2. Zygote 为什么需要一直活着:ZygoteInit.main 进入 runSelectLoop 的 while(true),靠 Os.poll 阻塞监听 zygote socket,来一个连接就 acceptCommandPeer 建 ZygoteConnection,有命令就 runOnce 一次,永远不退出。
  3. 孵化为什么快:不是启动动作快,而是 fork 的写时复制——Zygote 预先把 JVM、类、资源、so 装好,fork 应用进程时只拷贝 mm_struct、继承只读共享的物理页,省掉每次启动的重加载。

本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Android-Framework(三)-开机启动Zygote进程 · Yinchen 的博客