Android 8.1 开机流程分析(1)

1. 启动过程概述

image
图: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.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 := $(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 -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 通过两种方式创建设备节点文件

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

第二种方式对应 "热插拔"(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

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

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

watchdogd_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<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.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<fstab_rec*>)
    // 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 安全策略

猜你喜欢

转载自blog.csdn.net/qq_19923217/article/details/81240302
今日推荐