/dev/kmsg
);
参考相关资料,对 Android 6.0.1 系统中 init 进程启动关键流程进行分析。
/init 可执行文件的对应代码在 /system/core/init/init.cpp 中,入口为
main
函数。
1 |
// init.cpp |
init 首先处于内核空间,即
is_first_stage = true
,在挂载几个关键设备节点和初始化 SELinux 之后,使用
exec
重新执行 init 进入
second stage
,从而降低进程权限级别,从内核空间降低至 init 级别。
整个 init 流程所做的工作基本已经列举出来了,下面针对各个函数细节进行解析。
在 Android 系统提供的 log 系统未初始化前,使用内核的 log 系统进行日志输出。
1 |
// init.cpp |
1 |
// klog.h |
1 |
// klog.c |
TEMP_FAILURE_RETRY
是系统代码中常用的宏,用于在调用函数失败时进行不断重新调用,然后获得返回值:
1 |
#ifndef TEMP_FAILURE_RETRY |
single_handler_init
用于处理子进程退出后的工作,通过捕捉
SIGCHLD
信号,根据 .rc 文件中的配置对子进程进行清理或者重启操作。
1 |
// init.cpp |
1 |
// signal_handler.cpp |
当子进程退出时,内核将发出
SIGCHLD
信号,对于子进程的处理函数
singal_handler_init
流程概括如下:
SIGCHLD
信号回调函数(
SIGCHLD_handler
)绑定到 socket 写端,当有信号到来时,向写端写入“1”;
reap_any_outstanding_children
处理目前子进程的退出情况;
epoll_ctl
IO 监听设置函数将 socket 读端的可读时机绑定到回调函数,当读端可被读取时(说明 socket 写端写入了数据),表明有子进程退出,此时调用回调函数(
handle_signal
);
handle_signal
处理子进程退出,使用
wait_for_one_process
等待子进程退出后,通过进程 pid 查询其所承载的服务,根据服务对应的标记,针对性的对进程进行清理或者重启处理。
1 |
// init.cpp |
1 |
// property_service.cpp |
概括属性服务初始化流程如下:
property_init
为属性的设置和获取创建共享内存区域;
property_load_boot_defaults
加载默认属性文件
/default.prop
;
start_property_service
启动属性设置服务,创建名为
property_service
的 socket 服务端,当接收到属性设置请求时,检查权限设置属性,阻止设置
ro.*
的属性,当设置
ctl.*
属性时作出相应处理,通知属性变更,触发 init.rc 中依赖的 property 触发器。
setprop
命令(实现代码在 external/toybox/toys/android/setprop.c),将会通过 socket 发送设置属性的请求。
除了上述属性初始化过程中加载的
/default.prop
属性文件,init 通过解析 init.rc 文件执行其中的
load_all_props
步骤(高系统版本为
load_system_props
)还会从以下位置依次加载属性文件:
ALLOW_LOCAL_PROP_OVERRIDE
选项,且
ro.debuggable
属性被设置为 1 时,就会加载这个文件。那么开发者可以通过在 /data 分区 push 一个文件的方式,修改之前的设置
/data/proper/persist.
重启后不会丢失的属性,这些属性的前缀为
persist
/factory/factory.prop
较新版本的 Android 不再支持了
解析并根据 init.rc 文件内容启动相应的守护进程是 init 进程的核心工作,servicemanager 和 zygote 等服务均是 init 进程通过解析 init.rc 文件的创建的。
1 |
// init.cpp |
init_parse_config_file
首先解析 init.rc 文件,.rc 文件是用 .rc 文件的特定语法编写的,从 .rc 文件中可以解析出多个阶段的执行流程,例如
early-init
,
init
等,每个阶段有不同的意义。
init.rc 文件中还导入了许多子 .rc 文件,均是按照 .rc 文件语法编写。
.rc 文件的语法由 trigger 语句块和 service 语句块构成:
trigger 语句块中的命令,会在满足触发条件时被触发执行;service 语句描述需要启动的守护进程。
trigger 语句块的格式是,使用
on
开头,后面跟一个参数,这个参数可以是各个启动阶段的名称(例如
early-init
)或者一个
property
关键字,
property
后面是冒号 +“属性名=属性值”的格式,这种情况下,触发条件为相应属性的属性值变更为指定的值;
service 语句块的后面跟着服务名称和命令行。
语句块中执行指定动作(action)或命令(command),执行时,init 会分别把属性 init.action 或 init.command 的值设为当前正在执行的动作的名称或当前正在执行命令的名称。
service 语句块下面可以赋予多种选项(option),用来指示服务进程的运行规则,以及进程死亡后的重启规则。
下面是 .rc 文件中的典型阶段(按启动顺序排列),根据设备不同,不同厂商可能对其进行定制:
early-init 初始化的第一个阶段,用于设置 SELinux 和 OOM 创建文件系统,mount 点以及写内核变量 late-init 初始化晚期,挂载文件系统,启动核心系统服务 early-fs 文件系统半准备被 mount 前需要完成的工作 专门用于加载各个分区 post-fs 在各个文件系统(/data 分区除外)mount 完毕之后需要执行的命令 post-fs-data 解密 /data 分区(如果需要),并 mount 之 early-boot 在属性服务(property service)初始化之后,启动剩余内容之前的作业 正常启动命令 charger 当手机处于充电模式时,需要执行的命令列举 .rc 文件中支持的大部分命令,一部分和 shell 命令具有相同作用:
bootchart_init 启用启动时的信任链验证 chdir directory 等价于
cd
命令(调用
chdir
)
chmod octal_perms file
修改文件的指定权限(以 8 进制表示)
chown user group file
等价于
chown user:group file
命令
croot directory
等价于 Linux 的 chroot 命令(调用 chroot(2))
class_reset service_class
停止与
service_class
相关的所有服务
class_[start|stop] class
启动或者停止
class
参数指定的
service_class
的所有服务
copy src_file dst_file
类似
cp(1)
命令
exec command
enable service
启动一个已被 disable 的服务
export varible value
在全局环境中,设置环境变量
varible
的值,影响所有进程
insmod module.ko
加载一个内核模块
load_all_props
加载所有位置属性
load_persist_props
加载 /data/propert 目录中的各个文件中的属性
loglevel level
设置内核的日志级别
mkdir directory
创建一个目录(调用
mkdir(2)
)
[re]start service_name
启动/重启服务名与参数 service_name 一致的语句块中的服务
rm[dir] filename
删除一个文件或一个目录(调用
unlink(2)/dmdir(2)
)
restorecon[_recursive] path
用
path
参数指定文件重新加载 SELinux 上下文
setcon SEcontext
设置 SELinux 的上下文,init 上下文为
u:r:init:s0
setenforce[0|1]
强制启用或关闭 SELinux
setprop key value
设置指定的系统属性
stop service_name
停止服务名与参数 service_name 一致的语句块中的服务
symlink target src
创建一个符号连接 ln-s,即调用
symlink(2)
trgger trigger_name
激活一个 trigger 语句块(会使 init 重新运行该语句块)
wait file timeout
等待文件 file 创建完毕,等待超时为 timeout 秒
write file value
把 value 写到文件 file 中去,等价于
echo value > file
capability(7)
class
把服务加入服务组(service group),`可用 class[start
console
把服务定义为一个 console 服务,stdin/stdout/stderr 会被 link 到 /dev/console 上
critial
把服务定义为一个关键服务,一旦崩溃,会自动重启,超过一定次数,系统将重启至 recovery 模式
disable
表示服务不需要启动但,之后还可以手动重启
group
指定服务以指定的 gid 启动,init 会调用
setgid(2)
来完成这个操作
ioprio
指定服务的 I/O 优先级,init 会调用
ioprio_set
来完成这个任务
keycodes
指定触发服务的组合键(key chord)
oneshot
告诉 init 启动该服务,然后就不管它了(忽略掉
SIGCHLD
信号)
onrestart
枚举该服务重启时要执行的命令,通常用来重启其他依赖服务(dependent service)
seclabel
指定应用在该服务上 SELinux 标签(label)
setenv
在服务被
fork()
出来并
exec()
之前,设置环境变量,只有该服务可看到
socket
打开一个 socket,让该服务继承这个 socket
指定该服务以 uid 身份运行,init 将调用
setuid(2)
完成这个任务
writepid
把子进程的 pid 写入指定文件中,用于设置 cgroups 资源控制
查看一下 servicemanager、surfacefliger 和 zygote 服务在 init.rc 文件中的启动配置
1 |
# init.rc |
zygote 服务的启动在单独的 .rc 文件中,这个 rc 文件在 init.rc 的开头被导入:
1 |
# init.rc |
${ro.zygote}
表示 32 位和 64 位 zygote,这里看一下 32 位 zygote 启动描述:
1 |
# init.zygote32.rc |
所有的 service 里面只有 servicemanager、zygote、surfaceflinger 这 3 个服务有 onrestart 关键字来触发其他 service 启动过程。
可以看到它们之间的依赖关系:
class
表示依赖的服务组,看到 servicemanager 和 surfaceflinger 属于
core
,而 zygote 属于
main
。
在 init.rc 文件中查找这些服务组的启动时机:
1 |
# init.rc |
看到 core 服务组,在 boot 阶段被启动,优先与 main 服务组,boot 由 late-init 阶段触发;main 服务组在
vold.decrypt
属性被改变的多处时机被触发,这些属性将在 vold 服务启动时的相关流程被触发。
下面是 vold 服务的启动内容:
1 |
# init.rc |
vold 是用于管理和控制 Android 外部存储介质的服务进程,它的实现代码在
system/vold/cryptfs.c
中。
以 zygote 服务为出发点,分析启动一个服务的具体代码。
前面 init.cpp 中,调用
init_parse_config_file("/init.rc")
解析 init.rc 文件,内部会辗转调用到
parse_service
函数,它用来解析 service 信息,会创建一个
service
的结构体,保存服务进程的信息,同时创建了一个 socket,保存在结构体成员
socketinfo *sockets
中,同时 onrestart 相关的信息存放在成员
action onrestart
中,
action
也是一个结构体,存放相关执行动作:
1 |
// init.h |
触发启动服务的代码由
do_class_start
函数负责,它的实现在
/system/core/init/builtins.cpp
中:
1 |
static void service_start_if_not_disabled(struct service *svc) |
service_start
函数中会首先使用
fork
创建子进程,然后在子进程中调用
execve(svc->args[0], (char**) svc->args, (char**) ENV)
执行 zygote 的可执行程序
/system/bin/app_process
,从而进入 zygote 的流程中。
zygote 的实现代码在
/frameworks/base/cmds/app_process/app_main.cpp
中。
至于其他服务的启动,以此类推。
到这里就分析完了 init 进程的整个流程,这对于了解之后的系统服务的启动流程奠定了基础。
例如用户进程使用
recvfrom
系统调用,kernel 开始准备数据,对于 Network IO,很多数据一开始没有到达,需要等待,此时用户进程将被阻塞,当 kernel 等到数据准备好的时候,将数据从内核空间拷贝到用户空间,返回结果,此时用户进程解除 block 状态。
IO 多路复用(IO multiplexing):
和阻塞 IO 类似,在发出时会被阻塞,但可以等待多个数据报就绪(datagram ready),即可以处理多个链接。例如 select,它相当于一个代理,用户进程调用后会被阻塞,此时 select 在内核空间会监听多个 datagram(如 socket 连接),如果启动一个数据就绪了就返回。
系统会为僵尸进程保存一定的信息,包括 pid 和运行时间等,系统所能使用的进程号是有限的,如果产生大量的僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
僵尸进程的避免:
wait
和
waitpid
等函数等待子进程结束,这会导致父进程挂起;
SIGCHLD
安装 handler,因为子进程结束后, 父进程会收到该信号,可以在 handler 中调用
wait
回收;
SIGCHLD
,
SIG_IGN
) 通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号;
fork
两次,父进程
fork
一个子进程,然后继续工作,子进程
fork
一个孙进程后退出,那么孙进程被 init 接管,孙进程结束后,init 会回收。不过子进程的回收 还要自己做。