Android启动过程深入解析

  • 当按下Android设备电源键时究竟发生了什么?
  • Android的启动过程是怎么样的?
  • 什么是Linux内核?
  • 桌面系统linux内核与Android系统linux内核有什么区别?
  • 什么是引导装载程序?
  • 什么是Zygote?
  • 什么是X86以及ARM linux?
  • 什么是init.rc?
  • 什么是系统服务?

当我们想到Android启动过程时,脑海中总是冒出很多疑问。本文将介绍Android的启动过程,希望能帮助你找到上面这些问题的答案。

Android是一个基于Linux的开源操作系统。x86(x86是一系列的基于intel 8086 CPU的计算机微处理器指令集架构)是linux内核部署最常见的系统。然而,所有的Android设备都是运行在ARM处理器(ARM 源自进阶精简指令集机器,源自ARM架构)上,除了英特尔的Xolo设备(http://xolo.in/xolo-x900-features)。Xolo来源自凌动1.6GHz x86处理器。Android设备或者嵌入设备或者基于linux的ARM设备的启动过程与桌面版本相比稍微有些差别。这篇文章中,我将解释Android设备的启动过程。深入linux启动过程是一篇讲桌面linux启动过程的好文。

当你按下电源开关后Android设备执行了以下步骤。

Android启动过程深入解析

第一步:启动电源以及系统启动

当电源按下,引导芯片代码开始从预定义的地方(固化在ROM)开始执行。加载引导程序到RAM,然后执行。

第二步:引导程序

引导程序是在Android操作系统开始运行前的一个小程序。引导程序是运行的第一个程序,因此它是针对特定的主板与芯片的。设备制造商要么使用很受欢迎的引导程序比如redbootubootqi bootloader或者开发自己的引导程序,它不是Android操作系统的一部分。引导程序是OEM厂商或者运营商加锁和限制的地方

引导程序分两个阶段执行。第一个阶段,检测外部的RAM以及加载对第二阶段有用的程序;第二阶段,引导程序设置网络、内存等等。这些对于运行内核是必要的,为了达到特殊的目标,引导程序可以根据配置参数或者输入数据设置内核。

Android引导程序可以在\bootable\bootloader\legacy\usbloader找到。
传统的加载器包含的个文件,需要在这里说明:

  1. init.s初始化堆栈,清零BBS段,调用main.c的_main()函数;
  2. main.c初始化硬件(闹钟、主板、键盘、控制台),创建linux标签。

更多关于Android引导程序的可以在这里了解。

第三步:内核

Android内核与桌面linux内核启动的方式差不多。内核启动时,设置缓存、被保护存储器、计划列表,加载驱动。当内核完成系统设置,它首先在系统文件中寻找”init”文件,然后启动root进程或者系统的第一个进程。

第四步:init进程

init是第一个进程,我们可以说它是root进程或者说有进程的父进程。init进程有两个责任,一是挂载目录,比如/sys、/dev、/proc,二是运行init.rc脚本。

  • init进程可以在/system/core/init找到。
  • init.rc文件可以在/system/core/rootdir/init.rc找到。
  • readme.txt可以在/system/core/init/readme.txt找到。

对于init.rc文件,Android中有特定的格式以及规则。在Android中,我们叫做Android初始化语言。
Android初始化语言由四大类型的声明组成,即Actions(动作)、Commands(命令)、Services(服务)、以及Options(选项)

Action(动作):动作是以命令流程命名的,有一个触发器决定动作是否发生。
语法

 
  1. on <trigger>

  2. <command>

  3. <command>

  4. <command>

Service(服务):服务是init进程启动的程序、当服务退出时init进程会视情况重启服务。
语法

 
  1. service <name> <pathname> [<argument>]*

  2. <option>

  3. <option>

  4. ...

Options(选项)
选项是对服务的描述。它们影响init进程如何以及何时启动服务。
咱们来看看默认的init.rc文件。这里我只列出了主要的事件以及服务。
Table

Action/Service描述
on early-init设置init进程以及它创建的子进程的优先级,设置init进程的安全环境
on init设置全局环境,为cpu accounting创建cgroup(资源控制)挂载点
on fs挂载mtd分区
on post-fs改变系统目录的访问权限
on post-fs-data改变/data目录以及它的子目录的访问权限
on boot基本网络的初始化,内存管理等等
service servicemanager启动系统管理器管理所有的本地服务,比如位置、音频、Shared preference等等…
service zygote启动zygote作为应用进程

在这个阶段你可以在设备的屏幕上看到“Android”logo了。

第五步

在Java中,我们知道不同的虚拟机实例会为不同的应用分配不同的内存。假如Android应用应该尽可能快地启动,但如果Android系统为每一个应用启动不同的Dalvik虚拟机实例,就会消耗大量的内存以及时间。因此,为了克服这个问题,Android系统创造了”Zygote”。Zygote让Dalvik虚拟机共享代码、低内存占用以及最小的启动时间成为可能。Zygote是一个虚拟器进程,正如我们在前一个步骤所说的在系统引导的时候启动。Zygote预加载以及初始化核心库类。通常,这些核心类一般是只读的,也是Android SDK或者核心框架的一部分。在Java虚拟机中,每一个实例都有它自己的核心库类文件和堆对象的拷贝。

Zygote加载进程

  1. 加载ZygoteInit类,源代码:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
  2. registerZygoteSocket()为zygote命令连接注册一个服务器套接字。
  3. preloadClassed “preloaded-classes”是一个简单的包含一系列需要预加载类的文本文件,你可以在<Android Source>/frameworks/base找到“preloaded-classes”文件。
  4. preloadResources() preloadResources也意味着本地主题、布局以及android.R文件中包含的所有东西都会用这个方法加载。

在这个阶段,你可以看到启动动画。

第六步:系统服务或服务

完成了上面几步之后,运行环境请求Zygote运行系统服务。系统服务同时使用native以及java编写,系统服务可以认为是一个进程。同一个系统服务在Android SDK可以以System Services形式获得。系统服务包含了所有的System Services。

Zygote创建新的进程去启动系统服务。你可以在ZygoteInit类的”startSystemServer”方法中找到源代码。

核心服务:

  1. 启动电源管理器;
  2. 创建Activity管理器;
  3. 启动电话注册;
  4. 启动包管理器;
  5. 设置Activity管理服务为系统进程;
  6. 启动上下文管理器;
  7. 启动系统Context Providers;
  8. 启动电池服务;
  9. 启动定时管理器;
  10. 启动传感服务;
  11. 启动窗口管理器;
  12. 启动蓝牙服务;
  13. 启动挂载服务。

其他服务:

  1. 启动状态栏服务;
  2. 启动硬件服务;
  3. 启动网络状态服务;
  4. 启动网络连接服务;
  5. 启动通知管理器;
  6. 启动设备存储监视服务;
  7. 启动定位管理器;
  8. 启动搜索服务;
  9. 启动剪切板服务;
  10. 启动登记服务;
  11. 启动壁纸服务;
  12. 启动音频服务;
  13. 启动耳机监听;
  14. 启动AdbSettingsObserver(处理adb命令)。

第七步:引导完成

一旦系统服务在内存中跑起来了,Android就完成了引导过程。在这个时候“ACTION_BOOT_COMPLETED”开机启动广播就会发出去。

××××××××××××××××××××××××××××××××××××××××××××××××××××××××××

 

Android init源代码分析

 

本文描述init.rc脚本解析以及执行过程,读完本章后,读者应能
(1) 了解init.rc解析过程 (2) 定制init.rc

init.rc介绍 init.rc是一个文本文件,可认为它是Android系统启动脚本。init.rc文件中定义了环境变量配置、系统进程启动,分区挂载,属性配置等诸多内容。init.rc具有特殊的语法。init源码目录下的readme.txt中详细的描述了init启动脚本的语法规则,是试图定制init.rc的开发者的必读资料。
Android启动脚本包括一组文件,包括

init.rc
init.usb.rc
init.trace.rc
init.{hardware}.rc
init.environ.rc
init.zygote32.rc

这些文件可能分布于如下目录中
除init.rc外,其他文件都由init.rc中以import语句导入,一般来说init.rc文件存放通用配置,而其他特定模块以及特定硬件的配置则放置在独立的文件中,这样设计可以使init.rc脚本简洁,方便系统维护和升级。

一个简单的init.rc语法如下。(基于system/core/rootdir/init.rc裁剪)

# Copyright (C) 2012 The Android Open Source Project
#
# IMPORTANT: Do not create world writable files or directories.
# This is a common source of Android security bugs.
#

import /init.environ.rc
import /init.usb.rc
import /init.${ro.hardware}.rc
import /init.${ro.zygote}.rc
import /init.trace.rc
on early-init
    start ueventd

on init
    sysclktz 0
    loglevel 3
    mkdir /system
    mkdir /data 0771 system system
    write /proc/sys/kernel/panic_on_oops 1
# Load properties from /system/ + /factory after fs mount.
on load_all_props_action
    load_all_props
on post-fs
    # once everything is setup, no need to modify /
    mount rootfs rootfs / ro remount
on boot
    # basic network init
    ifup lo
    class_start core
    class_start main
on property:vold.decrypt=trigger_reset_main
    class_reset main
service ueventd /sbin/ueventd
    class core
    critical
on property:ro.debuggable=1
    start console
service debuggerd /system/bin/debuggerd
    class main
service bootanim /system/bin/bootanimation
    class main
    user graphics
    group graphics
    disabled
    oneshot


为了行文方便,下文提及init.rc,通常泛指Android启动脚本。 
init.rc可以定义两类结构:Actions与Services。
Actions
Actions是一组命令的集合,定义一个Actions如下,每个Actions都可以定义一个触发器(trigger),Actions格式如下:

on <trigger>
   <command>
   <command>
   <command>

其中<command>类似于shell命令,command对应一个函数,通常执行一条动作,例如创建一个文件夹等。

触发器(Triggers)
触发器只是一段字符串罢了 PS:在android/system/core/rootdir/下执行
grep -h "^on" --include="*.rc" -r .
可以当前init启动脚本所含有的trigger,如下。

on early-init
on init
on property:sys.boot_from_charger_mode=1
on load_all_props_action
on firmware_mounts_complete
on late-init
on post-fs
on post-fs-data
on boot
on nonencrypted
on property:sys.init_log_level=*
on charger
on property:vold.decrypt=trigger_reset_main
on property:vold.decrypt=trigger_load_persist_props
on property:vold.decrypt=trigger_post_fs_data
on property:vold.decrypt=trigger_restart_min_framework
on property:vold.decrypt=trigger_restart_framework
on property:vold.decrypt=trigger_shutdown_framework
on property:sys.powerctl=*
on property:sys.sysctl.extra_free_kbytes=*
on property:sys.sysctl.tcp_def_init_rwnd=*
on property:ro.debuggable=1
on property:ro.kernel.qemu=1
on boot
on post-fs-data
on property:sys.usb.config=none
on property:sys.usb.config=adb
on property:sys.usb.config=accessory
on property:sys.usb.config=accessory,adb
on property:sys.usb.config=audio_source
on property:sys.usb.config=audio_source,adb
on property:sys.usb.config=accessory,audio_source
on property:sys.usb.config=accessory,audio_source,adb
on property:persist.sys.usb.config=*

根据trigger的不同,可以将Actions大致分为两类: (1) 普通型
这类trigger的的作用仅仅是用于给一个Actions命名,方便查找和引用。如 early-init、init、late-init、early-fs、fs、post-fs、post-fs-data、early-boot、boot、charger等 一般来说,这类Actions将在Android启动时执行,其trigger暗示了执行对应Actions执行的时机。具体的执行流程将在本文最后介绍。 
此外,根据readme.txt描述,还有其他几种trigger,但在init.rc以及init源代码中却没有找到相关代码,如下所示,
device-added-<path> device-removed-<path>     设备节点被添加或移除时调用,。
service-exited-<name>
   这类trigger将在某service退出时执行。关于什么是service稍后介绍。

(2) 属性型
其trigger为property:<name>=<value> 其trigger不仅唯一标识了这个Actions,同时也设定了这类Actons执行的条件,当property <name>的值为<value>时才会被执行。 
Commands

command的格式如下
command-name <parament1> [parament2...]
<>表示必须存在的参数,[]表示可选参数 
说明:readme.txt中虽然有大部分commands的介绍,但并不完整。init.rc中所有commands都在keywords.h中定义,可使用如下命令提取。
sed -n "s/KEYWORD([,]\+[,]\+,[ \t]\+COMMAND.*/\1/p" keywords.h

目前Android4.4支持的Commands如下:

    chdir <direcotory> 改变工作目录
    chroot <directory> 改变当前进程的root目录
    class_start <serviceclass> 如果serviceclass内所有services尚未启动,则启动它
    class_stop  <serviceclass> 停止serviceclass内所有services
    class_reset <serviceclass> 重启serviceclass内所有services
    domainname <name>  设置domain名称
    enable  <servicename>
    exec <path> [ <argument> ]*  fork后执行path所执行的程序,该语句会阻塞直到path指定的程序执行完毕。
    export <name> <value> 设置全局环境变量,将会被该命令后所启动的进程继承。
    hostname <name> 设置主机名
    ifup  <interface> 启动interface所指定的网络接口
    insmod <path> 安装path所指定的内核模块
    mkdir <path> [mode] [owner] [group] 创建path制定的目录,并根据可选的mode、owner、group设定参数。如果未指定可选参数,则创建的文件夹权限将会设置为0755,而owner与group都为root
    mount_all
    mount  <type> <device> <dir> [ <mountoption] *
    powerctl
    restart <service>
    restorecon
    restorecon_recursive
    rm <path> 删除path指定的文件
    rmdir <path> 删除path指定的目录(目录为空才能删除)
    setcon
    setenforce
    setkey
    setprop
    setrlimit
    setsebool
    start <service>
    stop <service>
    swapon_all
    trigger
    symlink
    sysclktz
    wait
    write
    copy
    chown
    chmod
    loglevel
    load_persist_props
    load_all_props


Service   这里的service仅仅是init.rc中的概念,与通常意义上的“服务”概念无关。一个Service对应一个可执行程序,并且可以设定该程序的一些执行性质,如仅仅执行一次、或退出时自动重启。当service所代表的可执行程序在退出时自动重启时,该service通常意味着这是一个守护进程。

service字段由如下格式定义

service <name> <pathname> [ <argument> ]*
   <option>
   <option>
   ...

<name>字段为service的名字,<pathname>为该service对应的二进制程序的路径,随后是该程序的参数列表。
<option>是该service的属性(笔者注:也许用attribute更合适),目前Android4.4中所支持的option如下所示(ps,由 sed -n "s/KEYWORD([,]\+[,]\+,[ \t]\+OPTION.*/\1/p" keywords.h 命令生成)

    capability
    class <name> 设定service的class
    console
    critical
    disabled
    group <groupname> [<groupname>]* 设定进程
    keycodes
    oneshot  service只执行一次
    onrestart 当service终止时自动重启
    seclabeli
    setenv
    socket
    user <username>    
    ioprio


为了方便管理多个service,可为service设定class属性,具有同样class的多个service构成一个组,可以在Actions中通过class_start、class_stop、class_reset等命令启动、停止、重启动。 
此外init.rc中还有其他规则

  • 以#号开头的行为注释行
  • import语句导入其他init脚本文件,
  • \可用于转义换行符
  • 空格与Tab字符都可用作空白字符

前面介绍了init.rc的语法,假如要自己实现一个程序读取init.rc并解析其内容,该如何实现呢?读者不妨思考下。
Android中的实现非常的简洁。解析设计思路如下:

  • 引入section的概念。一个Actions、Service、import是一个section,分别实现不同的section解析代码。
  • 基于行解析,行解析函数与当前所在的section有关,使用函数指针实现。
  • 利用空白字符(一个或多个空行)实现分词,当检测到新的一行时,识别关键词为on、service、import,若是则认为一个section开始了,同时也意味着上一个section终结了。import使用递归实现
  • 每个Actions创建一个struct action数据结构,每个command创建一个struct command数据结构,action中有一个command的链表;每个service创建一个struct service数据结构。
  • 创建全局Actions链表,将识别到Actions都加入全局链表中,创建全局Service链表,将识别到的Service加入到全局链表中

执行思路如下:
创建一个额外的链表充当Actions执行队列action-queue。将一些Actions链表加入到执行队列中。
然后一次从队列中取出一个Actions,依次执行其中每一条command,当该Actions执行完毕后则从actions-queue取出下一个Actions继续执行。

init.rc main函数<part3>代码如下

    INFO("reading config file\n");
    init_parse_config_file("/init.rc")

init_parse_config_file("/init.rc")
在这个函数中将会加载Android系统/init.rc文件。

这个函数将init.rc读入内存,解析其内容(遇到import关键字,则加载对应的文件),
1)Actions 针对Actions,创建struct action数据结构。为Actions的每个command创建一个struct comand数据结构,struct actions存储struct command的链表。 
相关数据结构代码如下
init_parser.c

