1. 启动过程概述


图:android_boot_process

学习任何软硬件系统,研究系统启动过程都是一种非常有效地起步手段。上面的这张图可以帮助理解 Android 系统的启动过程。

(1) Boot ROM 阶段

Android 设备上电后,首先会从处理器上 ROM 的启动引导代码开始执行,片上 ROM 会需找 Boot loader 的代码,并加载到内存中。这一步由”芯片厂商”负责设计和实现。

(2) Bootloader 阶段

Bootloader 又称作引导程序, 是操作系统运行之前运行的一段程序,主要有检查 RAM、初始化系统的硬件参数等功能,然后找到 Linux kernel 的代码,设置启动参数,并最终加载到内存中。U-boot 就是一种通用引导程序。

(3) kernel 阶段

Linux 内核开始启动,初始化各种软硬件环境、加载驱动程序、挂载根文件系统、并执行 init 程序,由此开启 Android 的世界。

启动文件路径: source/kernel/init/main.c

(4) init 进程阶段

从这一步开始,就真正的迈入了 Android 的世界,init 进程是 android 世界的天子号进程,其他所有的进程都是由 init 进程直接或间接 fork 出来的。

init 进程负责启动系统最关键的几个核心 daemen 守护进程(Zygote、ServiceManager等),Zygote 进程又创建了 dalvik 虚拟机,它是 Java 世界的基础。此外还提供了诸如属性服务(property service)等一些其他的功能。

启动文件路径: source/system/core/init/init.cpp

这篇笔记主要记录 Android 世界的启动流程,也就是 init 进程到主界面点亮的这一段过程。

2. init 进程

2.1 kernel 代码启动 init 进程

kernel 运行起来之后会执行 start_kernel 函数,它负责进行 kernel 正式运行之前各个功能的初始化,在 start_kernel 函数的最后调用了 reset_init 函数启动了三个进程(idle、kernel_init、kthreadd),来进行操作系统的正式操作。

- idle 是操作系统的空闲进程,当 cpu 空闲的时候会去运行它- kernel_init 函数作为进程被启动,但是之后它将读取根文件系统下的init 程序,这个操作将完成从内核态到用户态的转变,  这个 init 进程是所有用户态进程的父进程,它生了大量的子进程,所以init进程将永远存在,其PID是1- kthreadd 是内核守护进程,其 PID2

下面的代码是 init 进程的具体的启动逻辑:

