专栏名称: 鸿洋
你好,欢迎关注鸿洋的公众号,每天为您推送高质量文章,让你每天都能涨知识。点击历史消息,查看所有已推送的文章,喜欢可以置顶本公众号。此外,本公众号支持投稿,如果你有原创的文章,希望通过本公众号发布,欢迎投稿。
目录
相关文章推荐
郭霖  ·  2024年终总结,花开终有时 ·  2 天前  
鸿洋  ·  Android AMS 自述 ·  2 天前  
郭霖  ·  原创:写给初学者的Jetpack ... ·  3 天前  
鸿洋  ·  再学安卓 - Zygote ·  3 天前  
51好读  ›  专栏  ›  鸿洋

再学安卓 - Zygote

鸿洋  · 公众号  · android  · 2024-12-30 08:35

正文

前言

在上一篇讲rc脚本的例子中,已经出现了zygote的身影,在Gityuan的系统启动架构图中也能看到zygote处在C++ Framework和Java Framework之间,由此可以猜测此进程必定是承上启下的开创性进程。首先让我们感性的认识一下这个进程。
adb shell "ps -A | grep zygote"

user          pid     ppid
root          1570     1    9069288 110472 0                   0 S zygote64
root          1630     1  534487112  50836 0                   0 S zygote
父进程id为1,就是上一篇的init进程,但有点奇怪,为啥有两个zygote?等会儿我们带着问题去源码中找答案。
看看跟zygote pid相关的进程有哪些:
adb shell "ps -A | grep 1570"

user          pid   ppid
root          1570     1    9069288 110472 0     0 S zygote64
system        3236  1570   17905656 760888 0     0 S system_server
u0_a206       4872  1570   12824556 477164 0     0 S com.android.systemui
u0_a248      14681  1570   10596876 120760 0     0 S com.android.mms
u0_a250      15976  1570    9754228 121768 0     0 S com.android.contacts
... ...
非常多且大部分是APP,他们的父进程都是pid 1570(zygote),其中包含一个叫system_server的进程。我们在第三篇曾单步调试过这个进程,当时它在UI中显示为system_process

到这里我们几乎可以笃定的相信所有应用进程都是zygote孵化而来,因此,接下来我们就看看zygote怎么启动,它又如何启动(孵化)别的进程。

1
zygote的启动

上一篇讲到init进程通过rc脚本启动后续进程,那么我们找一下zygote在哪里。
注:编译之前源码状态下,系统启动相关的rc脚本都在system\core\rootdir目录下,其他服务或进程的rc脚本一般都放在该模块代码根目录,比如:surfaceflinger.rc就在frameworks\native\services\surfaceflinger中。
// system\core\rootdir\init.rc
// 引入了zygote的rc脚本,adb shell getprop ro.zygote,查询到其值为zygote64_32

import /system/etc/init/hw/init.${ro.zygote}.rc
// system\core\rootdir\init.zygote64_32.rc
// 引入了init.zygote64.rc,同时定义了一个进程zygote_secondary

import /system/etc/init/hw/init.zygote64.rc
service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary --enable-lazy-preload
    ... ...
    onrestart restart zygote
    ... ...
// system\core\rootdir\init.zygote64.rc

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main      //入口
    ... ...
    socket zygote stream 660 root system  //此进程需要创建socket
    ... ...
    //zygote重启时,需要同时重启以下服务
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart media.tuner
    onrestart restart netd
    onrestart restart wificond
zygote的定义找到了,在哪里启动的呢?init.rc中一番搜索之后,得出结论。
// 1. late-init中触发zygote-start
on late-init
    ... ...
    trigger zygote-start
    ... ...

// 2. zygote-start中直接启动zygote
on zygote-start
    ... ...
    start zygote
    start zygote_secondary
    ... ...

这里我们就可以回答开头的疑问了,init确实启动了两个进程:zygote和zygote_secondary,从rc脚本的关系来看,zygote_secondary有点像zygote的守护进程,因为zygote_secondary重启时也会重启zygote。因此我们的主要目标还是zygote,zygote_secondary就不去深究了。