static list_declare(service_list);
static list_declare(action_list);
static list_declare(action_queue);

service_list是全局service链表,解析启动脚本过程中,service对应数据结构struct service将会挂载到这里。
action_list是全局Actions链表,Actions对应数据结构struct action将会挂载到这里。
至于action_queue在解析完毕之后,实际执行Actions时才会用到。

init.h


struct command
{
        /* list of commands in an action */
    struct listnode clist;

    int (*func)(int nargs, char **args);

    int line;
    const char *filename;

    int nargs;
    char *args[1];
};

struct action {
        /* node in list of all actions */
    struct listnode alist;
        /* node in the queue of pending actions */
    struct listnode qlist;
        /* node in list of actions for a trigger */
    struct listnode tlist;

    unsigned hash;
    const char *name;

    struct listnode commands;
    struct command *current;
};


struct command各字段含义如下: clist是“挂载”于链表的钩子
line变量存放对应的command语句在init脚本文件(可能是init.rc、init.usb.rc等)行号。
filename存放所在init脚本文件的名字
nargs存放command语句所表示的命令对应的函数的参数个数,
args是struct command的最后一个变量,看起来是一个单元素的数组,但是在init.rc解析过程中,确定nargs后,使用malloc申请内存时,args所表示的数组长度将是 n+1个。(与0长度数组的实现类似)。 func字段存放command语句所对应的函数指针。

