Android 10.0 源码阅读 | Android Init 进程启动流程研究

2,270 阅读12分钟

阅读源码的文章会是一个系列,本篇主要内容是 Android 源码中启动流程的第一部分,包含了 Linux 内核启动部分与 Android init 进程启动部分。

Linux 内核启动

为什么我会先提 Linux 的启动呢?一方面 Linux 内核是 Android 平台的基础,另一方面我最近接触了一些 Linux 的基础知识,所以希望把这些学到的东西也都记录下来。

内核的作用其实就是控制计算机硬件资源并提供程序运行环境,具体的比如有:执行程序、文件操作、内存管理及设备驱动等,而内核对外提供的接口也被称为系统调用。

既然内核这么重要,提供了各种程序运行所需的服务,那启动 Android 前肯定是需要先把内核启动起来的。具体内核如何启动,我们先来看看当我们按下开机键后都发生了什么。

计算机通电后首先会去找 ROM(只读内存),这里面被固化了一些初始化程序,这个程序也叫 BIOS,具体几步就像下面这样:

读取 BIOS(基本输入输出系统,放在 ROM 中):

  • 硬件自检,也就是检查计算机硬件是否满足运行的基本条件;
  • 这个程序中查看启动顺序,当然这个可以自行调整,这时就按照启动顺序去找下一阶段的启动程序在哪里;

主引导记录(BIOS 中把控制权交给启动顺序的第一位):

  • 读取该设备的第一个扇区的前 512 字节,如果以特定字符结束,就说明这个设备可以用于启动,如果不是就按照刚才 BIOS 中的启动顺序将控制权交给下一个设备,这最前面 512 字节也叫主引导记录(MBR);
  • MBR 中的 512 字节放不下太多东西,所以它主要是告诉计算机去哪里找操作系统(硬盘上);
  • 这时通过 MBR 的分区表在硬盘上找到对应位置;

通过 boot loader 启动操作系统:

  • Linux 使用的是 Grub2,它是启动管理器,会将各种 img 加载进来;
  • 操作系统内核加载到内存中;
  • 之后会创建初始进程(0 / 1 / 2),后面会由一号进程来加载用户态中其他内容;

而如果你熟悉 Linux,你就会知道 Linux 启动的入口函数是 start_kernel(在 init/main.c 中),它里面都做了什么比较重要的事情呢:

  • 0 号进程创建(后面会演变成 idle 进程);
  • 系统调用初始化;
  • 内存管理系统初始化;
  • 调度系统初始化;
  • 其他初始化:
    • 1 号进程创建(用户态);
    • 2 号进程创建(内核态);

Android init 进程启动

上面提到 1 号进程,也叫 init 进程,而创建 1 号 init 进程时就会执行 Android 源码中 system/core/init 下面的 main.cpp 了,它里面会根据不同的参数调用不同的方法:

int main(int argc, char** argv) {
    // 略一部分
    // ueventd 主要用来创建设备节点
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }
    if (argc > 1) {
        // 略一部分
        // selinux_setup
        if (!strcmp(argv[1], "selinux_setup")) {
            return SetupSelinux(argv);
        }
        // second_stage 
        if (!strcmp(argv[1], "second_stage")) {
            return SecondStageMain(argc, argv);
        }
    }

    return FirstStageMain(argc, argv);
}

通过对 system/core/init/README.md 的阅读可以知道 main 函数的会执行多次,启动顺序是这样的 FirstStageMain -> SetupSelinux -> SecondStageMain。

所以下面分开来看一下,这三个部分都做了做了什么:

FirstStageMain

// 文件位置:system/core/init/first_stage_init.cpp
int FirstStageMain(int argc, char** argv) { 
    //  ...
    //  其实上面省略的基本是挂载文件系统、创建目录、创建文件等操作
    //  比如挂载的有:tmpfs、devpts、proc、sysfs、selinuxfs 等
    //  把标准输入、标准输出、标准错误重定向到 /dev/null
    SetStdioToDevNull(argv);
    //  初始化本阶段内核日志
    InitKernelLogging(argv);
    //  ...
    //  比如获取 “/” 的 stat(根目录的文件信息结构),还会判断是否强制正常启动,然后切换 root 目录
    //  这里做了几件事:初始化设备、创建逻辑分区、挂载分区
    DoFirstStageMount();
    //  ...
    //  再次启动 main 函数,只不过这次传入的参数是 selinux_setup
    const char* path = "/system/bin/init";
    const char* args[] = {path, "selinux_setup", nullptr};
    execv(path, const_cast<char**>(args));
}