2
zygote的入口

zygote的入口函数在frameworks/base/cmds/app_process/app_main.cpp中。
等等,这里我们需要稍微停一下,你咋知道入口在这里?难不成自学一个模块,要在网上先搜索从哪里开始?如果没人输出过这个模块的文章,还学不成了?
其实不然,从开头rc文件的分析我们已经知道,zygote进程代码被存放在了一个二进制文件中/system/bin/app_process64,这个文件名一定在编译脚本(.bp文件)中出现过,所以我们需要在bp文件中找到它。这里又需要大家有一点AOSP编译方面的基础知识,但不要害怕,真的只需要一点,甚至直接打开bp文件都能猜个大概。bp文件的语法及详细解释可以参考官方文档(https://source.android.com/docs/setup/reference)。
我们直接在cs.android.com上搜索app_process64,出来的结果不是很满意,没有bp文件。仔细看看,后缀64?标识系统平台?去掉后缀再搜。
大概模块位置也清楚了。bp文件挑重点看。
cc_binary {
    name"app_process",
    srcs: ["app_main.cpp"],
    multilib: {
        lib32: {
            suffix: "32",
        },
        lib64: {
            suffix"64",
        },
    }
    ... ...
}
太明显了,官方文档基本不需要看。盲猜一下,它说的是:编译一个二进制文件,名称为app_process,源码在当前目录的app_main.cpp中,multilib指定多平台后缀。真真儿的大白话啊。

好了,这下我们可以非常安心的说出开头那句话:zygote的入口函数在frameworks/base/cmds/app_process/app_main.cpp中。

3
app_main.cpp


// frameworks/base/cmds/app_process/app_main.cpp
int main(int argc, charconst argv[])  {
    //很多与参数相关的操作,读取、解析、设置等
    ... ...
    if (strcmp(arg, "--zygote") == 0) {
        // rc文件中配置的启动参数有--zygote,因此变量zygote为true
        zygote = true;
        // ZYGOTE_NICE_NAME为宏定义,值为zygote64,即进程名
        niceName = ZYGOTE_NICE_NAME;  
    }
    ... ...
    if (zygote) {
        // runtime的start函数参数:一个类名、args字符数组(参数)以及zygote布尔变量
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (!className.isEmpty()) {
        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.");
    }
}
简单回溯一下runtime实例,可以得知,对应的类为AppRuntime,其父类为AndroidRuntime,start函数是在父类中实现的。
// frameworks\base\core\jni\AndroidRuntime.cpp
void AndroidRuntime::start(const char* className, const Vector& options, bool zygote)  {
    ... ...
    static const String8 startSystemServer("start-system-server");  
    // Whether this is the primary zygote, meaning the zygote which will fork system server.
    // 是否是主zygote就看是否有start-system-server参数,即是否负责启动system server进程。
    // 这就是zygote和zygote_secondary的区别之一
    bool primary_zygote = false;
    ... ...
    /* start the virtual machine */
    // 初始化JNI环境,这里我们只需要理解JNI就是连接C++和Java的桥梁即可
    JniInvocation jni_invocation;
    // Init函数中会动态加载libart.so库,这个在后面会用到
    jni_invocation.Init(NULL);
    JNIEnv* env;
    // 创建虚拟机,如何创建?后面介绍
    if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
        return;
    }
    onVmCreated(env);

    // startReg中注册当前阶段需要使用的JNI函数
    if (startReg(env) 0) {  
        ALOGE("Unable to register all android natives\n");  
        return;  
    }
    ... ...
    char* slashClassName = toSlashClassName(className != NULL ? className : "");  
    jclass startClass = env->FindClass(slashClassName);  
    if (startClass == NULL) {  
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);   
    } else {  
        jmethodID startMeth = env->GetStaticMethodID(startClass, "main",  
            "([Ljava/lang/String;)V");  
        if (startMeth == NULL) {  
            ALOGE("JavaVM unable to find main() in '%s'\n", className); 
        } else {
            // 最终调用com.android.internal.os.ZygoteInit的main函数
            // 这句调用是阻塞的,也就是说如果Java中的函数不执行完
            // CallStaticVoidMethod就不会返回
            env->CallStaticVoidMethod(startClass, startMeth, strArray);
        }
    }
    ... ...
}
以上代码中env(JNI环境)的各种调用就是C层调用Java函数的用法,看起来是不是跟Java反射调用的代码很相似。关于JNI的介绍,可以参考这一篇(https://juejin.cn/post/7139843955457261605),快速过一下,用到时再查阅细节。
稍微解释一下虚拟机的创建,只是介绍个开始。
// frameworks\base\core\jni\AndroidRuntime.cpp
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool primary_zygote)  {
    ... ...
    /*  
     * Initialize the VM. 
     * The JavaVM* is essentially per-process, and the JNIEnv* is per-thread. 
     * If this call succeeds, the VM is ready, and we can start issuing 
     * JNI calls. 
     */

    // JNI_CreateJavaVM最终会调用libart.so中的函数JNI_CreateJavaVM创建虚拟机
    // 需要研究虚拟机的伙伴,可以从这个so开始
    if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) 0) {
        ALOGE("JNI_CreateJavaVM failed\n");
        return -1;
    }
    return 0;
}