struct actions各字段含义如下:
alist变量是action_list链表钩子  qlist是action_queue链表钩子
类似, tlist的也是一个特殊链表的钩子
hash在目前init实现中未使用
name存放Actions的名字(也就是Actions的trigger) commands存放该Actions所有struct command链表
current在Actions执行时,存储当前正在被执行的struct command指针。  
actions_list是一个双向链表,其每一个节点都是struct action结构的alist对象,当init.rc解析完毕之后,二者效果如下所示。为了简单,链表只绘制了两个节点。

 

init_parse_config_file执行完毕后,解析出启动脚本中所有的Actions,并构造出上图所示的数据结构。可见利用action_list,可以遍历启动脚本中所有Actions,并能遍历出单个Actions中的所有command。 
2)service
init在解析init启动脚本,为每个service字段创建一个struct service数据结构

struct service {
        /* list of all services */
    struct listnode slist; //用于挂载于service_list的钩子
    const char *name; //存放service的名称
    const char *classname; //用于存放该service所隶属的class的名称
    unsigned flags; //位图变量,其各个位代表不同的servcie的属性(对应service中的option字段)
    pid_t pid; //当service对应的程序执行时,存放其进程号
    time_t time_started;    /* time of last start */ //存放进程启动时间
    time_t time_crashed;    /* first crash within inspection window */ //存放第一次进程崩溃时间
    int nr_crashed;         /* number of times crashed within window */ //存放进程崩溃次数
    