第一阶段更多的是文件系统挂载、目录和文件的创建,为什么要挂载,这样就可以是使用它们了,这些都完成后就再次调用 main 函数,进入 SetupSelinux 阶段。

SetupSelinux

// 文件位置:system/core/init/selinux.cpp
int SetupSelinux(char** argv) {
    //  初始化本阶段内核日志
    InitKernelLogging(argv);
    //  初始化 SELinux,加载 SELinux 策略
    SelinuxSetupKernelLogging();
    SelinuxInitialize();
    //  再次调用 main 函数,并传入 second_stage 进入第二阶段
    //  并且这次启动就已经在 SELinux 上下文中运行
    const char* path = "/system/bin/init";
    const char* args[] = {path, "second_stage", nullptr};
    execv(path, const_cast<char**>(args));
}

这阶段主要做的就是初始化 SELinux,那什么是 SELinux 呢?其实就是安全增强型 Linux,这样就可以很好的对所有进程强制执行访问控制,从而让 Android 更好的保护和限制系统服务、控制对应用数据和系统日志的访问,降低恶意软件的影响。

不过 SELinux 并不是一次就初始化完成的,接下来就是再次调用 main 函数,进入最后的 SecondStageMain 阶段。

SecondStageMain

//  文件位置:system/core/init/init.cpp
//  不那么重要的地方就不贴代码了
int SecondStageMain(int argc, char** argv) {
    //  又调用了这两个方法
    SetStdioToDevNull(argv);
    //  初始化本阶段内核日志
    InitKernelLogging(argv);
    //  ...
    //  正在引导后台固件加载程序
    close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
    //  系统属性初始化
    property_init();
    //  系统属性设置相关,而且下面还有很多地方都在 property_set
    //  ...
    //  清理环境
    //  将 SELinux 设置为第二阶段 
    //  创建 Epoll
    Epoll epoll;
    //  注册信号处理
    InstallSignalFdHandler(&epoll);
    //  加载默认的系统属性
    property_load_boot_defaults(load_debug_prop);
    //  启动属性服务
    StartPropertyService(&epoll);
    //  重头戏,解析 init.rc 和其他 rc
    // am 和 sm 就是用来接收解析出来的数据
    //  里面基本上是要执行的 action 和要启动的 service
    LoadBootScripts(am, sm);
    //  往 am 里面添加待执行的 Action 和 Trigger
    while (true) {
        //  执行 Action
        am.ExecuteOneCommand();
        //  还有就是重启死掉的子进程
        auto next_process_action_time = HandleProcessActions();
    }
}

这是整个启动阶段最重要的部分,我觉得有四个比较重要的点,它们分别是属性服务、注册信号处理 、init.rc 解析以及方法尾部的死循环。

属性服务

什么是属性服务,我觉得它更像关于这台手机的各种系统信息,通过 key / value 的形式供我们所有程序使用,下面内容就是我的模拟器进入 adb shell 后获取到的属性值,下面我从输出结果里面保留的一部分:

generic_x86:/ $ getprop
...
[dalvik.vm.heapsize]: [512m]
...
[dalvik.vm.usejit]: [true]
[dalvik.vm.usejitprofiles]: [true]
...
[init.svc.adbd]: [running]
...
[init.svc.gpu]: [running]
...
[init.svc.surfaceflinger]: [running]
...
[init.svc.zygote]: [running]
...
[ro.product.brand]: [google]
[ro.product.cpu.abi]: [x86]
...
[ro.serialno]: [EMULATOR29X2X1X0]
[ro.setupwizard.mode]: [DISABLED]
[ro.system.build.date]: [Sat Sep 21 05:19:49 UTC 2019]
...
//  zygote 启动该启动哪个
[ro.zygote]: [zygote32]
[ro.zygote.disable_gl_preload]: [1]
[security.perf_harden]: [1]
[selinux.restorecon_recursive]: [/data/misc_ce/0]
...
[wifi.interface]: [wlan0]

