启动进程(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除了建立一些目录外,还做了如下的工作

  1. 初始化属性

  2. 处理配置文件的命令(主要是init.rc文件),包括处理各种Action。

  3. 性能分析(使用bootchart工具)。

  4. 无限循环执行command(启动其他的进程)。

    尽管init完成的工作不算很多,不过代码还是非常复杂的。Init程序并不是由一个源代码文件组成的,而是由一组源代码文件的目标文件链接而成的。这些文件位于如下的目录。

BDK/system/core/init


主函数分析

 其中init.cpp是init的主文件,现在打开该文件,看看其中的内容。
 由于init是命令行程序,所以分析init.cpp首先应从main函数开始,main函数代码如下:

```int main(int argc, char* argv) { /代码 通过命令行判断argv[0]的字符串内容,来区分当前程序是init,ueventd或是watchdogd。

程序的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源代码,在out/target/product /geneic/root目录也可找到init.rc文件。

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).

  • 初始化语言以行为单位,由以空格间隔的语言符号组成。
    1. C 风格的反斜杠转义符可以用来插入空白到语言符号。
    2. 双引号也可以用来防止文本被空格分成多个语言 符号。
    3. 当反斜杠在行末时,作为折行符。
  • 以 # 开 始 ( 前面允许有空格 ) 的行为注释行。
  • Actions 和 Services 隐 含声明一个新的段落。
    1. 所有该段落下 Commands 或 Options 的声明属于该段落。
    2. 第一段落前的 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 .

  1. critical 这是一个设备关键服务 (device-critical service) . 如果它在 4 分钟内退出超过 4 次, 设备将重启并进入恢复模式。

  2. disabled

  3. 这个服务的级别将不会自动启动,它必须被依照服务名指定启动才可以启 动。

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 [mode] [owner] [group]

 在 <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

results matching ""

    No results matching ""