    uid_t uid; //该servcie对应进程的uid
    gid_t gid; //该service对应进程的gidinit_parse_config_file("/init.rc");
    gid_t supp_gids[NR_SVC_SUPP_GIDS];//该service对应进程的附加群组id
    size_t nr_supp_gids; //该service所隶属的附件组的数目
    char *seclabel; //存放selinux所需要的security context
    struct socketinfo *sockets;    struct svcenvinfo *envvars;

    struct action onrestart;  /* Actions to execute on restart. */
    
    /* keycodes for triggering this service via /dev/keychord */
    int *keycodes;"queue_property_triggers"
    int nkeycodes;
    int keychord_id;

    int ioprio_class;
    int ioprio_pri;
    int nargs; //对应service语句传入的参数数目
    /* "MUST BE AT THE END OF THE STRUCT" */
    char *args[1]; //存放service语句实际传入的参数,其长度将会被修正为nargs+1
}; /*     ^-------'args' MUST be at the end of this struct! */


解析完毕后,构造出如下所示的数据结构(示意图仅画链表仅绘制两个节点)"queue_property_triggers" 
 

一个比较奇怪的地方在于,为什么在struct service中为什么出现了struct action?这与service的onrestart属性有关,该属性比较特殊,在init.rc存在如下service:

service servicemanager /system/bin/servicemanager
    class core
    user system
    group system
    critical
    onrestart restart healthd
    onrestart restart zygote
    onrestart restart media
    onrestart restart surfaceflinger
    onrestart restart drm