属性服务相关代码在 SecondStageMain 阶段其实主要做了三件事:创建共享内存、加载各种属性值以及创建属性服务的 Socket。下面是这关于这几部分的片段:

property_init {
    //  创建目录 /dev/__properties__
    //  会从别的地方加载并解析属性,然后写到 /dev/__properties__/property_info 里
    //  在 __system_property_area_init 的调用链跟踪中,发现最终是通过 mmap 创建共享内存
}

property_load_boot_defaults {
    //  代码中很多这样的代码
    load_properties_from_file("/system/build.prop", nullptr, &properties);
    load_properties_from_file("/vendor/default.prop", nullptr, &properties);
    load_properties_from_file("/vendor/build.prop", nullptr, &properties);
    load_properties_from_file("/product/build.prop", nullptr, &properties);
    load_properties_from_file("/product_services/build.prop", nullptr, &properties);
    load_properties_from_file("/factory/factory.prop", "ro.*", &properties);
    //  会调用 PropertySet 设置这些属性值
}

StartPropertyService {
    //  创建 Sockte
    //  这个 Socket 就是用来处理系统属性的,所有进程都通过它来修改共享内存里面的系统属性
    property_set_fd = CreateSocket(...);
    //  开始注册监听,handle_property_set_fd 是回调处理函数
    epoll->RegisterHandler(property_set_fd, handle_property_set_fd);
}

代码上了理解起来并不那么难,只是可能要问为什么要使用共享内存?Socket 作用是什么?

首先共享内存是一种高效的进程间通信方式,本身这些属性值在内存中存在一份即可,不需要每个进程都复制一份到自己的空间中,而且由于是共享的,所以谁都能访问。但是如果谁都能随时来读写(除了只读部分的属性),那也还是会出问题,可能会出现内容不一致问题,所以大家并不是直接对共享内存进行操作,而是通过属性服务的 socket 的对其进行操作,这样就避免了所以进程直接对那块共享内存进行操作。

注册信号处理

在 SecondStageMain 阶段,其实就是注册了信号处理函数,从而可以对底层信号作出响应。对应函数是:

InstallSignalFdHandler {
    //  ...
    //  注册信号处理函数
    epoll->RegisterHandler(signal_fd, HandleSignalFd);
}

HandleSignalFd {
    //  ...
    //  ReapAnyOutstandingChildren 会对死掉的进程进行重启
    SIGCHLD -> ReapAnyOutstandingChildren
    SIGTERM -> HandleSigtermSignal
    default -> 打印日志
}

//  子进程异常退出后要标记需要重新启动
ReapAnyOutstandingChildren {
    //  ...
    ReapOneProcess {
         // ...
        service.Reap {
            //  ...
            //  设置要重启的标志位,但这里并不是真的启动
            flags_ &= (~SVC_RESTART);
            flags_ |= SVC_RESTARTING;
            onrestart_.ExecuteAllCommands();
        }
    }
}

init.rc 解析

init.rc 是什么?它是非常重要的配置文件,而且众多 rc 文件中 init.rc 是最主要的文件,不过这里我不会讲 rc 文件的语法是怎么样的,因为 system/core/init/README.md 中已经写的很清楚了,init.rc 会根据 on 分成不同阶段,并且由 trigger 进行不同阶段的触发,而每个阶段里面就是一条条要执行指令,比如 start 后面跟的就是要启动的服务,mkdir 就是创建目录。

既然分成了多个阶段,那先来看看触发阶段是怎么样的:

//  这三个阶段是顺序下去的,这三个阶段的触发顺序是写在 SecondStageMain 代码中的
early-init -> init -> late-init

//  late-init 中再去触发别的阶段
on late-init
    trigger early-fs
    trigger fs
    trigger post-fs
    trigger late-fs
    trigger post-fs-data
    trigger load_persist_props_action
    //  这里就是 zygote-start 启动了
    trigger zygote-start
    trigger firmware_mounts_complete
    trigger early-boot
    trigger boot

那么下面来看看 init.rc 解析在 SecondStageMain 阶段都做了啥:

