一、启动过程概述

                                                图:android_boot_process

 

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

1、Boot ROM 阶段

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

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 进程到主界面点亮的这一段过程。

二、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 是内核守护进程,其 PID 为 2;

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

文件路径:kernel/init/main.c

static 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 := $(TRAGET_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 -al
lrwxrwxrwx  1 root root       7 1969-12-31 19:00 ueventd -> ../init
lrwxrwxrwx  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 通过两种方式创建设备节点文件:

1、第一种方式对应 “冷插拔”(Cold Plug);即以预先定义的设备信息为基础,当 ueventd 启动后,统一创建设备节点文件。这一类设备节点文件也被称为静态节点文件。

2、第二种方式对应 “热插拔”(Hot Plug);即在系统运行中,当有设备插入 USB 端口时,ueventd 就会接收到这一时间,为插入的设备动态创建设备节点文件。这一类设备节点文件也被称为动态节点文件。

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

int 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

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

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

watchdog_main 主要是定时器作用,而 DEV_NAME 就是那个引脚,主要操作就是“喂狗”,往 DEV_NAME 写入数据复位信号。

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

int 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.cpp

if (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.cpp。

bool 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.cpp。

bool 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 handle = FirstStageMount::Create();    if (!handle) {        LOG(ERROR) << "Failed to create FirstStageMount";        return false;    }    return handle->DoFirstStageMount();}

Android8.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.cpp。

FirstStageMount::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] 的简称,是美国国家安全局和 SCC(Secure Computing Corporation) 开发的 Linux 的一个扩展强制访问控制安全模块。在这种访问控制体系的限制下,进程只能访问那些在它的任务中所需要的文件。

SELinux 在 Android 中的具体应用可以点击 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. 数据共享之Android中用Application类实现全局数据变量的使用
  2. Android(安卓)学习之《第一行代码》第二版 笔记(二十)播放多媒体文
  3. Android(安卓)SlidingMenu 开源项目 侧拉菜单的使用(详细配置)
  4. Android(安卓)Studio--文件存储
  5. android开发教程(十一)——android应用程序基础
  6. “adb不是内部或外部命令,也不是可运行的程序或批量文件“
  7. Android(安卓)NDK 交叉编译
  8. plist读写,NSArray,NSData,NSnumber,字典等简使用
  9. cocos2d-x在Android真机上使用Sqlite

随机推荐

  1. android 对象传输及parcel机制
  2. 玩转Android之Activity详细剖析
  3. Phonegap Device 获取设备信息
  4. mac终端配置Android(安卓)ADB命令
  5. Ubuntu11.10下编译android源码4.0.3
  6. 为什么应用商店里搜索不到你的App?
  7. android中的事件类型分为按键事件和屏幕
  8. update升级包版本信息的读取
  9. 如何成为Android高手
  10. 【Android笔记】执行命令行语句