文件路径:kernel/init/main.cstatic int __ref kernel_init(void *unused){    // ramdisk_execute_command 这个值为 "./init"    if (ramdisk_execute_command) {        ret = run_init_process(ramdisk_execute_command);        if (!ret)            return 0;        pr_err("Failed to execute %s (error %d)\n",               ramdisk_execute_command, ret);    }    /*     * We try each of these until one succeeds.     *     * The Bourne shell can be used instead of init if we are     * trying to recover a really broken machine.     */    if (execute_command) {        ret = run_init_process(execute_command);        if (!ret)            return 0;        panic("Requested init %s failed (error %d).",              execute_command, ret);    }    if (!try_to_run_init_process("/sbin/init") ||        !try_to_run_init_process("/etc/init") ||        !try_to_run_init_process("/bin/init") ||        !try_to_run_init_process("/bin/sh"))        return 0;    panic("No working init found.  Try passing init= option to kernel. "          "See Linux Documentation/init.txt for guidance.");  }

下面的笔记是真正执行 init 程序的代码,通过 system/core/init/Android.mk 下面对 LOCAL_MODULE_PATH 的定义,可以知道最终 init 可执行文件的安装路径在根文件系统。

LOCAL_MODULE_PATH := $(TARGET_ROOT_OUT)

2.2 ueventd/watchdogd 跳转及环境变量设置

int main(int argc, char** argv) {    // basename 是 C 库中的一个函数,得到特定的路径中的最后一个'/'后面的内容    if (!strcmp(basename(argv[0]), "ueventd")) {        return ueventd_main(argc, argv);    }    if (!strcmp(basename(argv[0]), "watchdogd")) {        return watchdogd_main(argc, argv);    }    if (REBOOT_BOOTLOADER_ON_PANIC) {        InstallRebootSignalHandlers();    }    add_environment("PATH", _PATH_DEFPATH);    bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);}

++程序代码说明++:
1. C++ 中主函数有两个参数,第一个参数 argc 代表参数个数,第二个参数是参数列表
2. 如果程序运行无其他参数,argc = 1,argv[0] = 执行进程的路径
3. init 进程有两个其他入口,ueventd(进入 ueventd_main)以及 watchdogd(进入 watchdogd_main)

rk3288:/sbin # ls -allrwxrwxrwx  1 root root       7 1969-12-31 19:00 ueventd -> ../initlrwxrwxrwx  1 root root       7 1969-12-31 19:00 watchdogd -> ../init可以看到 watchdog 和 ueventd 是一个软链接,直接链接到 init 程序所以当执行 /sbin/ueventd 或 /sbin/watchdogd 时,将会进入相应的 ueventd_main 和 watchdogd_main 入口点
  1. ueventd 主要是负责设备节点的创建、权限设定等一些列工作
  2. watchdogd 俗称看门狗,用于系统出问题时重启系统

2.2.1 ueventd_main

文件定义在 source/system/core/init/ueventd.cpp

Android 和 Linux 一样使用设备驱动来访问硬件设备,设备节点文件就是设备驱动的逻辑文件。但是 Android 根文件系统的映像中不存在 “/dev” 目录,该目录是 init 进程启动后动态创建的。

因此,创建 Android 设备节点文件的重任也在 init 进程身上,这就是 ueventd 的工作。

ueventd 通过两种方式创建设备节点文件

第一种方式对应 "冷插拔"(Cold Plug)即以预先定义的设备信息为基础,当 ueventd 启动后,统一创建设备节点文件。这一类设备节点文件也被称为静态节点文件。第二种方式对应 "热插拔"(Hot Plug))即在系统运行中,当有设备插入 USB 端口时,ueventd 就会接收到这一事件,为  插入的设备动态创建设备节点文件。这一类设备节点文件也被称为动态节点文件
文件路径:source/system/core/init/ueventd.cppint ueventd_main(int argc, char** argv) {    // 创建新建文件的权限默认值    // 与 chmod 相反,这里相当于新建文件后权限为 666     umask(000);    // 初始化日志输出    InitKernelLogging(argv);    LOG(INFO) << "ueventd started!";    // 注册 selinux 相关的用于打印 log 的回调函数    selinux_callback cb;    cb.func_log = selinux_klog_callback;    selinux_set_callback(SELINUX_CB_LOG, cb);    DeviceHandler device_handler = CreateDeviceHandler();    // 创建 socket,用于监听 uevent 事件    UeventListener uevent_listener;    // 通过 access 判断文件 /dev/.coldboot_done 是否存在    // 若已经存在则表明已经进行过冷插拔了    if (access(COLDBOOT_DONE, F_OK) != 0) {        ColdBoot cold_boot(uevent_listener, device_handler);        cold_boot.Run();    }    // We use waitpid() in ColdBoot, so we can't ignore SIGCHLD until now.    signal(SIGCHLD, SIG_IGN);    // Reap and pending children that exited between the last call to waitpid() and setting SIG_IGN    // for SIGCHLD above.    while (waitpid(-1, nullptr, WNOHANG) > 0) {    }    // 监听事件,进行热插拔处理    uevent_listener.Poll([&device_handler](const Uevent& uevent) {        HandleFirmwareEvent(uevent);        device_handler.HandleDeviceEvent(uevent);        return ListenerAction::kContinue;    });    return 0;}

2.2.2 watchdogd_main

“看门狗”本身是一个定时器电路,内部会不断的进行计时(或计数)操作,计算机系统和”看门狗”有两个引脚相连接,正常运行时每隔一段时间就会通过其中一个引脚向”看门狗”发送信号,”看门狗”接收到信号后会将计时器清零并重新开始计时。

一旦系统出现问题,进入死循环或任何阻塞状态,不能及时发送信号让”看门狗”的计时器清零,当计时结束时,”看门狗”就会通过另一个引脚向系统发送”复位信号”,让系统重启。

watchdogd_main 主要是定时器作用,而 DEV_NAME 就是那个引脚

主要操作就是”喂狗”,往 DEV_NAME 写入数据复位信号

文件路径:source/system/core/init/watchdogd.cppint fd = open(DEV_NAME, O_RDWR|O_CLOEXEC); if (fd == -1) {    PLOG(ERROR) << "Failed to open " << DEV_NAME;    return 1;}...while (true) {    write(fd, "", 1);    sleep(interval);}

2.2.3 install_reboot_signal_handlers

文件路径:source/system/core/init/init.cppif (REBOOT_BOOTLOADER_ON_PANIC) {    install_reboot_signal_handlers(); }

REBOOT_BOOTLOADER_ON_PANIC 在顶层 init 模块的 mk 文件中定义,userdebug 和 eng 版本的固件会打开该选项。

主要作用是:当 init 进程崩溃时,重启 bootloader,让用户更容易定位问题。

install_reboot_signal_handlers 函数将各种信号量,如 SIGABRT、SIGBUS 等的行为设置为 SA_RESTART,一旦监听到这些信号即执行重启系统。

2.3 init 进程第一阶段

在 init 的代码中根据环境变量 INIT_SECOND_STAGE 执行两条分路的代码,第一次执行完成之后,就将 INIT_SECOND_STAGE 的值设置为 true,然后重新执行一遍 init 程序,走第二条分路的代码,在下面的记录中将 init 第一阶段的执行称为内核态执行,第二阶段的执行称为用户态执行。

2.3.1 挂载基本文件系统并创建目录

文件路径:文件路径:source/system/core/init/init.cppbool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);if (is_first_stage) {    boot_clock::time_point start_time = boot_clock::now();    // Clear the umask.    umask(0); // 设置 umask 值为0,清空访问权限屏蔽码    // Get the basic filesystem setup we need put together in the initramdisk    // on / and then we'll let the rc file figure out the rest.    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));    // Don't expose the raw commandline to unprivileged processes.    chmod("/proc/cmdline", 0440);    gid_t groups[] = { AID_READPROC };    setgroups(arraysize(groups), groups);    mount("sysfs", "/sys", "sysfs", 0, NULL);    mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);    mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));    mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));    mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));    ...}...
is_first_stage

init 的 main 函数会执行两次,由 is_first_stage 环境变量控制

挂载基本文件系统

android init 进程在内核态的执行过程中,需要挂载上基本的文件系统。

文件系统相关函数的说明的介绍可以查看 这里

其中,/dev/ 分区是临时文件系统 tmpfs,使用 RAM 将所有的文件储存在虚拟内存中,
主要用于创建和存放设备文件节点,该分区可根据需要动态调整。

/sys/ 分区使用 sysfs 文件系统,把连接在系统上的设备和总线组织成为一个分级的文件,使得它们可以在用户空间存取。

/proc/ 分区使用 proc 文件系统,proc 文件系统是一个非常重要的虚拟文件系统,它可以看作是内核内部数据结构的接口。通过它我们可以获得系统的信息,同时也能够在运行时修改特定的内核参数。

selinuxfs 是虚拟文件系统,通常挂载在 /sys/fs/selinux,用来存放 SELinux 安全策略文件。

2.3.2 初始化日志输出

文件路径:文件路径:source/system/core/init/init.cpp// Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually// talk to the outside world...InitKernelLogging(argv);

这句的作用就是将 KernelLogger 函数作为 log 日志的处理函数,KernelLogger 主要作用就是将要输出的日志格式化之后写入到 /dev/kmsg 设备中。

2.3.3 挂载 system、vendor 等系统分区(DoFirstStageMount)

文件路径:文件路径:source/system/core/init/init_first_stage.cppbool DoFirstStageMount() {    // Skips first stage mount if we're in recovery mode.    if (IsRecoveryMode()) {        LOG(INFO) << "First stage mount skipped (recovery mode)";        return true;    }    // Firstly checks if device tree fstab entries are compatible.    if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) {        LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";        return true;    }    std::unique_ptr<FirstStageMount> handle = FirstStageMount::Create();    if (!handle) {        LOG(ERROR) << "Failed to create FirstStageMount";        return false;    }    return handle->DoFirstStageMount();}

Android 8.1 系统将 system、vendor 分区的挂载功能移植到 kernel device-tree 中进行。

在 kernel 的 dts 文件中,需要包含如下的 firmware 分区挂载节点,在 DoFirstStageMount 函数执行过程中会检查、读取 device-tree 中记录的分区挂载信息。

firmware {    android {        compatible = "android,firmware";        fstab {            compatible = "android,fstab";            system {                compatible = "android,system";                dev = "/dev/block/by-name/system";                type = "ext4";                mnt_flags = "ro,barrier=1,inode_readahead_blks=8";                fsmgr_flags = "wait";                                                                                                   };              vendor {                compatible = "android,vendor";                dev = "/dev/block/by-name/vendor";                type = "ext4";                mnt_flags = "ro,barrier=1,inode_readahead_blks=8";                fsmgr_flags = "wait";            };          };      };  };

is_android_dt_value_expected

// Firstly checks if device tree fstab entries are compatible.if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) {    LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";    return true;}

android device-tree 目录默认在 /proc/device-tree/firmware/android 下,如果 kernel 的启动参数 /proc/cmdline 中包含 androidboot.android_dt_dir 值得设定,则直接使用。

首先确认 android device-tree 目录下 fstab/compatible 目录属性值是不是 “android,fstab”。

dts compatible 节点的组织形式为 ,。 android,fstab 代表执行的功能为 fstab 分区挂载。

handle->DoFirstStageMount

文件路径:文件路径:source/system/core/init/init_first_stage.cppFirstStageMount::FirstStageMount()    : need_dm_verity_(false), device_tree_fstab_(fs_mgr_read_fstab_dt(), fs_mgr_free_fstab) {    if (!device_tree_fstab_) {        LOG(ERROR) << "Failed to read fstab from device tree";        return;    }    // Stores device_tree_fstab_->recs[] into mount_fstab_recs_ (vector)    // for easier manipulation later, e.g., range-base for loop.    for (int i = 0; i < device_tree_fstab_->num_entries; i++) {        mount_fstab_recs_.push_back(&device_tree_fstab_->recs[i]);    }}bool FirstStageMount::DoFirstStageMount() {    // Nothing to mount.    if (mount_fstab_recs_.empty()) return true;    if (!InitDevices()) return false;    if (!MountPartitions()) return false;    return true;}

FirstStageMount 的构造函数中通过 fs_mgr_read_fstab_dt 函数读取 /proc/device-tree/firmware/android/fstab 目录下的分布挂载信息,最后统计成 fstab_rec 类型的 vector 数组,

struct fstab_rec {    char* blk_device;    char* mount_point;    char* fs_type;    unsigned long flags;    char* fs_options;    int fs_mgr_flags;    char* key_loc;    char* key_dir;    char* verity_loc;    long long length;    char* label;    int partnum;    int swap_prio;    int max_comp_streams;    unsigned int zram_size;    uint64_t reserved_size;    unsigned int file_contents_mode;    unsigned int file_names_mode;    unsigned int erase_blk_size;    unsigned int logical_blk_size;};

MountPartitions() 函数遍历 fstab_rec 数组,找到 mount_source 和 mount_target,使用 mount 函数将 system、vendor或者 oem 分区挂载上。

成功挂载的 Log 打印如下:

[    1.608773] init: [libfs_mgr]__mount(source=/dev/block/by-name/system,target=/system,type=ext4)=0: Success[    1.611679] init: [libfs_mgr]__mount(source=/dev/block/by-name/vendor,target=/vendor,type=ext4)=0: Success

2.3.4 启动 SELinux 安全策略

SELinux是「Security-Enhanced Linux」的简称,是美国国家安全局「NSA=The National Security Agency」
和 SCC(Secure Computing Corporation)开发的 Linux 的一个扩张强制访问控制安全模块。
在这种访问控制体系的限制下,进程只能访问那些在他的任务中所需要文件。

SELinux 在 Andoird 中的具体应用可以点击 android 8.1 安全机制 — SEAndroid & SELinux

具体的源码分析查看 4.2 节介绍

2.3.5 开始第二阶段用户态执行的准备

这里主要就是设置一些变量如 INIT_SECOND_STAGE、INIT_STARTED_AT,为第二阶段做准备,然后再次调用 init 的 main 函数,启动用户态的 init 进程

if (is_first_stage) {    ...    setenv("INIT_SECOND_STAGE", "true", 1);    static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;    uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;    setenv("INIT_STARTED_AT", StringPrintf("%" PRIu64, start_ms).c_str(), 1);//记录第二阶段开始时间戳    char* path = argv[0];    char* args[] = { path, nullptr };    execv(path, args); //重新执行main方法,进入第二阶段    // execv() only returns if an error happened, in which case we    // panic and never fall through this conditional.    PLOG(ERROR) << "execv(\"" << path << "\") failed";    security_failure();}

小结

init 进程第一阶段做的主要工作是
1. 挂载分区 dev、system、vendor
2. 创建设备节点及设备节点热插拔事件监听处理(ueventd)
3. 创建一些关键目录、初始化日志输出系统
4. 启用 SELinux 安全策略

更多相关文章

  1. 关于用百度地图开发的准备工作
  2. Android(安卓)R文件消失
  3. Android中的sdk相关介绍
  4. 在Service中新开线程和直接新开线程的区别与意义
  5. 为你的APK进行数字签名
  6. unity和Android之间互相调用
  7. apk自我保护的一种实现方式——运行时自篡改dalvik指令
  8. Android(安卓)Service更新UI的方法之AIDL
  9. android桌面小部件appwidget使用ListView或者StackView如何刷新

随机推荐

  1. Android 开源项目列表
  2. Android调用外部程序
  3. 关于广播接受者的问题
  4. Android小项目——简易备忘录
  5. android获取通知权限
  6. VisionMobile:2014年Q1移动开发者经济报告
  7. 基于android的音乐APP大作业和设计指导项
  8. android studio 实现再按一次返回键退出
  9. Cordova + vue 打包安卓(Android) apk 及
  10. android短信监听