Android系统启动(一)— init 进程启动过程

Android 设备的启动必须经历三个阶段:Boot Loader、Linux Kernel 和 Android 系统服务。严格来说,Android 系统实际是运行在 Linux 内核之上的一系列“服务进程”,而这些服务进程的“老祖宗”就是 init 进程。

img

当按下启动电源时,系统启动后会加载引导程序,引导程序又启动 Linux 内核,在 Linux 内核加载完成后,第一件事情就是启动 init 进程。

Boot Loader 是在操作系统内核运行之前的一段小程序。通过这段小程序,可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好正确的环境。

init 进程是 Android 系统中用户空间的第一个进程,PID(进程号)为 1,是 Android 系统启动流程中一个关键的步骤,作为第一个进程,被赋予了很多极其重要的工作职责,比如创建 Zygote 进程和属性服务等。

它通过解析 init.rc 文件来构建出系统的初始运行状态,即Android 系统服务大多是在 init.rc 脚本文件中有描述并按照一定的条件启动。

进程间通信

init 进程启动做了很多工作,总的来说主要做了以下三件事:

  • 创建(mkdir)和挂载(mount)启动所需的文件目录;
  • 初始化和启动属性服务(property service);
  • 解析 init.rc 配置文件并启动 Zygote 进程;

init 进程是由多个源文件共同组成的,这些文件位于源码目录 system/core/init 中。

在 Linux 内核加载完成后,首先会在系统文件中寻找 init.rc 文件(/system/core/rootdir/init.rc),并启动 init进程。

Android 10 对 init 进程的入口函数进行了调整,不再是之前的 system/core/init/init.cpp 的 main 函数,而是换成了 system/core/init/main.cpp 的 main 函数,这样做的目的是把各个阶段的工作分离开,使得代码逻辑更加简明,代码如下所示:

// /system/core/init/main.cpp
// C++ 中主函数有两个参数,第一个参数 argc 表示参数个数,第二个参数是参数列表,也就是具体的参数
int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
    __asan_set_error_report_callback(AsanReportCallback);
#elif __has_feature(hwaddress_sanitizer)
    __hwasan_set_error_report_callback(AsanReportCallback);
#endif
    // Boost prio which will be restored later
    setpriority(PRIO_PROCESS, 0, -20);
  	/*
  	 * 1. strcmp 是 String 的一个函数,比较字符串,相等返回 0
  	 * 2. C++ 中 0 也可以表示 false
  	 * 3. basename 是 C 库中的一个函数,得到特定的路径中的最后一个 ‘/’ 后面的内容
  	 *    比如 /sdcard/miui_recovery/backup,得到的结果是 backup
  	 */
  	// 当 basename(argv[0]) 的内容为 ueventd 时 strcmp 的值为 0,!0 就是 1,执行 ueventd_main 函数
    if (!strcmp(basename(argv[0]), "ueventd")) { 
        return ueventd_main(argc, argv); // 主要负责设备节点的创建、权限设定等一系列工作
    }

    if (argc > 1) {
        if (!strcmp(argv[1], "subcontext")) {
            android::base::InitLogging(argv, &android::base::KernelLogger);
            const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();

            return SubcontextMain(argc, argv, &function_map);
        }

        if (!strcmp(argv[1], "selinux_setup")) {
            return SetupSelinux(argv);
        }

        if (!strcmp(argv[1], "second_stage")) {
            return SecondStageMain(argc, argv);
        }
    }

    return FirstStageMain(argc, argv); // 1
}

第一阶段:创建和挂载启动所需要的文件目录

注释 1 处的代码是 init 进程的第一阶段,在 /system/core/init/first_stage_init.cpp 文件中实现,代码如下所示:

