Android核心服务解析篇(三)——Android系统的启动(一)

简介: Android核心服务解析篇(三)——Android系统的启动(一)

从大的方面来说,Android系统的启动可以分为两个部分:第一部分是Linux核心的启动,第二部分是Android系统的启动。第一部分主要包括系统引导,核心和驱动程序等,由于它们不属于本篇要讲的内容,这里就不再讨论。在本篇博客中,我们重点讲解Android系统的启动,这一过程主要经过两个阶段,分别是应用的初始化流程与system_service进程及核心服务的创建流程。


1.初始化流程


初始化流程,顾名思义,它完成Android的一些初始化工作,包括设置必要的环境变量,启动必要的服务进程,挂载必要的设备等,而这些工作将会为整个Android打下坚实的基础。


①应用的初始化流程

在核心启动完成以后,将进入Android文件系统及应用的初始化流程,此时将会转向执行init.c中的main()函数(路径:/system/core/init/init.c),该函数的执行流程如下图所示:


1.png

2.png

下面我们了解一下上图中的注解


注解1:/dev表示设备文件系统或者udev挂载点,/proc用来挂载存放系统过程性信息的虚拟文件系统,/sys用于挂载“sysfs文件系统”。由于前面调用了umask(0),因此mkdir(“/dev”,0755)得到的权限应该是0755.


注解2:init.rc的解析结果是形成action_list(on关键字相关的部分),service_list(service_关键字相关的部分)以及action_queue(需要执行的命令或服务),以便后续流程使用。


注解3:解析/proc/cmdline文件,将其中的属性导入Android系统全局变量。


注解4:


Ⅰget_hardware_name()方法用于解析/proc/cpuinfo文件获取硬件信息,并用于拼接成一个init.<hardware_name>.rc文件,继续解析。


Ⅱ在解析init.rc文件的过程中,系统会根据该文件的内容形成一些需要命令,动作或者触发器的列表并将这些存入在内在中,以便在必要的时候使用。不同的厂商可能根据不同的硬件需求定制不同的.rc文件,这些.rc文件的名称一般为“init.<hardware_name>.rc”,而解析这些.rc文件的结果同样也会形成一些命令,动作或者触发器的列表,而这些列表将会合并解析init.rc所得的命令和动作的列表中,并且形成最终需要执行的命令和动作。


注解5:添加顺序为:early-init下的所有动作,wait_for_coldboot_done_action,property_init_action,keychord_init_action,console_init_action,set_init_properties_action,init下的动作,property_service_init_action,signal_init_action,check_startup_action,early-boot下的所有动作,boot下的所有动作,queue_property_triggers_action。这些动作组成了开机过程中看到的设备的状态,比如开机动画等。


注解6:这里会启动执行设置属性,创建或挂载动作以及启动服务等操作。需要注意是的这里启动的服务包括最重要的servicemanager和zygote服务进程。


至此,init进程进入死循环中处理一些消息以等待命令的到来。在这个过程中,我们将要了解以下知识。


Ⅰ在init运行的过程中产生了许多服务,它们是整个Android的基础,分别是ueventd,console,adbd,servicemanager,vold,netd,debuggerd,ril-daemon,surfaceflinger,zygote,drm,media,bootanim,dbus,bluetoothd,installd,flash_recovery,racoon,mtpd,keystore和dumstate。


Ⅱ整个init的行为甚至整个Android核心的属性都受到启动脚本init.rc的影响。


下面我们就重点介绍zygote的启动行为,详细了解init.rc的语法。


②init.rc的用法


Android初始化语言由声明的4个类型组成,它们分别是动作(action),命令(command),服务(service),和选项(option),以#开头的行表示注释。动作和服务声明新的一节并且有唯一的名字,所有的命令或者选项属于最近声明的节。如果下一个动作或者服务的名字已存在(也就是重名),则它将作为错误被忽略。


Ⅰ动作