onrestart属性后必须跟restart关键字,随后必须再跟一个service名称。 这段含义是定义一个名为servicemanager的service,对应的可执行程序为/system/bin/servicemanager,其多个option则设定servicemanager进程退出时,重启healthd、zygote、media、surfaceflinger、drm等service。
每个service均对应一个可执行程序,类似与一个Actions中的command,多条onrestart语句即多个command,可以认为一个service似乎隐含一个Actions,因此在struct service中包含了一个struct action,从而代码复用。

每个属性(option)由struct service中flags的一位表示,flags各个位含义如下(由于英文比较直观,不具体翻译了)

//init.h
#define SVC_DISABLED    0x01  /* do not autostart with class */
#define SVC_ONESHOT     0x02  /* do not restart on exit */
#define SVC_RUNNING     0x04  /* currently active */
#define SVC_RESTARTING  0x08  /* waiting to restart */
#define SVC_CONSOLE     0x10  /* requires console */
#define SVC_CRITICAL    0x20  /* will reboot into recovery if keeps crashing */
#define SVC_RESET       0x40  /* Use when stopping a process, but not disabling
                                 so it can be restarted with its class */
#define SVC_RC_DISABLED 0x80  /* Remember if the disabled flag was set in the rc script */
#define SVC_RESTART     0x100 /* Use to safely restart (stop, wait, start) a service */
#define SVC_DISABLED_START 0x200 /* a start was requested but it was disabled at the time */


