在桌面linux中,第一个用户态进程为/sbininit,它会读取/etc/inittab文件,以获取所支持的”运行级“(run-levels)、运行时配置信息(单用户、多用户、网络文件系统等)、需随机启动的进程以及当下用户按下CTRL+Alt+Del组合键时该做出何种反应等信息。
在android中也是用这样的init程序,但android中的init和linux中的init仅仅是名字相同罢了,它们之前的区别如下表所示:
| 配置文件 | /etcinittab |
/init.rc以及它导入的任何文件[通常是init.hardware.rc和init.usb.rc] |
| 多种配置 | 支持:”运行级“的概念(0:系统停机状态; 1:单用户工作状态;2-3: 多用户状态;……)。每个”运行级“都会从/etc/rcrunlevel.d2那里加载脚本 |
没有”运行级“的概念,但是通过触发器(trigger)和系统属性提供了配置选项 |
| watchdog | 支持:用respawn关键字定义过的守护进程会在退出时重启—除非该进程反复崩溃,会在这种情况下,反复崩溃的进程会被挂起几分钟 | 支持:服务默认是应该保持活跃的。除非启动它时使用了oneshot参数。服务启动时还可以使用critical参数,这会使系统在该服务无法启动时强制重启 |
| 收容孤儿进程 | 支持/sbin/init会调用wait4()系统调用去获取孤儿进程的返回码,避免出现僵尸进程 |
支持:/init注册了一个SIGCHLD信号的处理模块,SIGCHLD信号是内核在子进程退出时自动发送的,大多数进程会默默的调用wait(NULL)清理掉已退出的子进程,而不去管他的返回码是什么 |
| 系统属性 | 不支持 | 支持:/init通过共享一块内存区域的方式,让系统中所有进程都能读取系统属性(通过getopt),并通过一个名为”property_service“的socket让所有权限的进程能够(通过’setopt’)写相关属性 |
| 分配socket | 不支持:linux的init不会向子进程提供socket,这个功能交个inetd的 | 支持:/init 会绑定一个UNIX domain socket(从L版开始,是seqpacket socket)提供给子进程,子进程可以通过android_get_control_socker函数获取它 |
| 触发操作 | 不支持: linux只支持非常特殊的触发操作,如CTRL+Alt+Del和UPS电源事件,但是不允许任意的触发操作 |
支持:/init可以在任何一个系统属性被修改时,执行trigger语句块中的,由任何一个用户预先写好的指令 |
| 处理uevent事件 | 不支持:linux依靠的时hotplug守护进程(通常为udevd) | /init也会化身为ueventd,用专门的配置文件来指导其行为 |
/init是静态链接的可执行文件,在编译时它所依赖的库都已经被合并到这个二进制文件里去了、这样做的目的是为了防止仅仅因为缺少某个库而造成的系统无法正常启动的情况发生。在/init刚被执行时,只有和内核一起打包放在boot分区的RAM disk被mount上来,换句话说,系统中只有/和/sbin。
系统属性
android的系统属性提供了一个可以全局访问的配置设置仓库[1],它在形式上和MIB数组作为参数调用的sysctl(2)有些类似,只不过是在用户态中,由init实现了。在init的相关源码property_service.c中的代码,会按照下表给出的顺序,从多个文件中加载属性。
| 文件 | 内容 |
|---|---|
/default.prop(PROP_PATH_RAMDISK_DEFAULT) |
初始设置。注意。这个文件是initramfs中的一部分,直接到设备的闪存分区上是找不到的 |
/system/build.prop(PROP_PATH_SYSTEM_BUILD) |
编译android过程中产生的设置 |
/system/default.prop(PROP_PATH_SYSTEM_DEFAULT) |
通常是厂商添加的设置 |
/data/local.prop(PROP_PATH_LOCAL_OVERRIDE) |
如果编译init时使用了ALLOW_LOCAL_PROP_OVERRIDE选项,并且ro.debuggable属性被设置为1,那么就会加载这个文件。这使得开发者可以通过/data分区里push一个文件的方式,修改之前的配置 |
/data/proper/persist.*(PERSISTENT_PROPERTY_DIR) |
重启后不会丢失的属性(Persistent property),这些属性文件会被加上前缀persist。它们会被分别存放在/data/proper目录中的各个文件里,躲过重启。只要/init.rc脚本中有这条指令,init就会重新加载这些属性 |
除了上面中列出的属性文件外,还有另外一个属性文件/factory/factory.prop(PROP_PATH_FACTORY),这个属性文件现在已经不再被支持。
因为init时系统所有进程的祖先,所以只有它才天生适合实现系统属性的初始化。在它刚开始初始化的时候,init中的代码会调用property_init去安装系统属性。这个函数最终会调用map_prop_area()函数,并打开PROP_FILENAME(这个宏定义指的是/dev/__properties__),然后在关闭这个文件的描述符之前,用mmap(2)系统调用以”读/写“权限把这个文件的内容map到内存里。之后,init又会再次打开这个文件,不过这词用的是O_READONLY,然后再unlink掉它。
属性文件的只读文件描述符被设为可以被子进程继承。这使得系统中任何一个进程都可以方便的通过mmap(2)映射这个文件描述符的方式访问到系统的属性—尽管只能读。这是一个很巧妙的方法,它让所有可以访问属性的用户共享了这块被称为__system_property_area的物理内存[2](它的大小是用宏定义PA_SIZE来表示,这个宏定义的默认值为128KB)。除了init进程,别的进程都无法对__system_property_area进行写操作。下面为华为荣耀平板(JDN-W09)的输出结果。
1 | init 的内存区域 |
__system_property_area这块内存区域的开头部分是一个很短的头部,其中记录了一个序列号(内部版本号)、一个签名文件(0x504f5250,也是PROP)以及一个版本号。紧接着这个头部的时112字节的被存储在一种混杂使用了单词查找树[Trie树或字典树]和二叉树的数据结构中。这个数据结构在Bionic的system_properties.cpp文件中有相当明晰的文档,如下面代码所示[3]:
1 |
|
property_service
为了能提供写(系统属性)请求的服务,init专门打开了一个专用的UNIX domain socket—/dev/socket/property_service。只要能连上这个socket,任何人都可以对它进行写操作(0666)。这些(通过/dev/socket/property_service这个socket)写入的命令会被直接送给init,init首先回去检查调用者是不是有设置相关属性的权限(这些权限信息在property_perms数组中)。权限的检查方式为判断uid和gid,判断调用者的uid和gid(/dev/socket/property_service这个socket的调用者证书中包含相关信息)是否和数组中的定义相符。如果系统包含有SELinux增强,属性的namespace还会改用安全上下文(security context,定义带/property_contetxts中)提供更好的保护。
特殊的namespace前缀
init能够识别出一些特定的前缀,这些前缀表示应该如何处理相关属性。
- persist伪前缀:这个前缀是为了让这些属性在系统重启后依然生效,这些重启后不会丢失的属性保存在
/data/property/目录里的各个文件中,这些文件的拥有者必须要是root:root,且不是链接。 - ro伪前缀:这个是用来标记只读属性的。不管是操作者还是拥有者,它都能设置且只能设置一次。通常,这些设置在场上提供build文件时就已经设置好了。
- ctl前缀:这个前缀为了方便控制init中提供的各种服务而设立的,通过设置相关服务的
ctl.start或者ctl.stop属性的值,就能很便捷的控制相关的服务。(toolbox中的start和stop工具实际上就是利用这个带ctl前缀的属性来控制zygote、surfacefliger和netd的)。在control_perms数组中记录一个不连续的ACL(Access Control List,访问控制列表),通过uid和gid来指定谁有启动和停止的权利。从Lollipop版本之后,SELinux接管了ACL的访问控制职能。
属性的访问方法
在toolbox里提供了两个属性访问的命令行工具getprop和setprop,另外还有属性监听工具watchprops。属性访问的原生api都在system/core/include/cutils/property.h文件中:
1 | // 获取指定属性key和value,如果key的值不存在的话,也可以用参数default_value给一个默认值 |
sys/_system_properties.h文件中还定义了一些没有文档支持的接口,其中__system_property_wait_any(unsigned int serial),它会让调用它的进程进入休眠状态,直到某个属性被修改时被被唤醒,在watchprops工具中就用了这个函数。在框架中,使用接口android.os.SystemProperties类来访问系统属性。
.rc文件
init的主要操作是加载它的配置文件,并依照配置文件执行相应的命令。传统上使用的配置文件有两个:主配置文件/init.rc和与设备相关的配置文件/init.hardware.rc,其中hardware相关的配置应该被替换为从内核参数androidboot.hardware,或从/proc/cpuinfo那里获得的字符串。比如,模拟器中默认使用的这个hardware字段就是goldfish。在实体设备中,系统中直接复制了/init.goldfish.rc文件的情况也不在少数,这可能是因为大部分的设备安装的android系统直接复制了模拟器的文件系统,而忽视了这个细节的缘故。这个设计的主要目的是让所有的android设备都能够共享一个init.rc文件,而与设备紧密相关的特定配置则让厂商写在/init.hardware.rc文件里。但在实际应用中,有时为了省事,也会把一些硬件相关的配置直接加在了/init.rc中。
trigger、action和service
.rc文件是由trigger语句块和service语句块构成的。trigger语句块中的命令,会在触发条件被满足时执行;而service语句块中定义的时各个守护进程,init会根据命令启动相关守护进程,也可以根据相关参数修改服务状态。service语句块由关键字service开头,后面跟着服务名及命令行。trigger语句块由关键字on定义,后面跟着一个参数(这个参数既可以是预先规定的各个启动阶段boot stage的名称,也可以是一个property关键字,后面跟冒号加”属性名称=属性值“这样的表达式),如果条件被满足,相应的属性值会改成指定的值。在执行指定action或command时,init会分别把属性init.action或init.command的值设置为当前正在执行的动作的名称。预先规定的启动阶段见下表(并不一定每个启动阶段都必须使用):
| 启动阶段 | 内容 |
|---|---|
| early-init | 初始化的第一个阶段,用于设置SELinux和ODM |
| init | 创建文件系统,mount点以及写内核变量 |
| early-fs | 文件系统准备被mount前需要完成的工作 |
| fs | 专门用于加载各个分区 |
| post-fs | 在各个文件系统(/data分区除外)mount完毕后需要执行的命令 |
| post-fs-data | 解密/data分区(可选),并mount之 |
| early-boot | 在属性服务(property service)初始化之后,启动剩余内容之前需完成的作业 |
| boot | 正常启动命令 |
| charger | 当手机处于充电模式时需要执行的命令 |
init.rc的语法和命令集
init.rc及其导入的文件都有非常好的注释。init_parser.c中的代码在解析rc文件时,能否识别出COMMAND(值在on关键字开头的语句块中有效)和OPTION(只在service关键字开头的语句块中有效)。下表列出了init所支持的各个COMMAND关键字(keywords.h)。
| 命令 | 语法 |
|---|---|
| bootchart_init | 启用启动时的信任链验证 |
| chdir dir | 等价于cd命令 |
| chmod actal_perms file | 修改制定文件的权限 |
| chown user group file | 等价于chown user:group file命令 |
| Chroot dir | 等价于chroot |
| class_reset service_calss | 停止与service_class相关的所有服务 |
| class_[start|stop] class | 启动或者定制class参数制定的service_class相关的所有服务 |
| copy src dst | 类似于cp命令 |
| domainname domainname | 把制定的域名写入/prco/sys/kernel/domainname伪文件中 |
| exec command | 不再支持 |
| enable service | 启动一个被disable的服务 |
| export variable value | 全局环境中,设置环境变量variable的值,该命令的执行结果会影响所用到的所有进程 |
| hostname hostname | 把主机名写入/proc/sys/kernel/hostname伪文件中 |
| ifup interface | 激活指定的网卡 |
| insmod ko | 加载一个内核模块 |
| import filename.rc | 导入另一个rc文件 |
| load_all_props | 加载build,default,factory文件中规定的属性 |
| load_persist_props | 加载/data/propert目录中的各个文件中的属性 |
| loglevel level | 设置内核的日志级别 |
| mkdir dir | 创建一个目录 |
| mount fstype fs point | 把指定分区类型(fstype)的分区(fs)mount到指定的mount点(point)上去 |
| mount_all | mount所有在vold守护进程使用的/fstab.hardware中规定的文件系统,这会使init fork()出一个子进程,并在这个子进程中用fs_mgr执行mount操作。init同时也能检测出所有加密的文件系统 |
| powerctl shoutdown/reboot | 对shutdown和reboot的封装 |
| [re]start service_name | 启动/重启服务名与参数service_name相同的service语句块中规定的服务 |
| restorecon[_recursive] path | 用path参数指定的文件重新加载SELinux上下文 |
| rm[dir] filename | 删除文件或目录 |
| setcon SEcontext | 设置或改变SELinux的上下文,init使用的上下文是u:r:init:s0 |
| setenforce [0|1] | 强制启动或者关闭SELinux |
| setkey table index value | 设置键盘映射表的内容 |
| setprop key value | 设置指定的系统属性 |
| setsebool name value | 设置某个与SELinux相关的bool型属性 |
| setlimit category min max | 使用setlimit(2)系统调用,设置进程可用资源的上下限 |
| stop service_name | 停止服务名与参数service_name相同的service语句块中规定的服务 |
| swapon all | 激活所有fstab中的swap分区 |
| symlink target src | 创建一个符号链接 |
| sysclktz tzoffset | 设置系统时区 |
| verity_load_state | 加载DM-verity(分区加密)状态 |
| verity_update_state mount | 更新DM-verity状态,并设置系统属性partition.mount.verified |
| wait file timeout | 等待文件file创建完,等待最大时间不超过timeout |
| write file value | 把value写入到文件file中去 |
COMMAND关键字指令会被用在各个启动阶段中,去执行在系统启动时所需要的操作,如设置目录结构、调整权限、通过/proc或/sys设置相关的内核参数。启动阶段所要执行的指令处理完成后,就需要处理服务相关的指令,按照规定,service语句块中使用的命令必须是用OPTION关键字指令,这些指令确定了该如何运行服务,以及相关服务退出/崩溃时需要进行的操作。下表列出了各个可用的OPTION关键字。
| 关键字 | 说明 |
|---|---|
| capability | 支持Linux的capability(7) |
| class | 把服务加入某个服务组,可以用class_[start|stop|reset]命令同时操作组内的服务 |
| console | 把该服务定义为一个console服务,stdin/stdout/stderr会被link到/dev/console上 |
| critical | 把该服务定义为一个关键服务,关键服务崩溃后会自动重启。如果在CRITICAL_CRASH_WINDOW(240)秒之内,它的崩溃次数超过了CRITICAL_CRASH_THRESHOLD(4)次,系统将会进入recovery模式 |
| disable | 表示该服务不需要启动,但可以手动启动 |
| group | 规定该服务以指定的gid启动。init会调用setgid(2)来完成这一操作 |
| ioprio | 指定该服务的I/O优先级,init会调用ioprio_set来完成 |
| keycodes | 指定触发该服务的组合键 |
| oneshot | 让init启动该服务后就不在管它了(忽略SIGCHLD信号) |
| onrestart | 列出该服务重启时要执行的命令,通常用来重启其它依赖服务 |
| seclabel | 指定应用在该服务上的SELinux标签 |
| setenv | 该服务被fork()出来并被exec()之前,先给它设置一个环境变量。这个变量只对该服务有效 |
| socket | 告诉init打开一个UNIX Domain socket,并让该服务集成这个socket,这样做的目的是解决服务的stdin/stdout的设置问题 |
| user | 指定该服务以指定的uid运行,init将调用setuid(2)完成这个任务 |
| writepid | 把子进程的PID写到指定的文件中去,用于设置cgroup资源控制 |
启动服务
android中的init还是用pid=1进程的传统方式启动服务的,即它fork()出服务子进程,然后调用setuid(2)/setpid(2)设置服务子进程的权限,并设置该服务子进程用来捕获输入的socket及配置环境变量、I/O优先级(在服务块语句中使用ioprio关键字)和SELinux上下文。对于使用console关键字定义的服务,init会把/dev/console连到它的stdin/stdout/stderr上,而对其他服务,它会把stdio给干掉。只有当这些操作都执行完毕后,init才会去执行服务本身的二进制可执行文件。
在服务启动之后,init会维持一个指向该服务的父链接(parental link),一旦服务停止运行或者崩溃,init就会收到一个SIGCHLD信号,并注意这一事件,然后重启该服务。onrestart关键字会使init在各个指定的服务之间建立起关联,当特定的服务需要重启时,init会去运行这个使用了onrestart关键字的service语句块中的命令,或者重启与该服务有依赖关系的服务。对于每一个服务,init还会维持一个反映了该服务当前运行状态的属性init.svc.service。
组合键
init能够在用户按下某个组合键(keychords)是启动某些服务。每个键都有指定的ID(来自Linux的evdev输入机制)。写在keycodes关键字后面的,是android键盘布局文件(/system/usr/keylayout)中规定的键位代码,而不是android框架规定和使用的键位代码(frameworks/native/include/android/keycodes.h)。
要支持组合键,/dev/keychord文件是必须存在的。这个文件是由一个keychord内核驱动导出的设备节点。
mount文件系统
尽管有vold,但init仍然需要执行一些mount操作。在init刚刚启动时,只有root文件系统被mount上来,此时/system和/data都没有被mount,且vold等守护进程都在/system中,所以需要init进程至少把/systemmount上来。
init能够认出/init.rc中的mount_all指令,并执行mount所有默认文件系统(/fstab.hardware文件中)的操作。执行mount操作的代码位于fs_mgr中,无论/init还是vold都会使用它。当/init执行mount操作时,它首先会fork()一个子进程执行相关操作,这是为了避免在执行mount时出现问题而影响自己的启动活动。
被fork()出来的子进程会去执行mount操作,如果有需要,还会对文件系统进行fsck操作。fs_mgr中规定了对各种不同的文件系统执行fsck操作的程序的所在路径,而且这个fsck操作也是由再次fork()出来的子进程来完成。fs_mgr中会提升日志级别,所以,使用dmesg能够看到fs_mgr输出的各种消息。如果时机足够早的话,是可以看到fs_mgr特有标记的消息。
fork()出来的mount操作子进程会向父进程返回一个返回码。/init会根据这个返回码设置属性vold.decrypt的值,这个属性会被vold读取,并对相应的文件系统进行解密操作。如果没有任何文件系统被加密,init就会执行名为”nonencrypted”的trigger语句块中的指令。
init总结
init进程完全遵循建立服务的经典模式:初始化,然后陷入一个循环中,而且永远不想从中退出来。
初始化流程
init进程初始化工作由以下步骤组成:
检查自身是否被当成ueventd或watchdogd调用的,如果是,则余下的执行流程会转到相应的守护进程的主循环那里去。
创建
/dev、/proc、/sys等目录,并mount它们。添加文件
/dev/.booting,在启动完毕后,这个文件会被(check_startup)清空。调用
open_devnull_stdio()函数完成“守护进程化”操作(把stdin/stdout/stderr链接到/dev/null中)调用
klog_init()函数创建/dev/__kmsg__(Major 1, Minor 11),然后立即删除它。调用
property_init()函数,在内存创建__system_property_area区域。调用
get_hard_name()函数,读取/proc/cpuinfo伪文件中的内容,并提取出“Hardware”一行的内容作为硬件名。以这种方式获取硬件名。调用
process_kernel_cmdline()函数,读取/proc/cmdline伪文件中的内容,并把所有androidboot.XXX的属性都复制一份出来,变成rro.boot.XXX。初始化SELinux。SELinux是放在
/dev和/sys里面的。检查设备是否处在“充电模式”。如果设备处于“充电模式”,会使init跳过大部分初始化阶段,并且只加载各个服务中的“charger”类(当前只有“charger”守护进程有这个类)。如果设备没有处于“充电模式”,那么init将会去加载
/default.prop,正常执行启动过程。调用
init_parse_config_file()函数去解析/init.rc脚本文件。init会把
init.rc文件中各个on语句块里规定的action(action_for_each_trigger()函数)以及内置的action(queue_builtin_action()函数)添加一个名为action_queue的队列里去。最后,主循环中将逐个执行
init.rc中的所有命令init将在它的生命周期中的大多数时间里处于休眠状态,偶尔轮询各个文件描述符,并根据宏
BOOTCHART有没有被定义,来判断是否需要将日志发送到bootchart,只有在必要的时候init进程才会被唤醒。我们可以通过查看/proc伪文件系统,来了解init进程打开了哪些文件描述符。
主循环
init的主循环相当简洁,一共由三个步骤组成:
execute_one_command():从队列action_queue的头部取出一个action并执行。restart_processes():逐个检查所有已经注册过的服务,并在必要时重启。安装并轮询如下三个socket描述符。
property_set_id(/dev/socket/property_service),这个socket是用来让想要设置某个属性的客户端进程,通过它把需要设置的属性的key和value发送给init。init会根据对端进程的证书,来判断这个进程是否有权限进行属性设置。keychord_fd(/dev/keychord),它是用来处理上文讨论过的启动服务的组合键。signal_recv_fs它是socketpair中的一端,创建它是为了处理因子进程死亡而发来的SIGCHLD信号。当init收到这个信号后,sigchld_hander就会向socketpair(signal_fd)写入数据,这样在这个sockerpair的接受端就能收到这些数据,并导致init去调用wait_for_one_process(0)。这样就能获得已死亡的子进程的返回码,以便去释放该进程残留的资源,防止它成为僵尸进程,然后init会清理这个socketpair中的数据,等待下一个进程挂掉。如果挂掉的进程是critical服务的守护进程的话,init也会重启这个进程。
除了上述init监听的三个文件以外,/init是不会再其它任何地方接受输入的。唯一能够修改/init操作行为的方法就是编辑/init.rc,但这个文件位于一个完全隔离的分区,和内核放在一起,除非设备的Boot Loader被解锁,否则无法进行修改。