//  把这阶段关于 rc 文件相关的一些重要代码提取出来
int SecondStageMain(int argc, char** argv) {
    //  ...
    //  两个用于存储的容器
    ActionManager& am = ActionManager::GetInstance();
    ServiceList& sm = ServiceList::GetInstance();
    //  解析 init.rc
    LoadBootScripts(am, sm);
    //  ...
    //  加入触发 early-init 语句
    am.QueueEventTrigger("early-init");
    //  ...
    //  加入触发 init 语句
    am.QueueEventTrigger("init");
    //  ...
    //  代码中还有很多 QueueBuiltinAction,插入要执行的 Action
    am.QueueBuiltinAction(InitBinder, "InitBinder");
    //  ...
    //  加入触发 late-init 语句
      am.QueueEventTrigger("late-init");
}

LoadBootScripts(action_manager, service_list) {
    Parser parser = CreateParser(action_manager, service_list);
    //  系统属性中去找 ro.boot.init_rc 对应的值
    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    //  没找到的话就去当前目录找 init.rc 
    //  当前目录就是 system/core/init/
    if (bootscript.empty()) {
        //  无论没有找到最终解析的任务都是交给 ParseConfig 这个方法去处理 
        parser.ParseConfig("/init.rc");
        //  ... 
    } else {
        parser.ParseConfig(bootscript);
    }
}

其实上面的代码写主要做的就是解析 init.rc 文件中的内容,并且在加入要执行的动作。

方法尾部的死循环

这里面主要做的就是执行刚入 ActionManager 中的动作和看看是否有需要重启的进程。

while (true) {
    //  ...
    //  执行刚才加入 ActionManager 的动作
    am.ExecuteOneCommand();
    //  ... 
    //  HandleProcessActions 才是真正重启进程的地方
    auto next_process_action_time = HandleProcessActions();
}

HandleProcessActions {
    //  ...
    //  对需要重启的进行重启,前面会有很多判断
    auto result = s->Start();
}
    

到这里大致的 init 进程启动的三个阶段基本上清晰了。

不过由于是我第一次开始阅读 AOSP 源码,本篇文章讨论的内容比较有限,其中还有很多细节的东西并没有讨论到,比如:

  • Linux 启动流程的更多详细内容;
  • 具体挂载的那些文件是什么,它们都有什么用途;
  • 属性服务的完整读写流程是怎么样的;
  • 具体 init.rc 如何解析,如何执行;
  • zygote 的启动等等;

不过后续部分,比如 zygote 我会尽量在下次读完之后分享出来的。

总结与收获

如果你问我我读完这些有什么收获,我觉得下面这三点是我的主要收获:

  • 在某些情况下(比如前期资源不足或者前后依赖),我们可以将大任务拆解,并合理分配好执行次序(包括顺序、串并行安排等等),进而通过多阶段任务的配合从而完成一个整体的执行目标;
  • 当资源是共享的时候,最好不要不然所有人都直接对资源进行操作,而是引入中间人,大家只和中间人交互,具体资源由中间人和其交互;
  • 代码跑起来很重要,但是一个合理的监控模块也非常需要,这样可以在必要的时候检测出问题并及时作出响应;

感谢与参考

以上内容,除了源码本身外,还参考了以下链接(顺序不分先后):

计算机是如何启动的?

Linux 的启动流程

07 | 从BIOS到bootloader:创业伊始,有活儿老板自己上

08 | 内核初始化:生意做大了就得成立公司

Android启动流程简析(一)

Linux 引导过程内幕

Linux下0号进程的前世(init_task进程)今生(idle进程)----Linux进程的管理与调度(五)

Android 中的安全增强型 Linux

Android系统启动-Init篇

深入研究源码:Android10.0系统启动流程(二)init进程

Android P (9.0) 之Init进程源码分析

Android系统启动流程之init进程启动

Android启动流程——1 2 3

最后的最后

源码本身没有歧义,不过由于每个人基础不同,具体理解起来可能会些不同,所以有什么问题,也请大家多指点,多交流。

如果你觉得我写的还不错的话,那就通过点赞,点赞,还 tm 是点赞的方式给我反馈吧,感谢你的支持。