至此函数init_parse_config_file("/init.rc")核心功能介绍完毕,其具体代码就不赘述了。感兴趣的代码可以深入分析init.rc解析引擎的实现,非常有意思。 再强调一下,该函数执行在解析init启动脚本后,只是针对Actions和service创建数据结构,并分别添加到全局链表action_list与service_list中,仅此而已,并没有执行任何一条Actions command,或是启动任何一个service进程。此时action_queue还未添加任何有效节点。 
继续分析代码init.c随后的代码。 init.c main函数代码


    action_for_each_trigger("early-init", action_add_queue_tail);

    queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done");
    queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_lin一部分在ux_rng");
    queue_builtin_action(keychord_init_action, "keychord_init");
    queue_builtin_action(console_init_action, "console_init");

    /* execute all the boot actions to get us started */
    action_for_each_trigger("init", action_add_queue_tail);

    /* skip mounting filesystems in charger mode */
    if (!is_charger) {
        action_for_each_trigger("early-fs", action_add_queue_tail);
        action_for_each_trigger("fs", action_add_queue_tail);
        action_for_each_trigger("post-fs", action_add_queue_tail);
        action_for_each_trigger("post-fs-data", action_add_queue_tail);
    }

    /* Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
     * wasn't ready immediately after wait_for_coldboot_done
     */
    queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");

    queue_builtin_action(property_service_init_action, "property_service_init");
    queue_builtin_action(signal_init_action, "signal_init");
    queue_builtin_action(check_startup_action, "check_startup");

    if (is_charger) {
        action_for_each_trigger("charger", action_add_queue_tail);
    } else {
        action_for_each_trigger("early-boot", action_add_queue_tail);
        action_for_each_trigger("boot", action_add_queue_tail);
    }

        /* run all property triggers based on current state of the properties */
    queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");


