Android Init 进程启动流程研究(Android 10.0)

目录

Linux 内核启动

Android init 进程启动

FirstStageMain

SetupSelinux

SecondStageMain

总结与收获

感谢与参考


本篇主要内容是 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

作者:Gzw丶
链接:https://juejin.cn/post/6844903965688250382
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/lgglkk/article/details/128274636