动作是命令序列,它有一个触发器,用于确定行动应在何时发生。当发生某一个事件时,它可以匹配到一个动作触发器,并且该动作会被添加到要执行队列的尾部(除非它已经在队列中了)。


队列中的每个动作是按顺序出列的,具体如下所示:

on early-init
write /proc/1/oom-adj -16
setcon u:r:init:s0
start ueventd


动作表现为以下的形式:

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

触发器是一些字符串,这些字符串可用于匹配一定类型的事件,并且用于触发动作。下表罗列了一些触发器的定义。

触发器

说明
boot 当初始化流程触发的时候,boot是首先被触发的动作(在完成/init.conf文件加载之后)
<name>=<value> 当以<name>命名的属性被设为特定的值<value>时,该触发器发生
device-added-<path> 当添加设备节点时,device-added-<path>定义的触发器运行
device-removed-<path> 当移除设备节点时,device-removed-<path>定义的触发器运行
service-exited-<name> 当指定的服务退出时,service-exited-<name>类型的触发器运行
<string> 自定义的触发器,可由init代码负责管理


Ⅱ命令


命令是组成动作的成员,也就是说,动作由一个个命令组成。下表罗列了动作支持的命令。

命令 说明
exec  <path> [<argument>]*

fork并执行程序(<path>)。这在程序完成执行之前将阻塞一切进程,因此最好避免使用exec命令。该命令中两个参数的含义如下所示。

❶<path>:可执行文件的路径

❷[<argument>]*:可执行文件所需的参数,参数个数可以是0或者多个

export <name> <value> 设置名字为<name>的环境变量为<value>
ifup <interface> 打开网络接口<interface>
import <filename> 解析一个初始化配置文件,导入系统中
hostname <name> 设置主机名
chdir <directory> 修改工作目录,它的功能和cd命令一样
chmod <octal-mode> <path> 修改文件的访问权限
chown <owner> <group> <path> 修改<path>指定的问题的所有者和组
chroot <directory> 修改进程根目录为<directory>
class_start <serviceclass> 启动<serviceclass>类别的服务,如果它们没有运行的话
class_stio <serviceclass> 停止<serviceclass>类别的服务,如果它们已经处于运行状态的话
domainname <name> 设置域名
insmod <path> 在<path>上安装模块
mkdir <path> [mode] [owner] [group] 创建一个目录,其中目录路径以及名称由<path>指明。这里可以通过参数给定目录的模式,所有者和组。如果没有提供[mode] [owner] [group],则用权限755来创建目录,并且它属于root用户root组
mount <type> <device> <dir> [<mountoption>]* 尝试在目录<dir>上挂载被命名的设备,<device>可能是mtd@name的形式,以便指定名为mtd块的设备。<mountoption>包括ro,rw,remount和noatime等
setkey TBD
setprop <name> <value> 设置系统属性<name> 为<value>
setrlimit <resource> <cur> <max> 设置指定资源的使用限制
start <service> 启动指定的服务,如果服务还没有运行的话
stop <service> 停止指定的服务,如果服务目前正在运行的话
symlink <target> <path> 用值<target>来在<path>上创建一个符号链接
sysclktz <mins_west_of_gmt> 设置系统闹钟基准(如果系统闹钟为GMT,则为0)
trigger <event> 触发一个事件。用于执行该触发器中的操作
write <path> <string> [<string>]* 在<path>上打开文件并且用write(2)来将一个或多个字符串写到文件上。

在init.rc中,Android 定义了若干动作,并且这些动作用于完成Android的初始化工作。下面以其中一个动作的配置来说明一下:


on fs
mount yaffs2 mtd@system /system
mount yaffs2 mtd@system /system ro remount
mount yaffs2 mtd@userdata /data nosuid nodev
mount yaffs2 mtd@cache /cache nosuid nodev


这个例子配置了一个触发器为fs的动作,它由4条命令组成,这4条命令都使用mount命令挂载设备。