#if BOOTCHART
    queue_builtin_action(bootchart_init_action, "bootchart_init");
#endif


void action_for_each_trigger(const char *trigger, void (*func)(struct action *act))
该函数利用字符串trigger从从action_list中查找特定Actions,为每个符合条件的Actions执行action_add_queue_tail函数,也就是说将action_list中所有触发器为trigger的Actions添加到action_queue中。 
void queue_builtin_action(int (*func)(int nargs, char **args), char *name)
该函数完成两个操作:
1)构造一个触发器为name的struct action结构体,并创建一个struct command,对应函数为行参func
2)将struct action添加到action_queue链表末尾。

从名字上来看action_queue就是Actions队列,而队列具有FIFO特性。事实也是如此,action_queue可以用于控制Actions执行的顺序。 
上述代码结束后,action_queue链表最终具有如下形式,每个链表都是struct action数据结构,各节点通过struct action中的qlist变量彼此连接。   
白色节点来自init启动脚本,橘红色节点则有函数queue_builtin_action创建。

现在万事具备,只欠东风。接下来就是如何执行这些Actions了。
init.c main


    for(;;) {
        execute_one_command();
        ...
    }


execute_one_command函数分析
概括的说,这个函数每次从action_queue开头取出一个Actions,同时移除对应节点,依次执行每个command。由与main函数中for循环语句的存在,最终所有的action_queue中所有Actions都会被执行。大致执行流程如此。

简要总结下:init进程的逻辑时,任何一个Actions想要被执行,只要把自己放到action_queue中,然后在init进程的for循环中就会在之后的某个时间被执行。 
细心的读者已经发现,为什么action_queue中的Actions如此之少(请参考本文前面介绍Actions时),至少“post-fs”、“post-fs-data”、“boot”,以及"property:<name>=<value>”等Actions怎么没有?

这是因为init进程耍了花招,它把这部分Actions的触发放到了init.rc里,由Actions的一条特殊command: "trigger"触发。请注意,该trigger与Actions的名字(也称为trigger)不同。 见init.rc


on late-init
    trigger early-fs
    trigger fs
    trigger post-fs
    trigger post-fs-data

    # Load properties from /system/ + /factory after fs mount. Place
    # this in another action so that the load will be scheduled after the prior
    # issued fs triggers have completed.
    trigger load_all_props_action

    # Remove a file to wake up anything waiting for firmware.
    trigger firmware_mounts_complete

    trigger early-boot
    trigger boot


看到这里,先大胆猜测当init进程执行到“late-init” Actions时,对于每条trigger语句,取出后面所跟的字符串,从action_list(“妈蛋,终于想起我来了”)中找出该Actions对应的struct action结构,添加到action_queue中。快去看看代码确定一下是否如此。怎么找到command对应代码?前面提到init启动脚本所有关键字都keyword.h中,

//keyword.h
//-------------
    KEYWORD(trigger,     COMMAND, 1, do_trigger)
//builtins.c
//-------------
int do_trigger(int nargs, char **args)
{
    action_for_each_trigger(args[1], action_add_queue_tail);
    return 0;
}


bingo!原来trigger命令对应的函数do_trigger中再次调用了action_for_each_trigger,可见之前的猜测正确! 
至于"property:<name>=<value>”类型的Actions由于需要在属性条件满足时被执行,在android4.4的init的实现中对这类Actions的处理比较特殊,有两种方式:
(1) 通过action_queue,配合execute_one_command处理
参考上面action_queue链表图,在main函数中使用queue_builtin_action创建了一个特殊的Actions:"queue_property_triggers",该Actions内只有一个command,对应函数为queue_property_triggers_action,代码如下:

