• Android(java)学习笔记103:Framework运行环境之 Android进程产生过程


    1. 前面Android(java)学习笔记159提到Dalvik虚拟机启动初始化过程,就下来就是启动zygote进程:

    zygote进程所有APK应用进程的父进程每当执行一个Android应用程序,Zygote就会孵化一个子线程去执行该应用程序(系统内部执行dvz指令完成的)

     特别注意:系统提供了一个app_process进程,它会自动启动ZygoteInit.java和SystemServer.java这两个类,app_process进程本质上是使用dalvikvm启动ZygoteInit.java。并且启动后加载Framework中大部分类和资源。

    2. zygote进程启动流程:

    (1)在init.rc中配置zygote启动参数:

    init.rc存在于设备的根目录下,读者可以使用adb pull /init.rc ~/Desktop命令取出该文件,文件中和zygote相关的配置信息如下:

    1. service zygote /system/bin/app_process -Xzygote 
      /system/bin --zygote --start-system-server  
    2.     socket zygote stream 666  
    3.     onrestart write /sys/android_power/request_state wake  
    4.     onrestart write /sys/power/state on  
    5.     onrestart restart media  
    6.     onrestart restart netd  

    首先第一行中使用service指令告诉操作系统将zygote程序加入到系统服务中,service的语法如下:

    1. service service_name 可执行程序的路径 可执行程序自身所需的参数列表 

    service service_name执行程序的路径 可执行程序自身所需的参数列表

    此处的服务被定义为zygote,理论上讲该服务的名称可以是任意的。可执行程序的路径正是/system/bin/app_process,也就是前面所讲的app_process,参数一共包含四个,分别如下:

    -Xzygote,该参数将作为虚拟机启动时所需要的参数,是在AndroidRuntime.cpp类的startVm()函数中调用JNI_CreateJavaVM()时被使用的。

    /system/bin,代表虚拟机程序所在目录,因为app_process完全可以不和虚拟机在同一个目录,而在app_process内部的AndroidRuntime类内部需要知道虚拟机所在的目录。

    --zygote,指明以ZygoteInit类作为虚拟机执行的入口,如果没有--zygote参数,则需要明确指定需要执行的类名。

    --start-system-server,仅在指定--zygote参数时才有效,意思是告知ZygoteInit启动完毕后孵化出第一个进程SystemServer

    接下来的配置命令socket用于指定该服务所使用到的socket,后面的参数依次是名称、类型、端口地址。

    onrestart命令指定该服务重启的条件,即当满足这些条件后,zygote服务就需要重启,这些条件一般是一些系统异常条件。

    (2)启动Socket服务端口

    当zygote服务从app_process开始启动后,会启动一个Dalvik虚拟机,而虚拟机执行的第一个Java类就是ZygoteInit.java,因此接下来的过程就从ZygoteInit类的main()函数开始说起。main()函数中做的第一个重要工作就是启动一个Socket服务端口,该Socket端口用于接收启动新进程的命令。

    (3)加载preload-classes

    在ZygoteInit类的main()函数中,创建完Socket服务端后还不能立即孵化新的进程,因为这个"卵"中还没有必须"核酸",这个"核酸"就是指预装的Framework大部分类及资源。预装的类列表是在framework.jar中的一个文本文件列表,名称为preload-classes,该列表的原始定义在frameworks/base/preload-classes文本文件中,而该文件又是通过frameworks/base/tools/preload/

    (4)加载preload-resources

    preload-resources是在frameworks/base/core/res/res/values/arrays.xml中被定义的,包含两类资源,一类是drawable资源,另一类是color资源,如以下代码所示:

    1. <array name="preloaded_drawables"
    2.        <item>@drawable/sym_def_app_icon</item
    3. ... ...  
    4.    </array
    5.  
    6.    <array name="preloaded_color_state_lists"
    7.        <item>@color/hint_foreground_dark</item
    8.         ... ...  
    9.    </array

    而加载这些资源是在preloadResources()函数中完成的,该函数中分别调用preloadDrawables()和preloadColorStateLists()加载这两类资源。加载的原理很简单,就是把这些资源读出来放到一个全局变量中,只要该类对象不被销毁,这些全局变量就会一直保存。

    保存Drawable资源的全局变量是mResources,该变量的类型是Resources类,由于该类内部会保存一个Drawable资源列表,因此,实际上缓存这些Drawable资源是在Resources内部;保存Color资源的全局变量也是mResources,同样,Resources类内部也有一个Color资源的列表。

    (5)使用folk启动新进程

            folk是Linux系统的一个系统调用,其作用是复制当前进程,产生一个新的进程。新进程将拥有和原始进程完全相同的进程信息,除了进程id不同。进程信息包括该进程所打开的文件描述符列表、所分配的内存等。当新进程被创建后,两个进程将共享已经分配的内存空间,直到其中一个需要向内存中写入数据时,操作系统才负责复制一份目标地址空间,并将要写的数据写入到新的地址中,这就是所谓的copy-on-write机制,即"仅当写的时候才复制",这种机制可以最大限度地在多个进程中共享物理内存。

           第一次接触folk的读者可能觉得奇怪,为什么要复制进程呢?在大家熟悉的Windows操作系统中,一个应用程序一般对应一个进程,如果说要复制进程,可能的结果就是从计算器程序复制出一个Office程序,这听起来似乎很不合理。要立即复制进程就需要首先了解进程的启动过程。

           在所有的操作系统中,都存在一个程序装载器,程序装载器一般会作为操作系统的一部分,并由所谓的Shell程序调用。当内核启动后,Shell程序会首先启动。常见的Shell程序包含两大类,一类是命令行界面,另一类是窗口界面,Windows系统中Shell程序就是桌面程序,Ubuntu系统中的Shell程序就是GNOME桌面程序。Shell程序启动后,用户可以双击桌面图标启动指定的应用程序,而在操作系统内部,启动新的进程包含三个过程。

    第一个过程,内核创建一个进程数据结构,用于表示将要启动的进程。

    第二个过程,内核调用程序装载器函数,从指定的程序文件读取程序代码,并将这些程序代码装载到预先设定的内存地址。

    第三个过程,装载完毕后,内核将程序指针指向到目标程序地址的入口处开始执行指定的进程。当然,实际的过程会考虑更多的细节,不过大致思路就是这么简单。

    在一般情况下,没有必要复制进程,而是按照以上三个过程创建新进程,但当满足以下条件时,则建议使用复制进程:即两个进程中共享了大量的程序。

    举个例子,去澳大利亚看袋鼠和去澳大利亚看考拉,这是两个进程,但完成这两个进程的大多数任务都是相同的,即先订机票,然后带照相机,再坐地铁到首都机场,最后再坐14个小时的飞机到澳大利亚,到了之后唯一不同就是看考拉和袋鼠。为了更有效地完成这两个任务,可以先雇佣一个精灵进程,让它订机票、带相机、坐地铁、乘飞机,一直到澳大利亚后,从这个精灵进程中复制出两个进程,一个去看考拉,另一个去看袋鼠。如果你愿意,还可以去悉尼歌剧院,这就是进程的复制,其好处是节省了大量共享的内存。

    由于folk()函数是Linux的系统调用,Android中的Java层仅仅是对该调用进行了JNI封装而已,因此,接下来以一段C代码来介绍folk()函数的使用,以便大家对该函数有更具体的认识:

    /**  
     *FileName: MyFolk.c  
     */  
    #include <sys/types.h> 
    #include <unistd.h> 
    int main(){  
        pid_t pid;  
        printf("pid = %d, Take camera, by subway, take air! 
    ", getpid());  
        pid = folk();  
        if(pid > 0){  
            printf("pid=%d, 我是精灵! 
    ", getpid());  
            pid = folk();  
            if(!pid) printf("pid=%d, 去看考拉! 
    ", getpid());  
        }  
        else if (!pid) printf("pid=%d, 去看袋鼠! 
    ", getpid());  
        else if (pid == -1) perror("folk");  
        getchar();  
    }  

    以上代码的执行结果如下:

    $ ./MyFolk.bin  
    pid = 3927, Take camera, by subway, take air!   
    pid=3927, 我是精灵!   
    pid=3929, 去看袋鼠!   
    pid=3930, 去看考拉!  

          folk()函数的返回值与普通函数调用完全不同。当返回值大于0时,代表的是父进程当等于0时,代表的是被复制的进程。换句话说,父进程和子进程的代码都在该C文件中,只是不同的进程执行不同的代码,而进程是靠folk()的返回值进行区分的。

    由以上执行结果可以看出,第一次调用folk()时复制了一个"看袋鼠"进程,然后在父进程中再次调用folk()复制了"看考拉"的进程,三者都有各自不同的进程id。

    zygote进程就是本例中的"精灵进程",那些"拿相机、坐地铁、乘飞机"的操作就是zygote进程中加载的preload-classes类具备的功能。

    ZygoteInit.java中复制新进程是通过在runSelectLoopMode()函数中调用ZygoteConnection类的runOnce()函数完成的,而该函数中则调用了以下代码用于复制一个新的进程。

     

    forkAndSpecialize()函数是一个native函数,其内部的执行原理和上面的C代码类似。

    当新进程被创建好后,还需要做一些"善后"工作。因为当zygote复制新进程时,已经创建了一个Socket服务端,而这个服务端是不应该被新进程使用的,否则系统中会有多个进程接收Socket客户端的命令。因此,新进程被创建好后,首先需要在新进程中关闭该Socket服务端,并调用新进程中指定的Class文件的main()函数作为新进程的入口点。而这些正是在调用forkAndSpecialize()函数后根据返回值pid完成的,如以下代码所示:

     

    pid等于0时,代表的是子进程,handleChildProc()函数中的关键代码如下,首先是关于Socket服务端。

     

    接着从指定Class文件的main()函数处开始执行,如以下代码所示:

     

    至此,新进程就完全脱离了zygote进程的孵化过程,成为一个真正的应用进程。

    3. 上面的说明可能不太形象,下面感性的说明一下,如下:

    (1)我们来看看每个android进程如何产生的:

     下面来对Zygote进程孵化新进程的过程做进一步了解:
      • Zygote进程调用fork()函数创建出Zygote 子进程,
      • 子进程Zygote  共享父进程Zygote的代码区与连接信息。
          如下图所示,Fork()橙色箭头左边是Zygote进程,右边是创建出的Zygote‘子进程;然后Zygote’ 子进程将执行流程交给应用程序A,Android程序开始运行。
    新生成的应用程序A会使用已有Zygote父进程的库与资源的连接信息,所以运行速度很快。
     
     
    另外,对于上图,Zygote启动后,初始并运行DVM,而后将需要的类与资源加载到内存中。随后调用fork()创建出Zygote子进程,接着子进程动态加载并运行应用程序A。
          运行的应用程序A会使用Zygote已经初始化并启动运行的DVM代码,通过使用已加载至内存中的类与资源来加快运行速度。
     
    (2)Android进程模型:
          Linux通过调用start_kernel函数来启动内核,当内核启动模块启动完成后,将启动用户空间的第一个进程——Init进程,下图为Android系统的进程模型图:
     
     
    从上图可以看出,Linux内核在启动过程中,创建一个名为Kthreadd的内核进程,PID=2,用于创建内核空间的其他进程;同时创建第一个用户空间Init进程,该进程PID = 1,用于启动一些本地进程,比如Zygote进程,而Zygote进程也是一个专门用于孵化Java进程的本地进程,上图清晰地描述了整个Android系统的进程模型。
  • 相关阅读:
    整数的唯一分解定理【模板】
    poj 1419Graph Coloring 【dfs+补图+计算最大团+计算最大独立集 【模板】】
    poj 1065 Wooden Sticks 【贪心 新思维】
    poj 3126 Prime Path 【bfs】
    hihocoder 第五十二周 高斯消元·二【高斯消元解异或方程 难点【模板】】
    poj 1840 Eqs 【解五元方程+分治+枚举打表+二分查找所有key 】
    poj 1019 Number Sequence 【组合数学+数字x的位宽函数】
    括号匹配
    Digit
    Ace of Aces
  • 原文地址:https://www.cnblogs.com/hebao0514/p/4731938.html
Copyright © 2020-2023  润新知