Ⅲ服务


服务是一些程序,当它们退出的时候,init启动并且(选择性地)重新启动。服务表现为以下形式:

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


其中各个参数的含义如下所示:


❶<name>:为服务指定一个名字。


❷<pathname>:指定服务需要执行的文件路径。


❸[<argument>]*:启动服务所需要的参数,参数个数可以是0个或者多个。


Ⅳ选项


选项是服务的修改器,可以影响如何以及何时初始化运行服务。下表罗列了选项列表。


选项

说明
critical 这是一个对于设备来说比较关键的服务,如果它在4分钟内退出超过4次,那么设备将重新启动并进入recovery模式。
disabled 这个服务不能通过类别自动启动,它必须通过服务名字来显示启动
setenv <name> <value> 设置启动进程中环境变量(由<name>指定)的值为<value>
socket <name> <type> <perm> [<user> [<group>]] 创建名为/dev/socket/<name>的一个Unix域端口并且将它的fd传递到被启动的进程上
<type>必须是dgram,stream,seqpacket.设置用户和组的默认值为0
user <username> 在执行该服务之前变换用户名,如果进程需要Linux的能力,就不能使用该命令
group <groupname> [<groupname>]* 在执行该服务之前变换组名
oneshot 在服务退出时不要重新启动它
class <name> 为服务指定一个类名。一个被命名的类中的所有服务都可以一起被启动或停止。如果服务没有通过类选项来指定的话,它是在类default中的
onrestart 当服务重新启动时,执行一条命令

下面以init.rc文件中的配置为例简要说明一个服务的配置:

service zygote /system/bin/app_process -Xzygote /system/bin -zygote
--start-system-server
class main
socket zygote stream 666
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media 
onrestart restart netd


在上面代码中,第一行配置了一个名为zygote的服务,这个服务将会运行/system/bin/app_process,剩余部分为参数(以空格分割)。


剩下的几行代码声明了此服务的选项。这说明zygote是一个类型为main的服务(classmain)并且它会创建一个socket,这个socket的类型为stream,权限为666(socket zygote stream 666)。当重启此服务的时候,需要完成以下事情。


❶写/sys.android_power/request_state为wake


❷写/sys/power/state为on


❸重新启动media服务


❹重新启动netd服务


init.rc文件需要在init启动期被解析成系统可以识别的数据结构。前面我们读懂了init.rc的含义,下面我们就来看看init是如何保存和组织这些信息的,首先,我们来看看在init中如何表示动作,服务和命令,如下表所示:

组件

数据结构 说明

列表节点(listnode)

<<struct>>

listnode+next:listnode

+prev:listnode

listnode是一个表示位置的数据结构,可以用来定义不同类型节点(比如动作或者服务)的执行顺序

从左侧的数据结构中可以看出,这里包含了两个listnode的指针,它们用于指向前一个和后一个将要执行的节点

这些信息将帮助各种节点(动作,服务,以及命令等)组成一个双向循环列表

动作(action)

<<struct>>

action+alist::listnode

+qlist:listnode

+tlist:listnode

+hash:signed int

+*name:char

+commands:listnode

+*current:command

action中包含4个表示节点位置信息的节点,它们分别表示它本身在所有动作中的位置(alist),在添加动作的队列中的位置(qlist),以及在某个触发器中的所有动作列表的位置(tlist)

action 数据结构中包含了其他的重要信息,比如动作的名字(name),包含的所有命令列表(commands)以及当前命令

服务(service)

<<struct>>

service

+slist:listnode

+*name:char

+*classname:char

+flags:unsigned

+pid:pid_t

+time_started:time_t

+time_crashed:time_t

+nr_crashed:int

+uid:uid_t

+gid:gid_t

+supp_gids[NR_SVC_SUPP_GIDS]:gid_t

+*sockets:socketinfo

+*envvars:svcenvinfo

+onrestart:action

+*keycodes:int