static int queue_property_triggers_action(int nargs, char **args)
{
    queue_all_property_triggers();
    /* enable property triggers */
    property_triggers_enabled = 1;
    return 0;
}
void queue_all_property_triggers()
{
    struct listnode *node;
    struct action *act;
    list_for_each(node, &action_list) {
        act = node_to_item(node, struct action, alist);
        if (!strncmp(act->name, "property:", strlen("property:"))) {
            /* parse property name and value
               syntax is property:<name>=<value> */
            const char* name = act->name + strlen("property:");
            const char* equals = strchr(name, '=');
            if (equals) {
                char prop_name[PROP_NAME_MAX + 1];
                char value[PROP_VALUE_MAX];新浪微博
                int length = equals - name;
                if (length > PROP_NAME_MAX) {
                    ERROR("property name too long in trigger %s", act->name);
                } else {
                    int ret;
                    memcpy(prop_name, name, length);
                    prop_name[length] = 0;

                    /* does the property exist, and match the trigger value? */
                    ret = property_get(prop_name, value);
                    if (ret > 0 && (!strcmp(equals + 1, value) ||
                                    !strcmp(equals + 1, "*"))) {
                        action_add_queue_tail(act);
                    }
                }
            }
        }
    }
}

可见queue_all_property_triggers函数中会检查从action_list中查找所有property型的Actions,并判断其属性条件是否满足,若满足则使用action_add_queue_tail(act)将其添加到action_queue中。

仔细考虑一下,这种方式并不能保证所有property型的Actions都能在属性满足后被触发执行,参考action_queue链表图,这种方式仅仅能保证在"queue_property_triggers"之前的Actions执行中所设置的属性。设想这种情况,在init.rc添加某个Actions,希望可以在Android命令行终端中设置属性(android的set_property命令可以设置android属性),从而触发某个动作。仅仅通过action_queue配合execute_one_command是难以优雅实现的,所以init进程使用了第二种方式。
思考:非优雅的实现最简单方式,只要在for循环中定时轮询属性,但是延时间隔多少合适?若是间隔太短,则将会相当浪费CPU,若是间隔太长,则会导致设置某个属性到执行其对应Actions的时间过长。优雅的方式可以利用进程见通信机制避免CPU被浪费,并能保证响应的及时性。

(2) 借助进程间通信方式实现
首先细化init.c main函数中for循环代码,大致如下:

int main(int argc, char* argv[])
{
    ....
    for(;;) {
        execute_one_command();
        ....
        nr = poll(ufds, fd_count, timeout);
        if (nr <= 0)
            continue;
        for (i = 0; i < fd_count; i++) {
            if (ufds[i].revents == POLLIN) {这部分内容请阅读
                if (ufds[i].fd == get_property_set_fd())
                    handle_property_set_fd();
                ...
            }
        }
    }
}


当某个程序(进程)调用property_set(libcutils库)来自设置属性,就会给init进程发送一个消息(通过Unix domain socket),最终init接收到整个消息后,会调用
handle_property_set_fd() -> property_set(来自init/property_service.c)->  property_changed() -> queue_property_triggers()->action_add_queue_tail
在queue_property_triggers函数中从全局action_list中匹配 property:<name>=<value>类型的Actions,如果属性数值满足,则将该Actions加入到action-queue中,这样该Actions中的command将会在之后的execute_one_command()被调用。

在本文中涉及很多Android属性的操作,笔者将在文章《Android init源代码分析(3)属性系统》进一步分析Android的属性系统。 此外还有些问题需要解决: (1). service将如何执行,并且service存在多种属性,当包含restart属性时,init进程是如何重启的? (2). for循环中execute_one_command之后的大量代码的含义?

第一个问题将在文章《Android init源代码分析(4)service的处理》中具体分析。至于for循环中其他代码,将分散在init源代码分析(3)与(4)这两篇文章中。

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值