启动进程(BDK/system/core/init)分析
编者:厦门大学信息学院电子工程系2015级研究生 陈琼、陈硕,通信工程系2015级研究生 刘妍
init启动过程
概述
Brillo是一个物联网底层操作系统。它是源于Android,是对Android底层的一个细化,得到了Android的全部支持。而
Android是基于Linux的操作系统,所以init也是Android系统中用户空间的第一个进程,它的进程号是1。下面先简单的看一下init进程的启动过程。
LINUX进程启动图
目前Linux有很多通讯机制可以在用户空间和内核空间之间交互,例如设备驱动文件(位于/dev目录中)、内存文件(/proc、/sys目录等)。了解Linux的同学都应该知道Linux的重要特征之一就是一切都是以文件的形式存在的,例如,一个设备通常与一个或多个设备文件对应。这些与内核空间交互的文件都在用户空间,所以在Linux内核装载完,需要首先建立这些文件所在的目录。而完成这些工作的程序就是本文要介绍的init。Init是一个命令行程序。其主要工作之一就是建立这些与内核空间交互的文件所在的目录。当Linux内核加载完后,要做的第一件事就是调用init程序,也就是说,init是用户空间执行的第一个程序。
在分析init的核心代码之前,还需要初步了解init除了建立一些目录外,还做了如下的工作
初始化属性
处理配置文件的命令(主要是init.rc文件),包括处理各种Action。
性能分析(使用bootchart工具)。
无限循环执行command(启动其他的进程)。
尽管init完成的工作不算很多,不过代码还是非常复杂的。Init程序并不是由一个源代码文件组成的,而是由一组源代码文件的目标文件链接而成的。这些文件位于如下的目录。
BDK/system/core/init
主函数分析
其中init.cpp是init的主文件,现在打开该文件,看看其中的内容。
由于init是命令行程序,所以分析init.cpp首先应从main函数开始,main函数代码如下:
```int main(int argc, char* argv) {
/代码
程序的main函数原型为 main(int argc, char* argv[]), ueventd以及watchdogd的启动都在init.rc中描述,
由
init进程解析后执行fork、exec启动,因此其入口参数的构造在init代码中,将在init.rc解析时分析。此时我们只
需要直到argv[0]中将存储可执行文件的名字。
*/
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (!strcmp(basename(argv[0]), "watchdogd")) {
return watchdogd_main(argc, argv);
}
// Clear the umask.
/*
代码<part2>
umaks(0)用于设定当前进程(即/init)的文件模型创建掩码(file mode creation mask),注意这里的
文件是广泛意义上的文件,包括普通文件、目录、链接文件、设备节点等。
PS. 以上解释摘自umask的mannual,可在linux系统中执行man 3 umask查看。
Linux C库中mkdir与open的函数运行如下。
int mkdir(const char *pathname, mode_t mode);
int open(const char *pathname, int flags, mode_t mode);
Linux内核给每一个
进程都设定了一个掩码,当程序调用open、mkdir等函数创建文件或目录时,传入open的mode会现
在掩码做运算,得到的文件mode,才是文件真正的mode。
譬如要创建一个目录,并设定它的文件权限
为0777,mkdir("testdir", 0777)
但实际上写入的文件权限却未必是777,因为mkdir系统调用在创建testdir时,
会将0777与当前进程的掩码(称为umask)运算,具体运算方法为 0777&~umask作为testdir的真正权限。
因此上述init中首先调用umask(0)将进程掩码清0,这样调用open/mkdir等函数创建文件或目录时
,传入的mode就会作为实际的值写入文件系统。
umask(0);
add_environment("PATH", _PATH_DEFPATH);
bool is_first_stage = (argc == 1) || (strcmp(argv[1], "--second-stage") != 0);
/* 接下来创建目录,并挂载内核文件系统,它们是
*tmpfs,虚拟内存文件系统,该文件系统被挂载到/dev目录下,
主要存放设备节点文件,用户进程通过访问/dev目录下的设备节点文件可以
与硬件驱动程序交互。
*devpts,一种虚拟终端文件系统
*proc,虚拟文件系统,被挂载到/proc目录下,通过该文件系统可与内核数据结构交互
,查看以及设定内核参数。
*sysfs,虚拟文件系统,被挂载到/sys目录下,它与proc类似,是2.6内核在吸收
了proc文件系统的设计经验和教训的基础上所实现的一种较新的文件系统,为内核提供了统一的设
备驱动模型。(引用:http://www.ibm.com/developerworks/cn/linux/l-cn-sysfs/index.html)*/
if (is_first_stage) {
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
mkdir("/dev/pts", 0755);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL);
#define MAKE_STR(x) __STRING(x)
mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
mount("sysfs", "/sys", "sysfs", 0, NULL);
}
// We must have some place other than / to create the device nodes for
// kmsg and null, otherwise we won't be able to remount / read-only
// later on. Now that tmpfs is mounted on /dev, we can actually talk
// to the outside world.
/*该函数名字暗示将init进程的stido,包括stdin(标准输入,文件描述符为0)、stdout
(标准输出,文件描述符为1)以及stderr(标准错误,文件描述符号为2),全部重定向/dev/null
设备,但是细心的读者可能会有疑问,在代码<part2>中虽然挂载了tmpfs文件系统到/dev目录下,但
是并未创建任何设备节点文件,/dev/null此时并不存在啊,如何才能将stdio重定向到null设备中
呢?此时Anrdoid系统上处于启动的早期阶段,可用于接收init进程标准输出、标准错误的设备节点还
不存在。因此init进程一不做二不休,直接把它们重定向到/dev/__nulll__了。*/
open_devnull_stdio();
klog_init();
klog_set_level(KLOG_NOTICE_LEVEL);
NOTICE("init %s started!\n", is_first_stage ? "first stage" : "second stage");
if (!is_first_stage) {
// Indicate that booting is in progress to background fw loaders, etc.
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
//这一句用来初始化Android的属性系统,将在init之属性系统中专门介绍。
property_init();
// If arguments are passed both on the command line and in DT,
// 接下来init程序调用函数process_kernel_cmdline解析内核启动参数。
内核通常由bootloader(启动引导程序)加载启动,目前广泛使用的bootloader大都基
于u-boot定制。
内核允许bootloader启动自己时传递参数。在内核启动完毕之后,启动参数可
通过/proc/cmdline查看。
process_kernel_dt();
process_kernel_cmdline();
// Propagate the kernel variables to internal variables
// used by init as well as the current required properties.
export_kernel_boot_props();
}
/*这部分代码是在Android4.1之后添加的,随后伴随Android系统更新不停迭代。
这段代码主要涉及SELinux初始化。由于SELinux与Android系统启动关闭不大,暂不分析。*/
// Set up SELinux, including loading the SELinux policy if we're in the kernel domain.
selinux_initialize(is_first_stage);
// If we're in the kernel domain, re-exec init to transition to the init domain now
// that the SELinux policy has been loaded.
if (is_first_stage) {
if (restorecon("/init") == -1) {
ERROR("restorecon failed: %s\n", strerror(errno));
security_failure();
}
char* path = argv[0];
char* args[] = { path, const_cast<char*>("--second-stage"), nullptr };
if (execv(path, args) == -1) {
ERROR("execv(\"%s\") failed: %s\n", path, strerror(errno));
security_failure();
}
}
// These directories were necessarily created before initial policy load
// and therefore need their security context restored to the proper value.
// This must happen before /dev is populated by ueventd.
NOTICE("Running restorecon...\n");
restorecon("/dev");
restorecon("/dev/socket");
restorecon("/dev/__properties__");
restorecon("/property_contexts");
restorecon_recursive("/sys");
epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
ERROR("epoll_create1 failed: %s\n", strerror(errno));
exit(1);
}
signal_handler_init();
property_load_boot_defaults();
export_oem_lock_status();
start_property_service();
const BuiltinFunctionMap function_map;
Action::set_function_map(&function_map);
Parser& parser = Parser::GetInstance();
parser.AddSectionParser("service",std::make_unique<ServiceParser>());
parser.AddSectionParser("on", std::make_unique<ActionParser>());
parser.AddSectionParser("import", std::make_unique<ImportParser>());
parser.ParseConfig("/init.rc");
/*
* 解析完init.rc后会得到一系列的action等,下面的代码将执行处于early-init阶段的action。
* init将action按照执行时间段的不同分为early-init、init、early-boot、boot。
* 进行这样的划分是由于有些动作之间具有依赖关系,某些动作只有在其他动作完成后才能执行,
* 所以就有了先后的 区别。
* 具体哪些动作属于哪个阶段是在init.rc中的配置决定的
*/
ActionManager& am = ActionManager::GetInstance();
am.QueueEventTrigger("early-init");
// Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
// ... so that we can start queuing up actions that require stuff from /dev.
am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");
am.QueueBuiltinAction(keychord_init_action, "keychord_init");
am.QueueBuiltinAction(console_init_action, "console_init");
// Trigger all the boot actions to get us started.
am.QueueEventTrigger("init");
// Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
// wasn't ready immediately after wait_for_coldboot_done
am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");
/*这段函数将利用bootmode与字符串"charger"将其保存到is_charger变量中,
is_charger非0表明但前Android是以充电模式启动,否则为正常模式。正常启动
模式与充电模式需要启动的进程不同的,这两种模式启动具体启动的程序差别将在init.rc 解析时介绍。*/
// Don't mount filesystems or start core system services in charger mode.
std::string bootmode = property_get("ro.bootmode");
if (bootmode == "charger") {
am.QueueEventTrigger("charger");
} else {
am.QueueEventTrigger("late-init");
}
// Run all property triggers based on current state of the properties.
am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");
while (true) {
if (!waiting_for_exec) {
am.ExecuteOneCommand();
restart_processes();
}
int timeout = -1;
if (process_needs_restart) {
timeout = (process_needs_restart - gettime()) * 1000;
if (timeout < 0)
timeout = 0;
}
if (am.HasMoreCommands()) {
timeout = 0;
}
bootchart_sample(&timeout);
epoll_event ev;
int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout));
if (nr == -1) {
ERROR("epoll_wait failed: %s\n", strerror(errno));
} else if (nr == 1) {
((void (*)()) ev.data.ptr)();
}
}
return 0;}```
可以看出在main()函数的最后,init进入了一个无限循环,并等待一些事情的发生。 即:在执行完前面的初始化工作以后,init变为一个守护进程。init所关心的事件 有三类:属性服务事件、keychord事件和SIGNAL,当有这三类事件发生时, init进程会调用相应的handle函数进行处理。
主要函数简介
函数名 | 所在文件 | 功能概述 | |
---|---|---|---|
mkdir | system/core/init/init.cpp | 建立文件系统的基本目录 | |
mount | system/core/init/init.cpp | 装载文件系统 | |
open_devnull_stdio | system/core/init/init.cpp | 打开基本输入、输出设备 | |
log_init | system/core/init/init.cpp | 初始化日志功能 | |
init_parse_config_file | system/core/init/init_parser.cpp | 读取init.rc文件内容到内存数据区 | |
parse_config | system/core/init/init_parser.cpp | 识别init.rc文件中的 | Section(service and action series)和Text |
parse_new_section | system/core/init/init_parser.cpp | 识别section类别 | |
parse_service | system/core/init/init_parser.cpp | 对service | section第一行进行分析 |
parse_line_service | system/core/init/init_parser.cpp | 对service section的option选项进行分析 | |
parse_action | system/core/init/init_parser.cpp | 对action | section第一行进行分析 |
parse_line_action | system/core/init/init_parser.cpp | 对action | section的每一行独立的命令进行分析 |
action_for_each_trigger | system/core/init/init_parser.cpp | 触发某个action的执行 | |
action_add_queue_tail | system/core/init/init_parser.cpp | 将某个action的从action_list加到action_queue | |
execute_one_command | system/core/init/init.cpp | 执行当前action的一个command | |
action_remove_queue_head | system/core/init/init_parser.cpp | 从action_queue链表上移除头结点(action) | |
do_class_start | system/core/init/builtins.cpp | class_start | default对应的入口函数,主要用于启动nativeservice |
service_for_each_class | system/core/init/init_parser.cpp | 遍历service_list链表上的所有结点 | |
service_start_if_not_disabled | system/core/init/builtins.cpp | 判断service的flag是否disabled,如果不是,则调用相关函数,准备启动service | |
service_start | system/core/init/init.cpp | 启动service的主要入口函数,设置service数据结构的相关数据结构后,调用fork创建一个新的进行,然后调用execve执行新的service | |
fork Lib | function(ulibc) | 进程创建函数 | |
execve | Lib function(ulibc) | 调用执行新的service | |
poll | Lib function(ulibc) | 查询property_set_fd,signal_fd和keychord_fd文件句柄是否有服务请求 | |
handle_property_set_fd | system/core/init/property_service.cpp | 处理系统属性服务请求,如:service, | wlan和dhcp等等 |
handle_keychord | system/core/init/keycords.cpp | 处理注册在service | structure上的keychord,通常是启动service |
handle_signal | system/core/init/signal_handler.cpp | 处理SIGCHLDsignal |
认识init.rc和了解如何编写init.c
在前面分析了init进程的启动过程和main函数,以下内容将着重对配置文件
(init.rc)的解析做一下分析以及如何形成一份init.c。
init.rc脚本语法
init.rc文件不同于init进程,init进程仅当编译完Android后才会生成,而init.rc文 件存在于Android平台源代码中。init.rc在源代码中的位置为:@system/core /rootdir/init.rc。init.rc文件的大致结构如下图所示: init.rc文件并不是普通的配置文件,而是由一种被称为“Android初始化语言”(Android Init Language,这里简称为AIL)的脚本写成的文件。在了解init如何解析init.rc文件 之前,先了解AIL非常必要,否则机械地分析init.c及其相关文件的源代码毫无意义。
为了学习AIL,读者可以到自己Android手机的根目录寻找init.rc文件,最好下载到本地以
便查看,如果有编译好的Android源代码,在
AIL由如下4部分组成。
1. 动作(Actions)
2. 命令(Commands)
3.服务(Services)
4. 选项(Options)
这4部分都是面向行的代码,也就是说用回车换行符作为每一条语句的分隔符。
而每一行的代码由多个符号(Tokens)表示。可以使用反斜杠转义符在Token中插入空格。
双引号可以将多个由空格分隔的Tokens合成一个Tokens。如果一行写不下,可以在行尾
加上反斜杠,来连接下一行。也就是说,可以用反斜杠将多行代码连接成一行代码。
AIL的注释与很多Shell脚本一行,以#开头。
AIL在编写时需要分成多个部分(Section),而每一部分的开头需要指定Actions或
Services。也就是说,每一个Actions或Services确定一个Section。而所有的
Commands和Options只能属于最近定义的Section。如果Commands和Options在
第一个Section之前被定义,它们将被忽略。
Actions和Services的名称必须唯一。如果有两个或多个Action或Service拥有同样的 名称,那么init在执行它们时将抛出错误,并忽略这些Action和Service。 如何去写 Android init.rc (Android init language) Android 初始化语言由四大类声明组成 : 行为类 (Actions), 命 令类 (Commands) , 服务类 (Services), 选项类 (Options).
- 初始化语言以行为单位,由以空格间隔的语言符号组成。
- C 风格的反斜杠转义符可以用来插入空白到语言符号。
- 双引号也可以用来防止文本被空格分成多个语言 符号。
- 当反斜杠在行末时,作为折行符。
- 以 # 开 始 ( 前面允许有空格 ) 的行为注释行。
- Actions 和 Services 隐 含声明一个新的段落。
- 所有该段落下 Commands 或 Options 的声明属于该段落。
- 第一段落前的 Commands 或 Options 被 忽略。
- Actions 和 Services 拥 有独一无二的命名。在它们之后声明相同命名的类将被当作错误并忽略。
Actions
Actions 是一系列命令的命名。 Actions 拥有一个触发器 (trigger) 用 来决定 action 何时 执行。当一个 action 在符合触发条件被执行时,如果它还没被加入到待执行队列中的话,则加入到队列最后。
队列中的 action 依次执行, action 中的命令也依次执行。 Init 在 执行命令的中间处理其
它活动 ( 设备创建 / 销毁 ,property 设 置,进程重启 ) 。
Actions 表现形式为:
on
Services
Services 是由 init 启 动 ,在它们退出时重启 ( 可选 ) 。 Service 表 现形式为 :
service
Options
Options 是 Services 的 修饰,它们影响 init 何时、如何运行 service .
critical 这是一个设备关键服务 (device-critical service) . 如果它在 4 分钟内退出超过 4 次, 设备将重启并进入恢复模式。
disabled
- 这个服务的级别将不会自动启动,它必须被依照服务名指定启动才可以启 动。
setenv
设置已启动的进程的环境变量 <name> 的值 <value>
socket
创建一个名为 /dev/socket/<name> 的 unix domin socket ,并传送它的 fd 到已启动的进程
。 <type> 必 须为 "dgram" 或 "stream". 用户和组默认为 0.
user
在执行服务前改变用户名。当前默认为 root. 如果你的进程需要 linux 能 * 力,你不能使用这个
命令。你必须在还是 root 时请求能力,并下降到你 需要的 uid.
group
在执行服务前改变组。在第一个组后的组将设为进程附加组 ( 通过 setgroups()). 当 前默认为
root.
oneshot
在服务退出后不重启。
class
为 service 指 定一个类别名。同样类名的所有的服务可以一起启动或停止。如果没有指定类别的服务默认为 "default" 类。
onrestart
当服务重启时执行一个命令。
Triggers
Triggers( 触发器 ) 是一个字符串,可以用来匹配某种类型的事件并执行一个 action 。
boot
这是当 init 开 始后执行的第一个触发器 ( 当 /init.conf 被加载 )
当 property <name> 被设为指定的值 <value> 时 触发。
device-added-
device-removed-
当设备节点被添加或移除时触发。
service-exited-
当指定的服务存在时触发
Commands
exec
Fork 并执行一个程序 (<path>). 这将被 block 直 到程序执行完毕。
最好避免执行例如内建命令以外的程序,它可能会导致 init 被 阻塞不动。
export
设定全局环境变量 <name> 的值 <value> , 当这个命令执行后所有的进程都可以取得。
ifup
使网络接口 <interface> 联 机。
import
解析一个 init 配 置文件,扩展当前配置文件。
hostname
设置主机名
chmod
改变文件访问权限
chown
改变文件所属和组
class_start
当指定类别的服务没有运行,启动该类别所有的服务。
class_stop
当指定类别的服务正在运行,停止该类别所有的服务。
domainname
设置域名。
insmod
加载该路径 <path> 的 模块
mkdir
在 <path> 创 建一个目录 , 可选选项 :mod,owner,group.
如果没有指定,目录以 755 权限, owner 为 root,group 为
root 创 建 .
mount
尝试 mount <device> 到目录 <dir>. <device> 可以用 mtd@name
格式以命名指定一个 mtd 块设备。 <mountoption> 包 含 "ro","rw","remount","noatime".
setkey
setprop
设置系统 property <name> 的值 <value>.
setrlimit
设置 resource 的 rlimit.
start
启动一个没有运行的服务。
stop
停止一个正在运行的服务
。
symlink
创建一个 <path> 的 符号链接到 <target>
sysclktz
设置系统时区 (GMT 为 0)
trigger
触发一个事件。用于调用其它 action 。
write
打开 <path> 的 文件并写入一个或多个字符串。
Properties
Init 会更新一些系统 property 以 提供查看它正在干嘛。
init.action 当前正在执行的 action, 如 果没有则为 ""
init.command
被执行的命令,如果没有则为 ""
init.svc.
命名为 <name> 的 服务的状态 ("stopped", "running", "restarting")
init.rc 示例 :
```# not complete -- just providing some examples of usage # on boot export PATH /sbin:/system/sbin:/system/bin
export LD_LIBRARY_PATH /system/lib
mkdir /dev
mkdir /proc
mkdir /sys
mount tmpfs tmpfs /dev
mkdir /dev/pts
mkdir /dev/socket
mount devpts devpts /dev/pts
mount proc proc /proc
mount sysfs sysfs /sys
write /proc/cpu/alignment 4
ifup lo
hostname localhost
domainname localhost
mount yaffs2 mtd@system /system
mount yaffs2 mtd@userdata /data
import /system/etc/init.conf
class_start default
service adbd /sbin/adbd
user adb
group adb
service usbd /system/bin/usbd -r
user usbd
group usbd
socket usbd 666
service zygote /system/bin/app_process -Xzygote /system/bin --zygote
socket zygote 666
service runtime /system/bin/runtime
user system
group system
on device-added-/dev/compass
start akmd
on device-removed-/dev/compass
stop akmd
service akmd /sbin/akmd
disabled
user akmd
group akmd```
调试
默认情况下, init 执行的程序输出的信息和错误到 /dev/null. 为了 debug , 你可以通过 Android 程序 logwrapper 执行你的程序。这将复位向输出 / 错误输出到 Android logging 系统 ( 通过 logcat 访问 ) 。 例如 service akmd /system/bin/logwrapper /sbin/akmd