4
ZygoteInit

到这里,我们已经进入Java Framework。
// frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

public static void main(String[] argv{
    // 我们的zygote在rc文件中并没有配置懒加载,因此这里肯定会执行if分支
    if (!enableLazyPreload) {
        ... ...
        // 预加载类和资源
        // 比如:/system/etc/preloaded-classes中的Java类,我们熟悉的四大组件赫然在列
        preload(bootTimingsTraceLog);
        ... ...
    }

    if (startSystemServer) {
        Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);
        // {@code r == null} in the parent (zygote) process, 
        // and {@code r != null} in the child (system_server) process.
        // 这两行注释的意思是:
        // 在zygote进程中会返回null,接下来执行runSelectLoop
        // 在SystemServer进程中会返回一个runnable,接下来执行r.run()
        if (r != null) {
            r.run();
            return;
        }
    }

    Log.i(TAG, "Accepting command socket connections");
    // The select loop returns early in the child process after a fork and  
    // loops forever in the zygote.
    caller = zygoteServer.runSelectLoop(abiList);
    ... ...
}
系统启动流程到forkSystemServer函数这里就开始“分叉”了,因为一个新的进程SystemServer诞生了。zygote和SystemServer将分别执行各自的后续逻辑。我们在下一篇讲SystemServer的文章中会以forkSystemServer函数作为开始。这里我们继续跟踪zygote的逻辑。
// frameworks/base/core/java/com/android/internal/os/ZygoteServer.java

Runnable runSelectLoop(String abiList) {
    ... ...
    while (true) {
        ... ...
        // poll函数等待pollFDs文件描述符状态变化的消息
        //
        // pollTimeoutMs参数:
        // 为正:在指定的毫秒数内等待文件描述符的状态变化,超时或有消息到来后poll就返回
        // 为0:表示立即检测文件描述符状态,立即返回
        // 为-1:表示一直等待,直到有文件描述符的状态变化
        //
        // pollReturnValue返回值:
        // 为0:没有文件描述符可用,即没有需要处理的消息
        // 为正:有文件描述符可用,即需要处理消息
        // 为负:出现错误
        pollReturnValue = Os.poll(pollFDs, pollTimeoutMs);
        ... ...
    }
}
正常情况下,Zygote进程就会在这个循环里一直执行下去,响应新建进程的消息或继续循环等待。