// /system/core/init/first_stage_init.cpp
int FirstStageMain(int argc, char** argv) {
    if (REBOOT_BOOTLOADER_ON_PANIC) {
      	// 初始化重启系统的处理信号,内部通过 sigaction 注册新号,当监听到该信号时重启系统
        InstallRebootSignalHandlers(); 
    }
 
    boot_clock::time_point start_time = boot_clock::now(); // 用于记录启动时间

    umask(0); // Clear the umask. 清理 umask

    CHECKCALL(clearenv());
    CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
    
    /*---------- 创建和挂载启动所需的文件目录 ----------*/
  	// mount 是用来挂载文件系统的,mount 属于 Linux 系统调用
    CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755")); // 挂载 tmpfs 文件
    CHECKCALL(mkdir("/dev/pts", 0755)); // 创建目录,第一个参数是目录路径,第二个参数是读写权限
    CHECKCALL(mkdir("/dev/socket", 0755));
    CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL)); // 挂载 devpts 文件系统

#define MAKE_STR(x) __STRING(x)
  
    CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC))); // 挂载 proc 文件系统
#undef MAKE_STR
    
    CHECKCALL(chmod("/proc/cmdline", 0440)); // 8.0 新增,用于修改文件/目录的读写权限
    gid_t groups[] = {AID_READPROC}; // 8.0 新增,增加了个用户组
  	// 用来将 list 数组中所标明的组加入到目前进程的组设置中
    CHECKCALL(setgroups(arraysize(groups), groups));
    CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL)); // 挂载的 sysfs 文件系统
    CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL)); // 8.0 新增

    // mknod 用于创建 Linux 中的设备文件。这里提前创建了 kmsg 设备节点文件,用于输出 log 信息
    CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));

    if constexpr (WORLD_WRITABLE_KMSG) {
        CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
    }

    CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
    CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));

    CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
    CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));

    CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=1000"));

    CHECKCALL(mkdir("/mnt/vendor", 0755)); // 创建可供读写的 vendor 目录
    CHECKCALL(mkdir("/mnt/product", 0755)); // 创建可供读写的 product 目录

    CHECKCALL(mount("tmpfs", "/apex", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"));

    CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"));
#undef CHECKCALL
    SetStdioToDevNull(argv);
		// 初始化 Kernel Log 系统,这样就可以从外界获取 Kernel 日志
    InitKernelLogging(argv); 
    ...
}

由上面代码可知, init 进程的第一个阶段主要是用来创建和挂载启动所需的文件目录,其中挂载了 tmpfs、devpts、proc、sysfs 和 selinuxfs 共 5 种文件系统,这些都是系统运行时目录,也就是说,只有在系统运行时才会存在,系统停止时会消失。

四类文件系统:

  • tmpfs:一种虚拟内存文件系统,它会将所有的文件存储在虚拟内存中,如果将 tmpfs 文件系统卸载后,那么其下的所有内容将不复存在。 tmpfs 既可以使用 RAM,也可以使用交换分区,会根据实际需要改变大小。tmpfs 的速度非常惊人,毕竟它是驻留在 RAM 中的,即使用了交换分区,性能仍然非常卓越。由于 tmpfs 是驻留在 RAM 的,因此它的内容是不持久的。断电后,tmpfs 的内容就消失了,这也是被称作 tmpfs 的根本原因;
  • devpts:为伪终端提供了一个标准接口,它的标准挂接点是 /dev/pts。只要 pty 的主复合设备 /dev/ptmx 被打开,就会在 /dev/pts 下动态的创建一个新的 pty 设备文件;
  • proc:一个非常重要的虚拟文件系统,它可以看作是内核内部数据结构的接口,通过它我们可以获得系统信息,同时也能够在运行时修改特定的内核参数;
  • sysfs:与 proc 文件系统类似,也是一个不占有任何磁盘空间的虚拟文件系统。它通常被挂接在 /sys 目录下。sysfs 文件系统是 Linux 2.6 内核引入的,它把连接在系统上的设备和总线组织成一个分级的文件,使得它们可以在用户空间存取;

第二阶段:初始化并启动属性服务

// system/core/init/init.cpp
int SecondStageMain(int argc, char** argv) {
   
   ...
   PropertyInit(); // 1 对属性服务进行初始化
   ...
   Epoll epoll; // 创建 epoll 句柄
   if (auto result = epoll.Open(); !result.ok()) {
   
       PLOG(FATAL) << result.error();
   }
	 
   // 2 用于设置子进程信号处理函数,如果子进程(Zygote 进程)异常退出,init 进程会调用该函数中设定的信号处理函数来进行处理
   InstallSignalFdHandler(&epoll);
   InstallInitNotifier(&epoll);
   StartPropertyService(&property_fd); // 3 启动属性服务

   ...
}

在注释 1 处调用 PropertyInit() 函数来对属性进行初始化,并在注释 3 处调用 StartPropertyService 函数启动属性服务。

在注释 2 处调用 InstallSignalFdHandler 函数用于设置子进程信号处理函数,主要是用来防止 init 进程的子进程成为僵尸进程。为了防止僵尸进程的出现,系统会在子进程暂停或者终止的时候发出 SIGCHLD 信号,而 InstallSignalFdHandler 函数是用来接收 SIGCHLD 信号的(其内部只处理进程终止的 SIGCHLD 信号)。

假设 init 进程的子进程 Zygote 终止了,InstallSignalFdHandler 函数内部经过层层的函数调用和处理,最终会找到 Zygote 进程的信息,再重启 Zygote 服务的启动脚本。

僵尸进程

僵尸进程与危害:在 UNIX/Linux 中,父进程使用 fork 创建子进程,在子进程终止后,如果无法通知到父进程,这时虽然子进程已经退出了,但是在系统进程表中还为它保留了一定的信息(比如进程号、退出状态、运行时间等),这个进程就被称为是僵尸进程。系统进程表是一项有限资源,如果系统进程表被僵尸进程耗尽的话,系统就可能无法创建新的进程了。

孤儿进程

fork 函数调用之后,父子进程将交替执行,执行顺序不定。如果父进程先退出,子进程还没有退出,那么子进程的的父进程将变成 init 进程(托孤个了 init 进程,这是因为任何一个进程都必须是父进程)。

属性服务

Window 平台上有一个注册表管理器,注册表的内容采取键值对的形式来记录用户、软件的一些使用信息。即使系统或者软件重启,其还是能够根据之前注册表中的记录,进行相应的初始化工作。Android 也提供了类似的机制,叫做属性服务。

init 进程启动时会启动属性服务,并为其分配内存,用来存储这些属性,如果需要这些属性直接读取就可以了, 在 init.cpp 的 main 函数中与属性服务相关的代码有以下两行:

// /system/core/init/init.cpp
PropertyInit();
StartPropertyService(&property_fd);

这两行代码用来初始化属性服务配置并启动属性服务。

1 属性服务初始化与启动

PropertyInit() 函数的具体实现如下所示:

// /system/core/init/property_service.cpp
void PropertyInit() {
   
    selinux_callback cb;
    cb.func_audit = PropertyAuditCallback;
    selinux_set_callback(SELINUX_CB_AUDIT, cb);

    mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
    CreateSerializedPropertyInfo();
    if (__system_property_area_init()) {
    // 1
        LOG(FATAL) << "Failed to initialize property area";
    }
    if (!property_info_area.LoadDefaultPath()) {
   
        LOG(FATAL) << "Failed to load serialized property info file";
    }

    // If arguments are passed both on the command line and in DT,
    // properties set in DT always have priority over the command-line ones.
    ProcessKernelDt();
    ProcessKernelCmdline();
    ProcessBootconfig();

    // Propagate the kernel variables to internal variables
    // used by init as well as the current required properties.
    ExportKernelBootProps();

    PropertyLoadBootDefaults();
}

注释 1 处的 __system_property_area_init() 函数用来初始化属性内存区域。接下来看 StartPropertyService 函数的代码:

// /system/core/init/property_service.cpp
void StartPropertyService(int* epoll_socket) {
   
    InitPropertySet("ro.property_service.version", "2");

    int sockets[2];
    if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sockets) != 0) {
   
        PLOG(FATAL) << "Failed to socketpair() between property_service and init";
    }
    *epoll_socket = from_init_socket = sockets[0];
    init_socket = sockets[1];
    StartSendingMessages();

    if (auto result = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | 
                                   SOCK_NONBLOCK, false, 0666, 0, 0, {
   }); // 1
        result.ok()) {
   
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值