Android Framework- Android 第一个用户进程:Init进程(概述)

概述

  • 在介绍 Init 之前,先了解一下Android系统的启动过程。从系统角度看,android 系统启动大概分为三个大阶段:bootloader引导、装载,和启动Linux内核,启动Android系统三个大的阶段。其中Android系统的启动还可以分为 启动 Init进程、启动 Zygote 进程、启动 SystemService 、启动 SystemServer 、启动 Home 等多个阶段。
    在这里插入图片描述

Bootloader 引导

  • 当我们按下电源键时,最先运行的就是 Bootloader 。Bootloader作用是初始化基本的硬件设备(如:CPU,内存等),并且通过建立内存空间映射,为加载 Linux 内核准备好合适的运行环境,一旦 Linux 内核装载完毕,Bootloader会从内存中清理掉。
  • 如果在 Bootloader 引导期间,按下预定的组合键,可进入系统的特定模式。

装载和启动 Linux 内核

  • Android 的 Boot.img 存放的就是 Linux 内核和一个根文件系统。BootLoader会把 Boot.img 映像装载进内存。然后 Linux 内核会执行整个系统的初始化,完成后装载根文件系统,最后启动 Init 进程。

启动 Init 进程(基于 Android9 源码)

  • Linux 内核加载完毕后,会首先启动 Init 进程,Init 进程是系统的第一个进程,在 Init启动过程中,会解析 Linux 配置脚本 init.rc 文件的内容,Init 进程会创建文件系统,创建系统目录,初始化属性系统,启动Android系统重要的守护进程,这些进程包括 USB 守护进程,adb 守护进程,vold守护进程,rild守护进程等。最后 Init 进程也作为守护进程来执行修改属性请求,重启崩溃的进程程序等。
Init 进程的初始化过程
  • Init 进程源码位于 system/core/init 下。程序的入口main() 函数在init.c中。
main 函数执行流程
  • 首先执行的是下面两句:init 进程中包含了另外两个守护进程的代码,启动时如果是另外两个的信号,则启动那两个守护进程。(生成信号链接是在 Android.mk 文件中)
int main(int argc, char** argv) {
    // 如果文件名是 ueventd 则执行 ueventd守护进程的主函数 ueventd_main
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }
    // 如果文件名是 watchdogd 看门狗进程,则执行watchdogd的主函数 watchdogd_main
    if (!strcmp(basename(argv[0]), "watchdogd")) {
        return watchdogd_main(argc, argv);
    }
}
  • 初始化第一个阶段
if (is_first_stage) {
        boot_clock::time_point start_time = boot_clock::now();

        // Clear the umask.
        umask(0);

umask(0); 函数可以设置属性的掩码,参数为0 意味着创建文件的属性为 0777 。

  • 接下来就是创建一些基本目录,包括 /dev ,/porc , /sys 等,同时把一些文件系统 mount到相应目录,部分代码如下,包括一些基于虚拟的文件系统等。部分代码如下:
		// 获取我们需要放在initramdisk的基本文件系统设置
		// 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);

        //...
        //...
        // 挂载vold管理的设备的暂存区域
        mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
              "mode=0755,uid=0,gid=1000");
       // /mnt/vendor”用于挂载无法挂载的厂商分区
		// 供应商分区的一部分,例如,因为它们被挂载为可读写。
        mkdir("/mnt/vendor", 0755);
        // 第一阶段完成了
        LOG(INFO) << "init first stage started!";
		// 如果是 Recovery 模式进入系统会走下面的方法
		 SetInitAvbVersionInRecovery();
		// 如果传递了全局启动选项,则启用seccomp(否则在zygote中启用它)。
        global_seccomp();
  • 接下来 main 函数第二个启动阶段,下面是部分代码
// At this point we're in the second stage of init.
// 让init进程可以使用 Kernel log系统,可以打印Log
InitKernelLogging(argv);
// 在 dev/文件夹下创建一个 booting 空文件代表正在初始化中。初始化结束之后这个文件将会被删除
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
// 调用 property_init(); 来初始化Android的属性系统
// 它的作用是创建一个共享区域来存储属性值。
property_init();
// 解析 kernel 启动参数 dt 永远在 cmd前面执行
process_kernel_dt();
process_kernel_cmdline();
// 解析设备根目录下的 default.prop 文件 把定义好的属性值读取进系统属性 
property_load_boot_defaults();
  • LoadBootScripts 读取启动脚本
// 读取启动脚本 
LoadBootScripts(am, sm);
// 下面是 将一些任务根据是否当前手机是否充电来加入不同的列表中
//...
//...

LoadBootScripts,其中主要探讨的是 parser.ParseConfig("/init.rc"); 解析完成后将 init 文件中的 service 和 action 分别加入到内部的列表中

