Android系统的启动流程
Android系统的启动,主要是指Android手机关机后,长按电源键后,Android手机开机的过程。从系统角度看,Android的启动程序可分为:
1、bootloader引导
2、装载与启动Linux内核
3、启动Android系统
· 3.1、启动Init进程
· 3.1、启动Zygote
· 3.1、启动SystemService
· 3.1、启动Launcher
一、Bootloader启动
开机,开机就是给系统开始供电,此时硬件电路会产生一个确定的复位时序,保证CPU是最后一个被复位的器件,为什么CPU要最后被复位呢?因为,如果CPU第一个被复位,则当CPU复位后开始运行时,其他硬件内部的寄存器状态可能还没有准备好,比如磁盘或者内存,那么久可能出现外围硬件初始化错误。当正确完成复位后,CPU开始执行第一条指令,该指令所在的内存地址是固定的,这由CPU的制造者指定。不同的CPU可能会从不同的地址获取指令,但这个地址必须是固定的,这个固定地址所保存的程序往往被称为"引导程序(BootLoader)",因为其作用是装载真正的用户程序。而U-boot的启动过程大致上可以分为两个阶段:
· 第一阶段:汇编代码U-boot的第一条指令从cpu/armXXX/start.S文件开始
· 第二阶段:C代码从文件/lib_arm/board.c的start_armboot()函数开始。
二、Linux系统启动
关于Linux系统启动主要分为三个阶段,第一个阶段是自解压过程,第二个是设置ARM处理器的工作模式、设置一级页表等,第三个阶段主要是C代码,包括Android的初始化的全部工作。
(一) 自解压过程
(二) Linux初始化
Linux初始化又分为三个阶段
第一阶段
本阶段就是上面说的到的内核解压缩完成后的阶段。
第二阶段
从start_kernel函数开始
Linux内核启动的第一个阶段是从start_kernel函数开始的。start_kernel是所有Linux平台进入系统内核初始化后的入口函数,它主要完成剩余与硬件平台的相关初始化工作,在进行一些系列的与内核相关的初始后,调用第一个用户进程——init进程并等待用户进程的执行,这样整个Linux内核便启动完毕。该函数位于init/main.c文件中。
第三阶段
根文件系统至少包括以下目录:
· /etc/:存储重要的配置文件
· /bin/:存储常用且开机时必须用到的执行文件。
· /sbin/:存储着开机过程中所需的系统执行文件。
· /lib/:存储/bin/及/sbin/的执行文件所需要的链接库,以及Linux的内核模块
· /dev/:存储设备文件
上面五大目录必须存储在文件系统上,缺一不可。
一、init进程简介
通过上篇文章我们知道,Android设备启动要经过3个阶段,BootLoader、Linux Kernel和Android系统服务,一般情况下,他们都会相应的启动对动画对应。前面我们已经知道Andorid系统是如何启动的BootLoader和Linux Kernel的。
严格上讲,Android系统实际上是运行于Linux内核之上的一系列"服务进程",并不算一个完成意义上的"操作系统";而这一系列进程是维持Android设备正常工作的关键,所以它们肯定有一个"根进程",这个"根进程"衍生出了这一系列进程。这个"根进程"就是init进程。
init进程是Android系统启动的第一个进程。它通过解析init.rc脚本来构建出系统的初始形态。其他的"一系列"Android系统进程大部分也是通过"init.rc"来启动的。因为要兼容不同的开发商,所以init.rc脚本的语法很简单,并且采用的是纯文本编辑的,这样导致它可读性就会很高。
二、Init.cpp
init是Linux系统中用户空间的第一个进程(pid=1),Linux Kernel启动后,会调用/system/core/init/Init.cpp的main()方法
init总结
这里里面总结下init里面main方法做的事情如下:
· first stage 初始化环境变量和各种文件系统目录,klog初始化等
· selinux相关初始化完成,然后切换second stage 重启init进程
· 属性服务初始化,将各种系统属性默认值填充到属性Map中
· 创建epoll描述符结合注册socket监听,处理显示启动进程和挂掉的子进程重启
· 解析init.rc。把各种action、service等解析出来的填充到相应链表容器管理
· 有序将early-init、init等各种cmd加入到执行队列action_queue链表中
· 进入while()循环依次取出执行队列action_queue中的command执行,fork包括app_process在内的各种进程,epoll阻塞监听处理来自挂掉的子进程的消息,根据设定策略restart子进程。
Android系统启动—— zyogte进程 (C篇)
我们大家都是知道"一鼎三足"和"三角形的稳定性",那么支撑Android系统的三个"足"是什么?即init进程、SystemServer进程和Zygote进程。
一、为什么要研究 zygote?
Linux的进程是通过系统调用fork产生的,fork出的子进程除了内核中的一些核心的数据结构和父进程不同之外,其余的内存映像都是和父进程共享的。只有当子进程需要去改写这些共享的内存时,操作系统才会为子进程分配一个新的页面,并将老的页面上的数据复制一份到新的页面,这就是所谓的"写拷贝"。
通常,子进程被fork出来后,会继续执行系统调用exec,exec将用一个新的可执行文件的内容替换当前进程的代码段、数据段、堆和栈段。Fork加exec 是Linux启动应用的标准做法,init进程也是这样来启动的各种服务的。
· Zygote创建应用程序时却只使用了fork,没有调用exec。Android应用中执行的是Java代码,Java代码的不同才造成了应用的区别,而对于运行Java的环境,要求却是一样的。
· Zygote初始化时会创建创建虚拟机,同时把需要的系统类库和资源文件加载到内存里面。Zygote fork出子进程后,这个子进程也继承了能正常工作的虚拟机和各类系统资源,接下来子进程只需要装载APK文件的字节码文件就可以运行了。这样应用程序的启动时间就会大大缩短。
二、Zygote进程(C层)的启动
Zygote进程在init进程中以service的方式启动的。从Android 5.0开始,Zygote还是有变动的,之前是直接放入init.rc中的代码块中,现在是放到了单独的文件中,通过init.rc中通过"import"的方式引入文件。如下:代码在init.rc11行
import /init.${ro.zygote}.rc
属性rozyoget 可能取值有可能为如下:
· zygote32
· zygote32_64
· zygote64
· zygote64_32
(三)、zygote的启动流程
那我们就整理一下zygote的启动流程,大致如下:
AppRuntime继承自AndroidRuntime类,并且重载了onVmCreated 、onStarted、onZygoteInit和onExit函数。我们发现并没有重载start函数,而在app_main.cpp的main()函数的最后runtime.start函数,所以具体执行在AndroidRuntime类的start函数。那我们就来研究下AndroidRuntime类
三、关于虚拟机简介
Android系统在4.4版本的时候,发布了一个ART运行时,来替换之前的一直使用的Dalvik虚拟机,接来解决之前Dalvik虚拟机的性能问题,关于ART的实现原理,我后续会单独讲解。这里先略过。这里先简单的讲解下虚拟机。Dalvik虚拟机实则是也算是一个Java虚拟机,只不过它的执行的不是class文件,而是dex文件。所以,ART运行时的设计的最perfect的方案肯定也是类似于一个Dalvik虚拟机的形式。其实无论是ART虚拟机还是Dalvik虚拟机在接口上与Java虚拟机基本上是一致的(但是其内部的机制是不一样的)。这样才能无缝隙的衔接。那我们来简单的看下这三个虚拟机(Java虚拟机、Dalvik虚拟机、ART运行时) 如下图:
1、相同点
通过上图我们知道,Dalvik虚拟机和ART运行时都实现了3个抽象Java虚拟机的接口,即:
· 1、JNI_GetDefaultJavaVMInitArgs:获取虚拟机的默认是初始化参数
· 2、JNI_CreateJavaVM:在进程中创建虚拟机实例
· 3、JNI_GetCreatedJavaVMs:获取进程中创建虚拟机实例
2、不同点
以上描述的是Dalvik虚拟机与ART运行时的共同之处,当然它们之间还有不同点,最大的不同点在于,Dalvik虚拟机执行的是dex字节码,ART虚拟机执行的是本地机器吗。这意味着Dalvik虚拟机包含一个解释器,用来执行dex字节码,当前从Android2.2开始,也包含了一个JIT(Just-In-Time),用来在运行时动态地将执行频率很高的dex字节码翻译成本地机器码,然后再执行。通过JIT,就可以有效地提高Dalvik虚拟机的执行效率。但是,dex字节翻译成本地机器码是发生在应用程序运行过程中的,并且应用程序每一次重新运行的时候,都要做重做这个翻译的工作。所以即使用了JIT,Dalvik虚拟机总体的性能还是不能与直接执行本地机器码的ART运行时相比。
3、ART原理简介
那我们来看下,Android运行时从Dalvik虚拟机替换成ART运行时,并不要求开发者重新将自己的应用直接编译成目标的机器码。也就是说,其实开发者开发出来的应用程序经过编译和打包之后,仍然是一个包含dex字节码的APK文件。既然应用程序包含的仍然是dex字节码,而ART运行时需要的是本地机器码,这必然要有一个翻译的过程。这个翻译的过程。这个翻译的过程当然不能发生在应用程序运行的时候,否则的话就和Dalvik虚拟机JIT一样,并没有解决性能的问题。在计算机的世界里,与JIT相对的是AOT,即Ahead-Of-Time的简称,它发生在程序运行时之前。我们用静态语言(比如C/C++) 来开发的应用程序的时候,编译器直接就把他们翻译成目标机器码。这种静态语言的编译方式也是AOT的一种。ART运行时并不要求开发者将自己的应用直接编译成目标机器码。这样,将应用的dex字节码翻译成本地机器码的最恰当的AOT时机就是发生在应用安装的时候。在没有ART虚拟机之前,应用的安装过程,其实也会执行一次"翻译"的过程。只不过这个"翻译"过程是将dex字节码进行优化,这也就是由dex文件生成odex文件。这个过程在安装服务PackageManagerService请求守护进程installd来执行的。从这个角度来看,在应用安装的过程中将dex字节码翻译成本地机器码对原来的应用安装流程基本上就不会产生什么影响。
四、启动虚拟机
启动虚拟机主要包括两部分,即
· jni_invocation.Init(NULL):初始化JNI环境
· AndroidRuntime::startVm(...)函数:启动
(二)、Runtime::Init(const RuntimeOptions& raw_options, bool ignore_unrecognized)函数解析
·第一步:初始化内存映射模块
·第二步:调用ParsedOptions的静态函数Create(raw_options, ignore_unrecognized, &runtime_options)来解析ART运行时的启动选项,并且保存变量options指向一个ParsedOptions对象的各个成员变量中
·第三步:原子属性库的初始化
·第四步:Monitor初始化
·第五步:设置runtime_options的一些默认选项
·第六步:创建heap对象。
·第七步:配置一些选项
·获取dump_gc_performance_on_shutdown_ 并配置,XX:DumpGCPerformanceOnShutdown用于传递给dalvikvm,获得应用的GC性能时序
·配置JDWP,即Java Debug Wire Protocol 的缩写,它定义了调试器(debugger)和被调试的Java 虚拟机(target vm)之间的通信协议。
·配置JIT选项,用来提升代码执行效率。如果是5.0之后的,不需要开启该选项,因为ART是需要AOT的,但是也兼容dalvik的dex解释器,所以JIT也是有的
·创建线性分配器 linear_alloc_
·第八步:获取ART的架构
·第九步:初始化信号链
·第十步:根据ART的架构,进行对应信号的处理,所以所有信号都要交给fault_manager来处理。
·第十一步:创建JavaVMExt,这个JavaVMExt实例最终是要返回给调用的,使得调用者可以通过该JavaVMExt实例和ART虚拟机交互
·第十二步:创建一个线程,并且attach线程(attach的过程实际就是创建Thread对象并初始化Thread对象的过程)
·第十三步:attach后通过调用EnableObjectValidation函数来验证heap
·第十四步:创建ClassLinker实例,这是一个非常重要的实例,类的加载、链接和初始化都是在这个类中完成的。如果有BootImageSpace,则调用ClassLinker::InitFromBootImage来完成ClassLinker的初始化,如果没有BootImageSpace,则调用ClassLinker::InitWithoutImage来完成初始化。前者通过ImageSpace来加载系统类;后者是通过boot_class_path,boot_class_path是系统类DexFile数组,ImageSpace的优点是加载快,通过mmap加载一个系统类的镜像文件。
·第十五步:初始化sentinel_的值
·第十六步:如果有MethodTrace选项,则进行相应的配置
·第十七步:如果有Profiler选项,则进行相应的配置
·第十八步:提前分配一个OutOfMemoryError和NoClassDefFoundError
·第十九步:配置NativeBridge中间模块,从Android 5.0,开始在其ART的实现中,引入了一个叫做NativeBridge的中间模块,这个模块基本上就是为了JNI调用时进行动态转码用的,自带了基本上所有的处理逻辑。
可以关注我的公众号:Android架构师成长之路