+nkeycodes:int

+keychord_id:int

+ioprio_class:int

+ioprio_pri:int

+nargs:int

+*arg[1]:char

这个数据结构中包含了服务的信息,主要包括如下内容:


❶该服务在所有服务列表中逻辑位置的数据结构“listnode”(slist)


❷服务的基本信息,比如服务的名称,进程的相关信息,所需要参数信息等

命令(command)

<<struct>>

command+clist:listnode

+(*func)(int nargs,char **args):iint

+nargs:int

+*args[1]:char

这个数据结构中包括以下内容:


❶节点的位置信息(clist)


❷命令需要执行的函数的函数指针(func)


❸参数信息:nargs和args[1]


最后,我们通过解析init.rc中的一个片段来说明解析过程。


开始解析之前,需要了解整个解析过程至关重要的一个数据结构,那就是parse_state,它保存了整个解析过程中所处的状态,下图显示了它的“成分”

<<struct>>
parse_state
+*ptr:char
+*text:char
+line:int
+nexttoken:int
+*context:void
+(*parse_line)(struct parse_state *state,int nargs,char **args):void
+*filename:char


③用init解析整个init.rc文件


现在我们 回到init启动的初期,这里它调用了init_parse_config_file()方法,而这个方法就是解析init.rc文件的入口。用init解析整个init.rc文件的流程如下图所示。


3.png

4.png


下面我们了解一下上图中的注解、


注解1:state是一个被命名为parser_satte的结构体,用于保存当前文件的解析状态信息,包括解析的文件(filename),当前解析的行号(line),当前解析的文字指针(ptr),指示下一个动作的变量(nexttoken)以及解析这一行需要的函数指针(parse_line)等。


注解2:next_token()函数位于/system/core/init/parse.c中,用于分析init.rc文件的内容。它只返回3个状态,分别是:T_EOF(文件结束),T_NEWLINE(一行结束)和T_TEXT(表示遇到第一个空格)。


注解3:init.rc中每一行的信息通过空格被分割为若干段,而这些信息共同组成args[INIT_PARSER_MAXARGS]的内容,并由nargs计数。例如on fs经过解析后,这一行分为两段(分别是on和fs),分别存放在args中,计数器的值为2.。


注解4:init.rc的每一行经过分割后,需要分析其类型(由lookup_keyword返回)。/system/core/init/keywords.h中定义了所有关键字的类型。在片段KEYWORD(on,SECTION,0,0)中,on关键字是一个SECTION,有0个(也就是不需要)参数,没有对应的触发函数(也就是最后一个0)。


注解5:state.parse_line是一个函数的指针,可以根据关键字指向两种不同的解析方法——parse_line_service(处理服务的选项)和parse_line_action(处理行为的命令)。按照这个流程,init完成整个init.rc文件的解析,并生成service_list和action_list,后续流程所需要的信息将从这两个列表中获取,将需要执行的命令或启动的服务加入action_queue中,这样就完成了Android系统基础部分的启动。


在启动的过程中,需要特别注意的是,我们通过action_for_each_trigger()方法声明需要执行的命令队列,该方法的代码如下所示:

void action_for_each_trigger(const char * trigger,void (*func)(struct action *act)){
struct listnode * node;
struct action *act;
list_for_each(node,&action_list){
act=node_to_item(node,struct action,alist);
if(!strcmp(act->name,trigger)){
func(act);
}
}
}


在上述代码中,list_for_each()用于遍历action_list中的每一个节点,返回节点在列表中的位置信息,然后通过node_to_item()方法生成一个action的信息,最后执行func()函数。


action_for_each_trigger()方法在init.rc中是这样调用的:


action_for_each_trigger(early-init,action_add_queue_tail);

它的含义是,在action_list中查找名字为early-init的节点,并将其信息通过action_add_queue_tail()方法加入action_queue队列的尾部。