static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
    Parser parser = CreateParser(action_manager, service_list);

    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    if (bootscript.empty()) {
        parser.ParseConfig("/init.rc");
        if (!parser.ParseConfig("/system/etc/init")) {
            late_import_paths.emplace_back("/system/etc/init");
        }
        if (!parser.ParseConfig("/product/etc/init")) {
            late_import_paths.emplace_back("/product/etc/init");
        }
        if (!parser.ParseConfig("/odm/etc/init")) {
            late_import_paths.emplace_back("/odm/etc/init");
        }
        if (!parser.ParseConfig("/vendor/etc/init")) {
            late_import_paths.emplace_back("/vendor/etc/init");
        }
    } else {
        parser.ParseConfig(bootscript);
    }
}
  • main 函数最后会进入一个无限 for 循环
    会执行一些检测,如果一些服务进程意外停止,则执行重启操作。这也是为什么我们在命令行停止一些进程时立刻就会发现有一个新的进程启动出来。

解析启动 init.rc

  • Init 进程启动最重要的就是解析 init.rc 。
init 文件格式介绍
  • init.rc 是以块(section)为单位组织的。一个块可以包含多行。块可以分为两大类,一类称为行为(action)一类称为服务(service)。“行为”以 on 开头表示一堆命令的集合,“服务”以 service 开头,表示启动某个进程的方式和参数。“块” 以 on 或者 service 开头,直到下一个 on 或者 service 结束。中间所有行都属于这个“块”。注释以 # 开头

  • 无论行为还是服务,都不是以文件的顺序执行的,他们只是存在这里的定义,至于执行与否和何时执行要看init进程在运行时决定的。

  • init 中启动了很多守护进程(官方建议把不必要立即启动的进程放入 Zygote 进程去启动,我发现9.0的 service 启动项好像比 5.0的要少)

// 负责相应 uevent 事件
service ueventd /sbin/ueventd
    class core
    critical
    seclabel u:r:ueventd:s0
    shutdown critical

// 包含常用的 shell 命令ls cd 等
service console /system/bin/sh
    class core
    console
    disabled
    user shell
    group shell log readproc
    seclabel u:r:shell:s0
    setenv HOSTNAME console

init 进程对信号的处理

  • 当一个进程调用 exit() 函数退出的时候,会像它的父进程发送 SIGCHID 信号,父进程收到信号后,将释放已经分配和子进程的资源。同时父进程必须执行系统调用 wait() 或 waitpid() 来等待子进程结束,如果父进程不这么做,而且父进程初始化也没调用 sign(SIGCHID)来显式忽略对SIGCHID信号的处理,那么子进程将会一直保持当前退出的状态,这样的进程被称为僵尸(Zombie)进程。 Android 查看僵尸进程是 adb命令下 通过 ps 进程状态为 Z 就是僵尸进程。

  • 僵尸进程不能被调度,仅仅在进程列表中占用一个位置,记载该进程退出状态等信息。除此之外僵尸进程不再占用任何内存空间,虽然还留有一些小尾巴,它的危害是什么呢?Linux 系统对每个用户运行进程数量是有限的,如果超过最大限制创建进程将会失败,这就是僵尸进程最大的危害。当然如果只有少量的进程,也不会有什么危害。

  • init 进程是如何处理 SIGCHID 信号的:在main函数中调用下面代码

// Create a signalling mechanism for SIGCHLD. 创建SIGCHLD的信令机制。
sigchld_handler_init();
  • 信号的初始化是通过 sigaction(SIGCHLD, &act, 0); 函数来完成的
void sigchld_handler_init() {
    // Create a signalling mechanism for SIGCHLD.
    int s[2];
    if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) == -1) {
        PLOG(FATAL) << "socketpair failed in sigchld_handler_init";
    }

    signal_write_fd = s[0];
    signal_read_fd = s[1];

    // Write to signal_write_fd if we catch SIGCHLD.
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = SIGCHLD_handler;
    act.sa_flags = SA_NOCLDSTOP;
    // 信号的初始化操作;&act 指定了信号的处理函数,还可以指定一些标志,如只有子进程终止了才接收信号
    sigaction(SIGCHLD, &act, 0);

    ReapAnyOutstandingChildren();

    register_epoll_handler(signal_read_fd, handle_signal);
}

  • 在 Linux 系统中,信号又称为软中断,信号的到来会中断进程正在处理的工作,因此在信号处理的函数中,不要去调用一些不可重入的函数。而且 Linux 不会对信号进程排列,不管在信号处理期间来了多少个信号,当前的信号处理完成以后,内核只会在发送一个信号给进程,因此为了不丢失信号,我们信号处理函数应该越快越好。
  • 但是对于 SIGCHLD 信号,父进程执行等待操作,这个时间比较长,解决这个问题的方式是:创建了一个对本地 socket 进程线程通信,这样当信号来时,只需要向 socket 写入数据就可以返回了,不会耽搁下一个信号处理。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值