【翻译】Android Init Language
Android Init Language
在Android系统启动的时候,会进行一系列的初始化操作,这些操作通常都是有rc文件来声明的。而rc文件中描述的规则就是本文所讲的Android Init Language。
原文链接: AOSP/system/core/init/README.md
Android 初始化语言由五大类的语句组成:Actions,Commands,Services,Options,Imports。
所有的这些分类都是以行为单位的,中间使用空格进行分割。可以使用C语言样式的反斜杠转义符来在命令里插入空格,也可以使用双引号来避免文本被分割成多个指令。当反斜杠在行尾的时候,可以实现命令的换行。
以#开头的行是注释内容(#前面允许有空格)。
系统属性可以用${property.name}的形式使用,这也适合在需要串联多个文件的场景中使用,如:import /init.recovery.${ro.hardware}.rc
Actions和Services都会默认生成一个上下文,所有的Commands和Options都属于它最近的上下文。在第一个上下文的前面声明的Commands和Options都会被忽略掉。
Sercices具有唯一的名字,如果另一个Service定义时名字已经被定义过了,则该Service会被忽略掉并记录在log中。
init.rc文件
Android Init 语言被用在以rc为后缀的文本文件中,在系统中有多个地方用到了这些文件,如下所述:
/system/etc/init/hw/init.rc是主要的.rc文件,他在init进程执行的最开始的位置被加载,负责系统的初始化设置。
init进程在加载完system/etc/init/hw/init.rc之后立即加载{system, system_ext, vendor, odm, product}/etc/init/目录下的所有rc文件。在后面的Imports章节中会具体解释这些。
没有第一阶段挂载机制的旧设备可以在mount_all的时候导入init脚本,然而这种方式已经被废弃了,并且在Android Q之后不再支持。
这些路径的作用:
/system/etc/init/用来初始化核心系统项,如SurfaceFlinger、MediaService、logd等。/vendor/etc/init/用来初始化SoC供应商的服务项,如核心Soc功能所需的操作或者守护进程等。odm/etc/init用来初始化设备制造商的项目,如运动传感器或其他外围功能所需的操作或者守护进程等。
在system、vendor或者odm分区上的所有的二进制服务,都应该有对应的声明在rc文件中的服务入口,这些rc文件应该位于他们所在分区的/etc/init/目录下。编译系统的时候会有一个宏LOCAL_INIT_RC,来帮助开发者处理这些rc文件。每个用来初始化的rc文件都应该包含与他的服务相关联的所有的操作。
例如,logcatd.rc和Android.mk文件位于system/core/locat目录中。在userdebug模式下编译系统时,在Android.mk中的LOCAL_INIT_RC宏就会将logcatd.rc文件放在/system/etc/init中。在mount_all命令阶段Init进程会加载logcatd.rc,并且在适当的时机将服务的操作加入到队列中运行。
根据服务的守护进程来将init.rc文件进行区分比以前使用一个完整的init.rc更好一些,这种方式确保了init进程通过该文件读取到唯一的服务入口和操作,并对应于其实际的二进制文件,而单个init.rc则不是这样的。并且当多个服务被添加到系统中时,这种方式更有助于解决合并冲突,因为每个服务都是在一个单独的文件中的。
APEX的版本化RC文件
从Android Q的主线代码开始,每个主线模块都有自己的init.rc文件,init进程按照/apex/*/etc/*rc的命名模式来处理这些文件。
因为APEX模块必须可以在多个Android版本上运行,因此他们在服务的定义上必须是不同的参数。从Android T开始,通过将SDK版本信息合并到init文件的名称中来实现这点。文件的后缀名从.rc变成.#rc,其中#是该RC文件支持的最低SDK版本。如一个指定SDK=31的rc文件其命名可能是init.31rc。通过这种方式,APEX可以包含多个init文件,如下示例。
对于一个APEX模块,在/apex/sample-module/apex/etc/目录下会有如下文件:
init.rcinit.32rcinit.35rc
选择的规则就是选取不超过当前SDK版本的最大的.#rc文件。为使用数字修饰的.rc会被认为是SDK=0。
当这个APEX模块被安装在SDK<=31的设备上时,系统会处理init.rc文件;当被安装在SDK 32,33,34的设备上时,系统会处理init.32rc;当SDK>=35时,会选择init.35rc。
这种版本规则进用于APEX模块的init文件,它不适用于存储在/system/etc/init、/vendor/etc/init或者其他目录下的init文件。该规则从Android S开始使用。
Actions
Actions是指一系列被命名的命令操作。Actions会有一个触发器用来决定何时执行该命令,当发生与该Action的触发器匹配的事件时,该Action就会被加入到待执行队列的尾部(除非它已经在队列中了)。
队列中的Action逐个出列,每个Action的命令也是按顺序执行。init进程会在这些活动中的命令执行之间处理其他活动(如设备的创建\销毁、属性设置、进程重启等)。
Actions使用以下形式声明:
1 | |
Actions会被添加到队列中,并根据包含他们的文件被解析的顺序执行(参阅import部分),然后在单独的文件中按顺序执行。
例如某个文件的内容如下:
1 | |
然后当启动的事件boot被触发时,并且属性true的值为true,则执行命令的顺序为:
1 | |
Services
Services是一种在初始化启动并在退出时重启(可选的)的程序,它用以下形式进行声明“
1 | |
Options
Options是Services的修饰符,他们影响着服务如何运行以及何时运行。
1 | |
当执行该服务时可以设置capability。capability应该是一个不带CAP_前缀的Linux capability,如NET_ADMIN或SETPCAP。可以通过链接http://man7.org/linux/man-pages/man7/capabilities.7.html查看Linux的capability列表。如果没有提供capability,那么该服务会被删除掉所有的capability,即便该服务以root身份运行。
1 | |
为Services设置类名,在同一个类名下的所有的服务可以一起启动或者停止,如果没有指定类名,会指定为默认的类名default。第一个类名(必须的)之外的其他类名用来将服务分组,如animation类应该包含启动动画和关机动画所需的所有服务。由于这些服务可以在引导过程中很早的就启动,并且可以运行到关机的最后的阶段,因此无法保障他们对/data分区的访问。这些服务可以检查/data分区下的文件,但是不应该一直打开着这些文件,此外他们还应该在/data不可用时也能运行。
1 | |
服务可能需要一些控制台命令,第二个参数(可选的)可以选择一个指定的控制台而不是使用默认控制台。默认的控制台/dev/console可以通过设置androidboot.console内核参数来修改。在所有的情况下前缀/dev/都可以被省略,因此/dev/tty0可以被执行为console tty0,这个操作可以标准输入输出stdin、stdout、stderr连接到这个控制台中。它与stdio_to_kmsg命令互斥,其中后者只能重定向stdout和stderr。
1 | |
这是一个设备关键型的服务。如果他在致命崩溃的mins分钟内或者在完成启动前退出超过4次,该设备就会重启到fatal reboot目标。其中默认的致命崩溃mins值为4,默认的fatal reboot目标是bootloader。针对测试来说,可以通过设置属性init.svc_debug.no_fatal.<service-name>的值为true来跳过fatal reboot。
1 | |
该服务不会跟随它的类名服务一起自动启动,它必须明确的指定名称或者接口名称来启动。
1 | |
可以进入位于path路径下的type命名空间。仅网络服务的命名空间可以被设置type为net。注意只能进入给定的一个type命名空间。
1 | |
打开一个文件,并将其fd传递给启动的进程,其中type可以取值为r, w ,rw。对于native的可执行文件,请参阅libcutils的android_get_control_file()。
1 | |
在执行该服务前设置分组名称,第一个参数(必须的)后的其他参数被用来设置为子分组(通过setgroups())。默认的分组为root(或许默认可以设置为nobody)。
1 | |
将该服务于它的AIDL或者HIDL服务相关联。其中接口名称必须是完整的限定名称而不是一个值名称,例如,这被使用来允许servicemanager或hwservicemanager来延迟启动服务。当服务有多个接口时,该标签应该多次使用。使用HIDL接口的一个实例就是interface verdor.foo.bar@1.0::Ibaz default,对于AIDL,则使用interface aidl <instance name>。其中AIDL中的实例名称是在servicemanager中注册的服务名字,可以通过adb shell dumpsys -l来列出这些实例名称。
1 | |
通过系统调用SYS_ioprio_set来设置IO优先级和IO优先级类, 类必须是rt,be,idle中的一个。优先级必须是一个0-7的整数。
1 | |
设置键盘码用来触发该服务,如果同时按下设置的所有的键盘码,则服务会被启动。典型用法就是用来启动bugreport服务。
这些参数也可以设置为一个属性值而不是一系列的键盘码,这种情况下,只能设置一个选项:属性名需要是典型的属性展开格式。这个属性必须包含用逗号分隔的键盘码列表,或者使用文本none表示此服务不响应键盘码。
例如:keycodes ${some.property.name:-none},其中some.property.name表示的是“123,124,125”。由于键盘码在init进程中很早就被处理了,因此只有PRODUCT_DEFAULT_PROPERTY_OVERRIDES属性才能使用。
1 | |
将child的memory.limit_in_bytes设置为最小为limit_in_bytesbytes大小,以及物理内存大小的百分比limit_percent(仅当挂载memcg的时候)。值必须大于等于0。
1 | |
将child的memory.limit_in_bytes设置为指定的属性的值(仅当挂载memcg的时候)。这个属性会覆盖memcg.limit_in_bytes和memcg.limit_percent。
1 | |
将child的memory.soft_limit_in_bytes设置为指定的值(仅当挂载memcg的时候)。值必须大于等于0。
1 | |
将child的memory.swappiness设置为指定的值(仅当挂载memcg的时候)。值必须大于等于0。
1 | |
当fork这个服务的时候设置一个新的pid或者挂载的命名空间。
1 | |
当服务退出时,不再重启该服务。
1 | |
当服务重启时,执行命令(见下文)。
1 | |
设置child的/proc/self/oom_score_adj为指定的值,取值范围为-1000 到 1000。
1 | |
这个服务的定义会覆盖掉别的具有相同名字的服务的定义,这通常意味着在/odm上定义的服务会覆盖掉/vendor上定义的服务,init进程会用该服务的最后一个服务的定义来处理这个服务。需要密切注意init.rc文件的解析顺序,因为他有一些特殊的向后兼容性内容。import章节会更加具体的解释这些。
1 | |
为这个服务进程设置优先级,取值范围为-20到19,默认值为0,优先级通过setpriority()设置。
1 | |
如果该进程无法被启动,或者服务进程终止时退出码不是CLD_EXITED,状态值不是0,重启系统到指定的target目标,其中target参数和sys.powerctl具有相同的格式。这通常适合和exec_start内置程序一起使用,用于在启动期间做一些必备检查。
1 | |
如果一个服务是非onshot的,那么当它退出时会在轮到它的再次启动的时候往后推迟这个值之后再重新启动,默认值是5秒以避免频繁崩溃。对于需要定期执行的服务可以设置这个值,如:设置为3600表示该服务每小时执行一次,设置为86400表示该服务每天执行一次。
1 | |
将进程资源的限制应用到当前的Service上。rlimit是可以被子进程继承的,因此可以有效的将rlimit作用于当前Service的进程树上,它和setrlimit命令的作用是类似的。
1 | |
在执行该服务前设置seclabel。主要用于从rootfs运行的服务,如ueventd,adbd等。system分区上的服务可以使用基于问他们的文件安全上下文的定义的策略转换。如果没有指定并且在策略中也没有定义转换,则默认使用init进程的上下文。
1 | |
在启动的进程中设置环境变量属性。
1 | |
为该服务进程设置关闭行为。如果为指定该参数,则在关机时使用SIGTERM和SIGKILL来终止该服务。如果该参数设置为critical,则关机时不会终止该服务,直到超时。当关机超时的时候,即使服务被设置为critical也一样会被杀死。当设置为sritical的服务在关机时如果没有在运行,则此时也会被启动。
1 | |
在执行该服务前发送SIGSTOP信号,这通常用于调试,在下面的关于调试的章节中会说明该如何使用。
1 | |
创建一个名为/dev/socket/name的套接字,并将其fd传递给启动的进程,该套接字会在启动服务的时候同步启动。参数type必须为dgram、stream或seqpacket,其也可以以+passcred结尾来启动套接字的SO_PASSCRD,或者以+listen结尾使其成为一个监听的套接字。user和group默认是0,seclabel是该套接字的SELinux策略上下文,可以通过seclabel明确指定或者根据服务的可执行文件的安全上下文来计算得出。对于native可执行文件可以参考libcutils中的android_get_control_socket()。
1 | |
将stdout和stderr重定向到/dev/kmsg_debug中。这对于在很早启动的、并且不使用Android原生日志的、并且我们还想要获取这些日志记录的服务而言是非常有用的。它只有在/dev/kmsg_debug启用时才会启用,而且仅在userdebug和eng版本中启动。这与console命令是互斥的,后者还能额外的将stdin也重定向过去。
1 | |
当进程fork的时候设置它的任务配置。这是为了取代writepid操作来将进程移动到cgroup中。
1 | |
提供服务的一个超时时间,超过该时间后服务会被杀死。这里尊重oneshot类型的服务,因为oneshot的服务不会被自动重启,而其他的服务会。该属性与前面的restart_period结合起来使用更合适。
1 | |
标记该服务可以在后续的启动序列中被APEX服务给覆盖(通过override方法)。如果被标记了updatable的服务在所有的APEX模块激活前就被启动,则该次执行会被推迟,直到所有的APEX模块激活后再执行。如果未被标记为updatable,则不能被APEX覆盖。
1 | |
在执行该服务前设置用户,当前默认的用户是root(或许可以设置为nobody)。在Android M中,进程应该使用这个选项,即使他们需要Linux capabilities。以前想要获取Linux capabilities,进程需要以root身份运行,再去请求这些capabilities,然后降低为所需的uid。现在有一种新机制,通过fs_config来允许设备制造商将Linux capabilities添加到需要使用的特定的二进制文件中。这种机制在[This mechanism is described on http://source.android.com/devices/tech/config/filesystem.html](This mechanism is described on http://source.android.com/devices/tech/config/filesystem.html)中有详细的描述。使用这种新机制,进程可以使用`user`选项来选择他们想要的`uid`而不必以`root`的身份运行。从`Android Q开始,进程也可以直接在他们的.rc文件中直接请求capabilities,查看前面的capabilities`章节。
1 | |
在fork时将子进程的pid写入到文件中,适用于cgroup/cpuset使用。如果在/dev/cpuset/下没有指定文件,但是系统属性ro.cpuset.default被设置为一个非空的cpuset名称(如/foreground),此时pid就会写入到/dev/cpuset/cpuset_name/tasks文件中。使用该操作将进程移动到cgroup中已经过时了,请使用task_profile方式。
Triggers
Triggers是一个字符串,可以用来匹配特定类型的事件,然后触发Action的执行。
触发器又可以分为事件触发器和属性触发器。
事件触发器是由trigger命令或者init进程中的QueueEventTrigger()函数触发,他们采用简单的字串形式,如boot或late-init。
属性触发器是由指定的属性将值设置为给定的新值或者改为任意的新值后触发的,他们采用property:的形式。
一个Action可以有多个属性触发器,但是只能有一个事件触发器。
例如:on boot && property:a=b定义了一个Action,他会在boot事件发生时,并且属性a的值为b的时候才会执行。
on property:a=b && property:c=d定义的Action会在以下三种情况执行:
- 在初始启动时属性a的值为b, 属性c的值为d。
- 不论在什么时候,当a的值变为b后,并且此时c的值已经是d了。
- 不论在什么时候,当c的值变为d后,并且此时a的值已经是b了。
触发序列
init进程在早期启动期间使用以下的触发器序列,他们是在init.cpp中定义的内置触发器:
early-init序列中的第一位,在配置了cgroup后,在ueventd的冷启动完成前触发。init在冷启动完成后触发charger当属性值ro.bootmode值为charger的时候触发late-init当属性ro.bootmode的值非charger的时候触发,或者healthd从充电模式启动的时候触发。
其余的触发器在init.rc中配置,并非内置的。默认的序列是在init.rc中的on late-init下指定的,在init.rc内部的Action已经被忽略了。
early-fs启动voldfsvold已经起来了,挂载未标记为first-stage或者延迟挂载的分区post-fs配置一些依赖早期挂载的任何东西late-fs挂载标记为延迟挂载的分区post-fs-data挂载并配置/data分区,设置加密。如果在first-stage中未能挂载/metadata,则在这个阶段再次格式化。zygote-start启动zygoteearly-boot在zygote启动后boot在early-boot的Action全部完成之后
Commands
1 | |
开启或者关闭bootchart。这些都存在于默认的init.rc文件中,但是仅当/data/bootchart/enabled存在时才会启用bootchart,否则这些都是无效的。
1 | |
修改文件的权限。
1 | |
修改文件的属主和属组。
1 | |
启动某个类下的所有的未启动的服务,有关启动服务的更多消息,参阅start条目。
1 | |
停止并停用某个类下的所有的已启动的服务。
1 | |
停止某个类下的所有的已启动的服务,但是并不停用,后续可以通过class_start重新启动他们。
1 | |
重启某个类下的所有的服务,如果指定了--only-enabled,那么disable的服务都会被跳过。
1 | |
复制文件,类似于write,但是对于二进制文件和大文件来说很有用。对于src文件,不允许复制符号链接文件、全局可写文件以及组内可写文件;对于dst文件,如果文件不存在的话复制过来的文件的模式默认是0600。如果dst文件已存在,则不会去复制。
1 | |
逐行复制文件,和copy类似,对于sys节点很有用,但是不会去处理多行数据的sys节点。
1 | |
设置域名。
1 | |
启用一个disable的服务,就像是没有标记为disable一样。如果该服务应该正在运行,那么它现在就会直接启动。典型的使用就是bootloader在需要时会设置一个变量来启动某个服务,如下:
1 | |
1 | |
使用给定的参数fork并执行某个命令。该命令在--之后,因此可以提供安全上下文、用户和组信息。注意在该命令执行完毕前,不会去执行别的命令。seclabel可以是一个-来表示默认情况,在参数argument中可以使用属性值。init会停止执行命令,直到在fork的进程退出后才恢复。
1 | |
使用给定的参数fork并执行某个命令。这和前面的exec命令类似,区别就是init不会停止执行命令,不论fork的进程是否退出。
1 | |
启动一个Service并且停止其他命令,直到它返回。这个命令和exec类似,但是用的是一个具体的Service来代替exec的参数。
1 | |
设置一个全局变量(在这个命令之后启动的所有进程都会继承到这个变量)。
1 | |
设置主机名。
1 | |
使用某个在线网络接口。
1 | |
安装某个路径下的模块。-f:即使当前内核版本与编译模块的内核版本不一致也强制安装。
1 | |
找到某个接口名的服务,并在其上运行start、restart、stop命令(如果能找到服务)。name可以是一个完整限定的HIDL名称,这种情况下它的名字为<interface>/<instance>;如果是一个AIDL服务,则名字为aidl/<interface>。例如:android.hardware.secure_element@1.1::ISecureElement/eSE1或者aidl/aidl_lazy_test_1。
注意,这些命令只作用于服务接口指定的接口,而不是运行时注册的接口。
这些命令的使用示例:
intercace_start android.hardware.secure_element@1.1::ISecureElement/eSE1会启动一个HIDL服务,该服务提供android.hardware.secure_element@1.1::ISecureElement/eSE1和eSE1实例。
interface_start aidl/aidl_lazy_test_1会启动一个AIDL服务,该服务提供aidl_lazy_test_1接口。
1 | |
导入某个路径下的文件中定义的全局变量。文件的每行内容必须是export <name> <value>的格式。
1 | |
(该操作已启用,没有任何操作)
1 | |
当/data被解密时加载持久属性,他已经包含在了默认的init.rc中。
1 | |
设置log等级,从7(所有的log)到0(仅致命log)。这些数字对应内核日志的级别,但是不会影响到内核日志的级别。使用write命令项/proc/sys/kernal/printk中写入来修改内核级别。在参数中可以使用属性值。
1 | |
用于标记/data挂载后的点。
1 | |
创建一个目录,可以选定模式、属主和属组。如果没有提供这些参数,则默认创建的权限为755,属主和属组都是root用户组。如果提供了参数,并且目录已存在,则更新目录的权限等这些信息。
参数action可以是以下取值:
None:不采用加密操作,如果parent有加密,则采用父目录的加密。Require:加密目录,如果加密失败,则终止启动进程。Attemp:尝试设置一个加密策略,但是失败后会继续执行。DeleteIfNessary:如果需要设置加密策略,则递归删除掉目录。
参数key可以是以下取值:
ref:使用系统范围内的DE key。per_boot_ref:使用每次启动生成的key。
1 | |
在给定的fs_mgr-format的fstab上调用fs_mgr_mount_all,并带有参数early或late。如果参数为--early,init进程会跳过挂载带有latemount标志的分区并触发fs的加密状态事件。如果参数为--late,init进程只会挂载带有latemount的分区。默认没有设置参数的情况下,mount_all会处理所有给定的fstab。如果fstab参数也没有指定,在运行时按照顺序从/odm/etc、/vendor/etc、/中扫描fstab.${ro.boot.fstab_suffix}、fstab.${ro.hardware}、fstab.${ro.hardware.platform}。
1 | |
尝试将指定设备挂载到指定的目录。flags参数包括ro、rw、remount、noatime,options参数包括barrier=1、noauto_da_alloc、discard,多个参数可以使用逗号隔开,如:barrier=1,noauto_da_alloc。
1 | |
挂载APEX后执行任务。例如,为已挂载的APEX创建数据目录,解析配置文件,更新链接器配置。通过将apexd.status设置为ready可以在APEX通知挂载事件后,只执行一次。
1 | |
停止并重新启动正在运行的服务,如果该服务正处于重新启动中,则不做任何处理,否则会启动该服务。如果设置了--only-if-running,则只会重启正在运行的服务。
1 | |
被init.rc创建的文件则不需要,因为init进程会自动正确的进行标记。
1 | |
递归的将将给定的文件重新恢复到在file_context配置中指定的安全上下文中。
1 | |
在给定的路径上调用unlink(2)。你可能想使用exec -- rm ..来替代(前提是系统分区已经挂载了)。
1 | |
在给定目录上调用rmdir(2)
1 | |
在给定的文件或者目录中的文件上调用readahead(2), 通过参数--fully读取完整的文件内容。
1 | |
设置系统属性,参数value中可以使用系统属性。
1 | |
为资源设置rlimit,这适用于设置rlimit之后启动的所有进程,它旨在init的早期设置然后全局应用。参数resource最好使用它的文本表示形式(cpu、etio、RLIM_CPU、RLIM_RATIO等)。他也可以为指定的资源枚举对应不同的int值。参数cur和max设置设置为unlimited或-1来表示无限制的rlimit。
1 | |
启动一个服务,如果该服务没有在运行的话。注意这不是同步的,即使是同步的,也不能保证操作系统的调度器会充分执行该服务以保障有关服务状态的任何信息。请使用exec_start命令来获取同步版本的start。
这产生了一个重要的后果:如果这个服务为其他服务提供功能,如提供一个通信管道,那么在这些服务前简单的启动这个服务,是无法保证其他服务请求之前管道是否已经建立。
1 | |
停止服务如果当前服务正在运行的话。
1 | |
在给定的fstab文件上运行fs_mgr_swapon_all。如果fstab参数也没有指定,在运行时按照顺序从/odm/etc、/vendor/etc、/中扫描fstab.${ro.boot.fstab_suffix}、fstab.${ro.hardware}、fstab.${ro.hardware.platform}。
1 | |
创建一个符号链接。
1 | |
设置系统时钟基数。(0表示系统时钟以GMT计时)
1 | |
触发一个事件。用于将另一个action入队列。
1 | |
卸载挂载在该路径上的文件系统。
1 | |
在给定的fstab文件上运行fa_mgr_unmount_all。如果fstab参数也没有指定,在运行时按照顺序从/odm/etc、/vendor/etc、/中扫描fstab.${ro.boot.fstab_suffix}、fstab.${ro.hardware}、fstab.${ro.hardware.platform}。
1 | |
内部实现细节是用于更新dm-verity和设置被adb remount使用的partition.mount-point.verified属性值,因为fs_mgr不能自己直接设置他们。从Android 12开始,这是必须的,因为CtsNativeVerifiedBootTestCases将读取属性partition.${partition}.verified.hash_alg来检查sha1是否未被使用。
1 | |
轮询给定的文件是否存在,当找到的时候或者超时的时候返回。如果超时时间未指定,默认的是5秒。超时时间可以是小数,使用浮点数表示。
1 | |
等待系统属性的值变成期望值,其中参数value也可以使用系统属性表示。如果该属性值已经是期望值了,会直接返回。
1 | |
打开文件,并且通过write(2)写入内容。如果文件不存在则创建该文件,如果存在该文件会被截断。参数content中可以使用系统属性。
Imports
1 | |
解析init配置文件以拓展当前配置。如果path是一个目录,该目录下的每一个文件都会被解析为配置文件。它不是递归的,嵌套的目录不会被解析。
import关键字不是一个命令,而是它自己的一部分,这意味着他不是作为Action的一部分发生,而是它是作为一个正在解析的文件处理的,并且遵循下面的逻辑。
init进程只有三次导入.rc文件的机会:
- 在初始启动的时候,它会导入
/system/etc/init/hw/init.rc或者属性ro.boot.init_rc所指示的脚本。 - 在导入
/system/etc/init/hw/init.rc之后会立即导入/{system, system_ext, vendor, odm, product}/etc/init/. - (已弃用)当导入
/{system, vendor, odm}/etc/init/或者指定路径下的.rc执行mount_all的时候,在Android Q以后不再允许。
由于遗留问题,文件的导入顺序有点复杂。但是能保证一下内容:
/system/etc/init/hw/init.rc被解析,然后递归导致它的每个导入。/system/etc/init/下的内容按照字母顺序依次解析,在解析每个文件时递归导入。- 对于
/system_ext/etc/init/、/vendor/etc/init/、/odm/etc/init/、/product/etc/init/目录执行步骤2
下面的伪代码可能会描述的更清晰一些:
1 | |
Actions会按照解析的顺序执行。例如post-fs-data操作,在/system/etc/init/hw/init.rc中的post-fs-data总是第一个被执行按照他们在文件中出现的顺序。然后是/system/etc/hw/init.rc文件中导入的post-fs-data执行等。
Properties
init进程通过以下属性提供状态信息。
init.svc.<name>给定名称的服务的状态信息。(stopped, stopping, running, restarting)
dev.mnt.dev.<mount_point>,dev.mnt.blk.<mount_point>,dev.mnt.rootdisk.<mount_point>
与参数mount_point相关联的块设备。其中mount_point已经从/目录被替换成了.目录,因此如果引用根节点/,则会使用/root。dev.mnt.dev.<mount_point>表示着挂载到文件系统上的块设备(例如dmN或者sdaN/mmcblk0pN访问/sys/fs/ext4/${dev.mnt.dev.<mount_point>}/)。
dev.mnt.blk.<mount_point>表示块设备上的磁盘分区。(如sdaN/mmcblk0pN访问/sys/class/block/${dev.mnt.blk.<mount_point>}/)
dev.mnt.rootdisk.<mount_point>表示上述磁盘分区中的root磁盘。(如sda/mmcblk0访问/sys/class/block/${dev.mnt.rootdisk.<mount_point>}.queue)
init进程会响应以ctl开头的属性,这些属性采用ctl.[<target>_]<command>的格式,其值作为参数。参数target是可选的,它主要指定匹配的服务。target只有一个选项即interface,它表示属性值指向服务提供的接口而不是服务名称本身。
例如:
SetProperty("ctl.start", "logd")会通过start命令启动logd。
SetProperty("ctl.interface_start", "aidl/aidl_lazy_test_1")会通过start命令启动aidl_lazy_test_1接口的服务。
注意这些属性只能设置,是无法读取到的。
这些命令如下:start、restart、stop
这相当于在属性值指向的服务上执行start、restart、stop命令。
oneshot_on、oneshot_off命令会打开或关闭属性值所指向的服务的oneshot标记,这特别适用于延迟加载的Hal,当它是延迟加载的Hal时,oneshot必须是打开状态,否则是关闭状态。
sigstop_on、sigstop_off命令会打开或者关闭属性值所指向的服务的sigstop功能。有关此特性的更多详细信息,请参阅下面的Debugging章节。
Boot timing
init进程会在系统属性中记录一些启动时机相关的信息。
1 | |
启动后的时间,以ns为单位(通过CLOCK_BOOTTIME时钟),从init进程的第一阶段开始。
1 | |
init进程的第一阶段启动花费的时间,单位ns。
1 | |
SELinux过程花费的时间,单位ns。
1 | |
加载内核模块花费的时间,单位ns。
1 | |
init进程等待ueventd冷启动阶段结束的时间,单位ns。
1 | |
启动后服务首次启动的时间,单位ns(通过CLOCK_BOOTTIME时钟)。
Bootcharting
这个版本的init包含执行bootchart的代码:生成日志文件,这些文件后面可以通过http://www.bootchart.org/处理。
在模拟器上,使用-bootchart timeout操作来启动bootchart,并设置超时时间。
在设备上:
1 | |
当你收集完数据后,记得不要忘记删除这个文件。
日志文件会被写入到/data/bootchart/,并且提供了一个脚本来检索文件并创建一个bootchart.tgz,并可以被bootchart的命令行工具使用:
1 | |
需要注意一点,bootchart会显示init进程从时间0开始执行一样,因此你需要查看dmesg来确定内核实际启动init的时间。
Comparing two bootcharts
有一个名为compare-bootcahrts.py的方便的脚本可以用来比较所选进程的开始和结束时间。前面提到的grab-bootcharts.sh会在/tmp/android-bootcahrt下创建一个叫做bootchart.tgz的压缩文件,如果在同一个机器上的不同目录下留下了两个这样的压缩文件,这个脚本可以列出他们时间戳的差异。例如:
使用方式:system/core/init/compare-bootcharts.py base-bootchart-dir exp-bootchart-dir
1 | |
Systrace
SysTrace(http://developer.android.com/tools/help/systrace.html)可用于在userdebug或eng构建模式下获取启动时间的性能分析报告。
下面是跟踪wm和am事件的示例:
1 | |
这个命令会导致这台机器重启,设 bn备重启完成后,追踪报告会被从设备设备上获取并写入到trace.html中,通过CTRL+C获取到。
限制:记录追踪事件是在持久系统属性之后开始的,因此在此之前的时间不会被记录。一些服务如vold、surfaceflinger、servicemanager会受到该限制的影响,因为他们是在持久系统属性启动前启动的。zygote进程以及它fork出的其他进程是不会受到影响的。
Debugging Init
当一个服务从init启动时,他可能会执行execv()失败,这不是典型的错误,并且可能在新的服务启动时指向一个链接器错误。在Android中链接器会打印它的日志到logd和stderr中,因此他们可以在logcat中看到。如果在logcat可以被访问之前遇到了错误,可以使用stdio_to_kmsg服务命令将链接器输出到stderr中的日志重定向到kmsg,然后就可以通过串行端口读取这些日志了。
不建议在没有init的情况下启动init服务,因为init会设置大量的难以手动复制的环境(用户、组、安全标签、功能等)。
如果需要从一开始就调试服务,则可以添加sigstop命令选项。该命令会在调用exec之前就立即向服务发送SIGSTOP。这为开发者提供了一个窗口,可以在继续使用SIGCONT之前附加调试器,trace等。
这个标志也可以通过ctl.sigstop_on和ctl.sigstop_off属性来动态控制。
下面就是一个通过上述方法动态调试logd的示例:
1 | |
下面也是同样的例子,只是用了strace:
1 | |
初始化脚本的验证
init脚本会在构建的时候检查其正确性,具体检查如下:
- 是否是格式良好的
Action、Service以及Import。例如:在Action前没有加on,在import后没有多余的行。 - 所有的命令都映射上一个有效的关键字,并且参数也在正确的范围内。
- 所有的服务都是可用的,这比检查命令的方式更严格,因为服务的参数是可选的并且是完全解析的。例如
UID和GID必须解析。
init脚本的其他部分仅在运行时解析,因此在构建期间不会检查,包括以下部分:
- 命令参数的有效性,例如:不检查文件路径是否实际存在,
SELinux是否允许才做,UID和PID是否解析。 - 不检查服务是否存在并且是否是有效的
SELinux定义 - 不检查服务是否之前在其他脚本中已经定义过
Early Init Boot Sequence
最早初始化引导的顺序分为三个阶段:第一阶段、SELinux设置、第二阶段。
第一阶段的初始化负责加载系统其余部分的最低需求。具体来说,包括挂载/dev、/proc以及挂载early mount的分区(需要包含系统代码的所有分区,如system和vendor),并且将system.img挂载到设备的/目录。
注意在Android Q中,system.img总是包含了TARGET_ROOT_OUT并且在第一阶段的初始化中挂载到/上。Android Q还需要动态分区,因此需要ramdisk来启动Android系统。可以使用Recovery Ramdisk来启动系统而不需要一个专用的ramdisk。
第一阶段的初始化根据设备的配置有三个变更:
- 对于
system-as-root的设备,第一阶段的初始化是/system/bin/init的一部分,并且/init上的符号链接指向/system/bin/init以实现向后的兼容性。这些设备不需要做任何事来挂载system.img,因为根据定义,它早已被内核挂载为根文件。 - 对于具有
ramdisk的设备,第一阶段的初始化是一个位于/init的静态的可执行文件。这些设备挂载system.img到/system,然后切换到root权限将/system移动到/。挂载完成后会释放掉ramdisk的内容。 - 对于将
Recovery作为ramdisk的设备,第一阶段的初始化包含位于recovery ramdisk中的/init。这些设备首先切换root到/first_stage_ramdisk,然后删除环境变量中的recovery组件,然后再按照步骤2处理。请注意,是否正常启动还是启动到recovery模式,取决于内核命令行或者在Android S及以后的版本的bootconfig中是否存在androidboot.force_normal_boot=1。
当第一阶段的初始化结束后,他会使用selinux_setup参数执行/system/bin/init,在这个阶段SELinux就会可选的编译并加入到系统中。selinux.cpp包含更多关于该进程的信息。
最后当该阶段完成后,他会使用second_stage参数启动/system/bin/init,此时也是init的主要阶段运行,他会通过init.rc继续启动。