注:while循环中会有USAP(Unspecialized App Process)的逻辑,这是Android10之后,新增的提升进程创建效率的机制,简单来说,就是zygote会预创建一些进程放入池中,当有新建需求时,就从池中取出一个。为了突出主线,我们剥离了这部分逻辑,若想深入了解,可以参考芦大佬的这篇文章(https://juejin.cn/post/6922704248195153927)。

5
总结


流程来到zygote这里,逻辑变得复杂,Zygote自身的初始化、虚拟机的创建、特殊Zygote进程(webview_zygote)的创建、SystemServer的创建、普通APP进程的创建、USAP机制,代码分支众多,判断条件一层叠一层,我们紧紧抓住Zygote主线进行探索,后续再逐步完善其他分支的理解。让我们用一张图概括一下这段历程。

C/C++下的日志开关

面对复杂的逻辑,仅仅看代码已经不能满足需求,因此我们需要观察日志。对于不熟悉C/C++的伙伴,简单介绍一下相关内容。
app_main.cpp为例,其中main函数一进来就会看到:
if (!LOG_NDEBUG) {  
  String8 argv_String;  
  for (int i = 0; i     argv_String.append("\"");  
    argv_String.append(argv[i]);  
    argv_String.append("\" ");  
  }  
  ALOGV("app_process main with argv: %s", argv_String.string());  
}
LOG_NDEBUG是一个宏定义变量,是v级日志的输出开关,为0输出,为1不输出。默认情况下,只要你编译的是release版本,那么LOG_NDEBUG都为1。如果我们想要看到ALOGV的输出,就需要在当前cpp文件最开始加入"#define LOG_NDEBUG 0"。不要被if语句的判断条件迷惑,如果不在文件开头定义LOG_NDEBUG为0,即使把if分支里面的代码提出来,一样得不到日志输出。这是为什么?因为ALOGV也是一个宏定义,且它的定义与LOG_NDEBUG的取值相关。具体原因看看以下宏定义就明白了。另外,除了在IDE中看日志,我们也经常使用命令导出日志到本地查看:adb logcat -d > log.txt
// system/logging/liblog/include/log/log_main.h
/*
 * Normally we strip the effects of ALOGV (VERBOSE messages),
 * LOG_FATAL and LOG_FATAL_IF (FATAL assert messages) from the
 * release builds by defining NDEBUG.  You can modify this (for
 * example with "#define LOG_NDEBUG 0" at the top of your source
 * file) to change that behavior.
 */


// 其实上面的注释已经说了打开日志的方法,有时看注释确实能走很多捷径
// NDEBUG也是一个宏定义变量,它是编译器在编译期间定义的,我这里在代码中打印它是为1
// 我猜测如果编译的是debug版本,NDEBUG应该打印出来就是0
#ifndef LOG_NDEBUG
#ifdef NDEBUG
#define LOG_NDEBUG 1
#else
#define LOG_NDEBUG 0
#endif
#endif
... ...
// ALOGV的宏定义
#ifndef ALOGV
#define __ALOGV(...) ((void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__))
#if LOG_NDEBUG
#define ALOGV(...)                   \
  do {                               \
    __FAKE_USE_VA_ARGS(__VA_ARGS__); \
    if (false) {                     \
      __ALOGV(__VA_ARGS__);          \
    }                                \
  } while (false)
#else
#define ALOGV(...) __ALOGV(__VA_ARGS__)
#endif
#endif
通过上面的头文件内容,还可以知道,我们可以通过LOG_TAG这个宏定义变量设置日志Tag,也是在cpp文件开头定义即可,就像我们经常在Java类第一行写的“public static final String TAG = xxx”。


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

推荐文章
郭霖  ·  2024年终总结,花开终有时
2 天前
鸿洋  ·  Android AMS 自述
2 天前
鸿洋  ·  再学安卓 - Zygote
3 天前
腾讯汽车  ·  丧心病狂!制止乘客吸烟竟被...
7 年前
小西玖玖  ·  从此无心看风景,任它回忆满心头
7 年前
朱莉生活日记  ·  闺蜜,等我们老了,就这样好不好?
6 年前