然后在init的无限循环中遍历action_queue中的每一个节点,执行它们所包含的命令。


讲到这里,我们了解了init如何对待init.rc文件的内容。下面扩展一下知识,概要介绍一下Android系统中*.rc文件的关键字及其使用需求,如下表。如果你想修改或编写自己的.rc文件,那么请关注下表。

关键字

类型

参数个数

capability

OPTION

0

chdir

COMMAND

1

chroot

COMMAND

1

class

OPTION

0

class_start

COMMAND

1

class_stop

COMMAND

1

class_reset

COMMAND

1

console

OPTION

0

critical

OPTION

0

disabled

OPTION

0

domainname

COMMAND

1

exec

COMMAND

1

export

COMMAND

2

group

OPTION

0

hostname

COMMAND

1

ifup

COMMAND

1

insmod

COMMAND

1

import

SECTION

1

keycodes

OPTION

0

mkdir

COMMAND

1

mount

COMMAND

3

on

SECTION

0

oneshot

OPTION

0

onrestart

OPTION

0

restart

COMMAND

1

rm

COMMAND

1

rmdir

COMMAND

1

service

SECTION

0

setenv

OPTION

2

setkey

COMMAND

0

setprop

COMMAND

2

setrlimit

COMMAND

3

socket

OPTION

0

start

COMMAND

1

stop

COMMAND

1

trigger

COMMAND

1

Symlink

COMMAND

1

sysclktz

COMMAND

1

user

OPTION

0

wait

COMMAND

1

write

COMMAND

2

copy

COMMAND

2

chown

COMMAND

2

chmod

COMMAND

2

loglevel

COMMAND

1

load_persist_props

COMMAND

0

ioprio

OPTION

0


相关文章
|
3天前
|
Shell Android开发
Android系统 adb shell push/pull 禁止特定文件
Android系统 adb shell push/pull 禁止特定文件
16 1
|
4天前
|
Android开发
Android 11 添加Service服务SELinux问题
Android 11 添加Service服务SELinux问题
13 1
|
14天前
|
监控 负载均衡 Cloud Native
ZooKeeper分布式协调服务详解:面试经验与必备知识点解析
【4月更文挑战第9天】本文深入剖析ZooKeeper分布式协调服务原理,涵盖核心概念如Server、Client、ZNode、ACL、Watcher,以及ZAB协议在一致性、会话管理、Leader选举中的作用。讨论ZooKeeper数据模型、操作、会话管理、集群部署与管理、性能调优和监控。同时,文章探讨了ZooKeeper在分布式锁、队列、服务注册与发现等场景的应用,并在面试方面分析了与其它服务的区别、实战挑战及解决方案。附带Java客户端实现分布式锁的代码示例,助力提升面试表现。
30 2
|
3天前
|
存储 Java Android开发
Android系统 设置第三方应用为默认Launcher实现和原理分析
Android系统 设置第三方应用为默认Launcher实现和原理分析
18 0
|
3天前
|
Android开发
Android构建系统:Android.mk(2)函数详解
Android构建系统:Android.mk(2)函数详解
12 1
|
3天前
|
存储 Java API
Android系统 文件访问权限笔记
Android系统 文件访问权限笔记
35 1
|
3天前
|
移动开发 Java Unix
Android系统 自动加载自定义JAR文件
Android系统 自动加载自定义JAR文件
21 1
|
3天前
|
Shell Android开发 开发者
Android系统 自定义动态修改init.custom.rc
Android系统 自定义动态修改init.custom.rc
23 0
|
3天前
|
测试技术 Android开发 开发者
RK3568 Android系统客制化动态替换ro任意属性
RK3568 Android系统客制化动态替换ro任意属性
24 1
|
3天前
|
存储 Linux Android开发
RK3568 Android/Linux 系统动态更换 U-Boot/Kernel Logo
RK3568 Android/Linux 系统动态更换 U-Boot/Kernel Logo
18 0

推荐镜像

更多