我是init进程

2023年 10月 10日 95.1k 0

为何要写系列文章

自己从事Android开发已经有很多年了,从App开发到Android framework层甚至再底层 自己还算有一些经验。时常想着能通过写文章的方式把自己的经验与大家分享下。

于是乎我就在考虑技术类文章应该怎么写?或者说什么样的技术类文章会让读者的阅读体验更好?
首先技术类文章需要是系列性的成体系的;其次是文章尽量不要完全以源码分析为主,不要流水线的方式分析方法调用链,为啥这样说呢?源码或者方法调用链只是一个思想或者一个解题思路的具体实现,只知其表不知其里。如果能用简单的语言把解题思路、原理讲解清楚,能把复杂的问题简单话。读者能从中学习到为啥要这样做?这样做的好处是啥?即时使用另外一种语言也可以实现相应的功能(语言只是一个工具而已),或者把相应的思路应用于工作中也是可以的。 我这里提到的文章尽量不要完全以源码分析为主,不是说文章里面不要出现源码,比如某个开源库的源码设计的非常的漂亮,我觉得非常有必要把设计绝妙地方展示出来与大家一起讨论分享;最后文章读起来不要太乏味,我希望文章是易懂的有趣的,有读下去的欲望的。

计划写哪些类型的文章?
我会先从Android framework层开始写起,大概会涉及到init进程系列servicemanager系列zygote系列systemserver进程系列ActivityManagerServiceWindowManagerServiceInputManagerServicePackageManagerServicesurfaceflinger进程系列四大组件系列binder系列handler系列窗口系列ui绘制系列事件分发系列类加载机制系列等。我希望能把它们关联起来,而不是一个孤岛。比如Android系统启动流程会把init进程系列servicemanager系列zygote系列systemserver进程系列它们关联起来,它们在Android启动中分别扮演着非常重要的作用。

其次是App开发系列,大概会涉及到架构mvc、mvp、mvvm性能优化系列组建化插件化热更新gradle插件AOP跨平台等。

本文概要

本文以自述的方式来介绍init进程,文中“我”指的是init进程,并且还穿插了对话。以这种方式来讲解技术主要的目的是希望大家能以一种轻松、简单、不枯燥的方式来了解init进程。通过本文希望您可以了解init进程是啥?它的作用有哪些?它在Android中的重要性。

我的父亲

各位乡亲父老,大家好!今天来到了我的主场,我来隆重的介绍下我自己:“我本名叫init进程,大家可以叫我init。一般自我介绍都把自己耀眼的、能亮瞎眼睛的亮点展示出来,那我也落一回俗套。我把自己比作Android中的‘女娲’,我就像女娲造人一样,我创造了Android里面的各种进程,比如大家熟知的:zygote进程、surfaceflinger进程等等。毫不夸张的说没有我,整个Android系统就立马歇菜,Android里面的进程都是我的子子孙孙,我就是它们的鼻祖,我敢说我是世界上子孙最多最多的并且没有之一的‘人’,没人敢不同意吧。“

这时候突然一个声音出现了:”init你也敢说你是Android所有进程的鼻祖,好意思吗?你也不想想是谁创造了你,难道你是石头缝蹦出来的吗?大家好,我是swapper进程(我还有另外一个名字idle进程),大家可以叫我swapper这样更亲切。我的进程id是0,init它的进程id是1,聪明的大家从进程id已经能分辨出谁是真正的鼻祖了吧。init进程你要摆正你的位置,你是Android用户空间所有进程的鼻祖,而我才是Android所有进程的鼻祖

init这时候小声的说:”好吧,我承认你是我的‘父亲’,我是用户空间所有进程的鼻祖,这里的用户空间是啥玩意儿?“

“好问题,Linux操作系统中除了用户空间还有另外一个空间叫内核空间。Linux操作系统毕竟也是软件,只不过它是特殊的软件,软件是需要与硬件打交道的,为了各种硬件资源的安全性,操作系统就虚拟化出了用户空间内核空间这样的概念,用户空间里面有很多的进程,比如各种app进程、systemserver进程等,用户空间进程可访问的资源是有限的,无法直接访问底层硬件和内核代码,如需要访问必须进行系统调用进入内核空间,让内核帮忙。内核空间里面同样也有很多的进程,这些进程可以直接访问各种资源没有任何限制。“

”还是不明白,能再讲详细点吗?“

”那我就打个比方吧,你去银行办理业务,银行柜台工作人员所在的房间你是不是根本进不去,你要办理业务,那只能通过银行工作人员帮你办理。那银行工作人员所在的房间就可以类比为内核空间,工作人员可以理解为是各种内核空间里的进程,工作人员操控的电脑、公章、钱等等可以类比为内核空间进程操控的cpu、内存、键盘等硬件资源。来办理业务的人所在的空间类比为用户空间,办理业务的人可以理解为用户空间的各种进程。划分出内核空间用户空间,就是为了保证系统安全性和对底层硬件的访问控制,这样用户空间进程想访问这些关键资源必须需要内核空间这个‘大管家’层层筛查才可以。这样类比应该明白了吧。“

“明白了,那能赶紧讲讲我是怎么被创造出来的吗?太想知道自己的身世了。”

swapper挺了挺胸脯,自信的说:“那就先讲下我是怎么来的吧,毕竟先有我才有你。Android系统是基于Linux内核的,内核启动后我就被创建出来了,我的进程id是0,并且我是Android所有进程中唯一一个没有使用fork/clone方法被创建的进程,当我的各项工作完毕后,我会创建init进程(进程id是1)和kthreadd进程(进程id是2)。上面提到过init进程是Android用户空间所有进程的鼻祖,那kthreadd进程是Android内核空间所有进程的鼻祖。你们’兄弟俩‘是我唯一直接创建的两个进程,大伙儿可以看下面的代码。“

goldfish/init/main.c (下面方法来自这个文件,这是模拟器对应的kernel文件)

noinline void __ref rest_init(void)
{
	struct task_struct *tsk;
	int pid;

	rcu_scheduler_starting();
	
    //kernel_thread方法在内核创建一个进程,创建完毕会返回值为1的pid,并且会执行 kernel_init 方法
    pid = kernel_thread(kernel_init, NULL, CLONE_FS); //niu kernel 开始创建init进程
	
    省略代码......

    //同样调用kernel_thread方法在内核创建一个进程,pid为2,并且会执行 kthreadd 方法
	pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
	
    省略代码......
}

init说:”看了上面代码,发现我和kthreadd都是通过kernel_thread方法创建的,那我是怎么变为用户空间进程的。“

”这问题问的好,kernel_thread方法是内核创建进程的方法。你演变为用户空间进程的关键是do_execve这个函数在起作用,先看下关键代码。“

goldfish/init/main.c (下面方法来自这个文件,这是模拟器对应的kernel文件)

//init进程创建后 会调用这个函数
static int __ref kernel_init(void *unused)
{
	省略代码......

    //依次执行下面目录的init程序(init程序就是个so库),哪个执行成功就退出
    [1.2]
	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;

	省略代码......
}


[1.2]
static int try_to_run_init_process(const char *init_filename)
{
	
    省略代码......
    [1.3]
	ret = run_init_process(init_filename);

	省略代码......
	return ret;
}

[1.3]
static int run_init_process(const char *init_filename)
{
	省略代码......
    
    //执行这个函数后,init进程进入用户空间运行
	return do_execve(getname_kernel(init_filename),
		(const char __user *const __user *)argv_init,
		(const char __user *const __user *)envp_init);
}

上面代码最终会在用户空间执行init.so的main函数,从而开始init进程的初始化过程。

do_execve:是 Linux 内核中的一个函数,它是 execve 系统调用的实际执行函数。execve 系统调用用于在用户空间中执行一个新的程序并替换当前进程的代码和数据。当用户空间进程调用 execve 系统调用时,内核会执行 do_execve 函数来完成程序加载和进程替换的过程。

swapper拍了拍init的肩膀,严肃的说:“init你和你的弟弟kthreadd已经‘长大成人了’,虽然你们不敢相信自己如此神速的长大,但是这是事实。用户空间的管理权我就放心的交给你了,同样内核空间的管理权就交给kthreadd进程了,你们要齐心协力保证Android的正常运行。”

我和我的子孙

谢谢我的’父亲‘swapper讲清楚我的’身世‘,我既然是Android用户空间所有进程的鼻祖,那我就带我的子孙们给大家亮个相,让大家对我们有一个初步的印象。先看下我们全家的一个‘大合照’(大合照只是我们家族的一部分而已)

从大合照,大家一眼就能看出我在家族中的地位吧,在我们的家族中有一个规则:在大合照中凡是层级越高的,它的“寿命”越长。因为我是处于最顶层的,因此我的“寿命”是最长的,我生的最早死的最晚你说气人不。

还有另外一个规则:如果父进程非正常死掉,它的所有子进程也都得死掉。若我死掉了,在死掉的时候会杀掉所有的子孙进程;若zygote子进程死掉了,会杀掉systemserver进程和所有的App进程,当zygote子进程重新启动后,会启动systemserver进程和桌面等进程。

一个公司能正常运转离不开每个员工的付出,也离不开员工之间以及部门之间的完美协作。每个进程就如公司的员工,每个进程做着自己最专业的事情,进程与进程之间的协作保证一个模块或者体系的运行,模块与模块或者体系与体系之间的相互协作保证了Android系统的正常运行。比一个图片显示在屏幕上需要App进程、systemserver进程、surfaceflinger进程之间的完美协作才能完成。那我就把几个常见的子进程介绍给大家(没办法孩子多了就这个烦恼,很多孩子都没咋见过面)

surfaceflinger进程

大家好,我是surfaceflinger进程,估计熟悉我的朋友应该比较少,那我先低调的介绍下自己(毕竟我一直都很低调),我的主要作用是:把多个来源的图像数据,如需合成则进行合成 否则直接提交给display驱动进行显示。看了这个介绍有朋友应该还是对我比较陌生,不要急在后面的章节我会详细的把自己展示给大家。

lmkd进程

大家好,我是lmkd进程,大家应该看到‘lmkd’这个单词不知道啥意思吧,它其实不是一个单词,它是’Low Memory Killer Daemon‘的缩写,中文意为 "低内存杀手守护进程"。看到杀手这个词会不会感到胆怯,放心我不会伤害到大家,我是用来查杀进程的。我会监控内存的使用情况,当达到一定阀值的时候,我会去杀掉一些处于后台的、优先级低的、占用内存高的进程,别看这些进程或多或少是我的’远方亲戚‘,那我也不能手软。所以给大家一个忠告:咱们自己的App要保证在收到低内存警告的时候,把不用的资源一定要清理掉,否则别怪我到时候不客气哦。我是一个守护进程,我会在后台默默的为内存的健康保驾护航。

logd进程

大家好,我是log进程,大家经常使用的log功能就是我来实现的。

servicemanager进程

大家好,我是servicemanager进程,我是为binder通信服务的,可以说没有我整个Android系统的binder通信就完全瘫痪了,所以我需要提前启动,同样在后面会详细的介绍我。

zygote进程

大家好,我是zygote进程,我可以很骄傲的说:“我是Android所有可运行java代码进程的鼻祖,我也是init进程直接创建的子进程中唯一可以运行java代码的进程,我的子孙也是很多的,如systemserver进程及各种app进程等。同样在后面会详细的介绍我。”

上面的这些子进程我称它们为"后台进程“,它们都在后台默默的工作。不像各种App进程可以在舞台中间把自己最靓丽的一面展示给大家。介绍了这么多我的子进程,那我有必要介绍给大家我是如何创建它们,以及它们的创建时机也是不一样的,我相信大家肯定有兴趣听一听。创建子进程是我最重要的工作之一。

我的子进程是如何创建的

我不像我的’父亲‘swapper只有两个‘孩子’,所以它能主动的创建我和我的兄弟。而我呢就没办法采用主动的方式去创建我的子进程,主要原因是:首先我的子进程很多;其次每个子进程创建的时机或条件都不一样,有的希望在启动前期创建,有的希望在启动后创建,有的希望在某个条件满足的时候创建。一句话就是创建子进程的条件太复杂了(不像我的‘父亲’在某个固定的点创建我们兄弟俩就完事了)。因此我没办法去关心我应该创建哪些子进程(如果关心的话会把我累死),那我就采用了另外一个方法:我不关心需要创建哪些子进程,哪个子进程想要被创建,那就把相关的信息配置好,在我启动的过程中我会收集这些配置信息,进而根据配置信息来创建,我给这个方法起了一个名字被动创建

选用脚本语言来配置信息

被动创建子进程的思路是不是很清晰,但是有个问题:配置信息如何配置呢?我当时想到两种方案:一种是通过代码来配置;另外一种是通过脚本语言来配置。我最终选了第二种方案,那我就来给大家讲讲为啥选了第二种方案。

通过代码来配置

这方案的实现思路大致是这样的:用c/c++来定义一套数据结构/实体类(数据结构/实体类是用来存放配置信息的),有需求的模块把自己的配置信息配置好后,需要调用某个方法或者某个类的方法把这些配置信息保存到内存中。
下面是伪代码:

//下面的文件在init进程
//A子进程的配置信息
A进程在创建前需要提前执行的一些动作
A进程的名字
A进程对应的二进制可执行文件路径

//B子进程的配置信息
B进程在创建前需要提前执行的一些动作
B进程的名字
B进程对应的二进制可执行文件路径

......

其他子进程的配置信息

这方案可以解决问题,但是如果站在使用者的角度就会发现存在问题:首先由于配置的子进程信息特别多,那承载配置信息的文件会越来越膨胀;其次若有一丁点的改动都需要重新编译init的代码,效率是如此的低下;再其次还有跨平台的问题。

一个解决方案好坏的评判标准可不是简单的以解决了问题为主,还要站在使用者的角度进行考虑,使用者成本越低越好,不是给使用者增加麻烦而是减少麻烦。于是乎就有了第二个方案。

通过脚本语言来配置

这方案的实现思路是:定义一套脚本语言,或者更通俗点定义一套语法规则(肯定比c/c++的语法规则要简单很多),利用这套语法规则来进行配置。

这方案完全解决了第一个方案的问题,并且还带来了好处:首先脚本语言通常比传统的编译语言更加灵活和容易上手;其次脚本文件不需要编译,完全提升了开发效率;再其次脚本文件通常具有较为清晰和易读的语法和结构,便于进行维护和更新;脚本语言是跨平台的。

init自言自语的说道:“脚本语言的实现方案带来了这么多好处,我尽然能想到,我真是个天才啊!”

定义脚本语言

首先需要给这个脚本语言起一个响亮的名字,就叫Android Init Language吧,接着需要制定脚本文件的后缀,javascript也是脚本语言,它的脚本文件是以'.js'为后缀的,那咱们的脚本文件就以‘.rc'作为后缀。既然名字和后缀制定好了,那就来制定语法规则吧。

制定语法规则需要依据于咱们要解决啥问题,问题已经非常明确了:被创建子进程信息如何配置?可以把这个问题细化为下面4个步骤来解决此问题

  • 配置子进程基础信息:这一步主要用来配置子进程的基础信息,比如子进程的名字、可执行文件路径等,init进程就可以立马明白是哪个子进程被创建
  • 配置触发条件:主要配置子进程何时或者满足什么条件的情况下被创建,因为不同子进程的创建条件都是不一样的,因此init进程可以从这一步得知是在“什么时候”或者“什么条件满足”的时候来创建子进程
  • 配置前置命令:主要配置子进程在创建之前需要执行一些提前操作或者提前执行的命令,比如有的子进程在创建之前需要提前创建一些目录等操作
  • 配置创建子进程命令:这一步非常的简单,init进程遇到这个命令,就开始执行创建子进程的操作
  • 那就按上面的4个步骤依次来制定语法规则

    配置子进程基础信息

    使用service关键字作为开头来配置子进程的基础信息,它的语法格式如下:

    # name:子进程的名字
    # pathname:可执行文件路径
    # argument:可执行文件的main方法被执行的时候,参数会传递到main方法
    # option:其他的一些配置信息,比如子进程是否可重启等
    service   [  ]*
       
       
       ...
    

    配置触发条件

    使用on关键字来标识触发条件,它的语法如下:

    # 这个语法就非常的简单明了,在什么条件下做哪些事情
    on 触发条件
    
    # 下面是一些简单的例子
    # 在init启动时候做哪些事情
    on init
    
    # 在init启动前期做哪些事情
    on early-init
    
    # 在init启动后期做哪些事情
    on late-init
    
    

    触发条件的配置是不是非常的简单,也可以把触发条件称为action,大意为在满足条件的时候开始执行动作。

    配置前置命令

    前置命令的配置首先要有一个非常关键的前提条件,那就是必须基于触发条件来配置,如下:

    # 在触发条件达成的时候,分别执行command1 command2等命令,命令如:mkdir、mount、chmod、chown、trigger等
    on 触发条件
       command1
       command2
       ......
    
    # 例子
    on post-fs
        # 调用exec执行对应操作
        exec - system system -- /system/bin/vdc checkpoint markBootAttempt
        
        # mount操作
        mount rootfs rootfs / remount bind ro nodev
    
        # 创建目录操作
        mkdir /cache/recovery 0770 system cache
    
    

    其实前置命令这些配置信息是完全可以作为子进程基础信息来配置的,但为啥没这样做呢?主要的原因是:如果这样做了,假如多个子进程配置了相同的前置命令,那这些相同的前置命令分散于各处,带来了不好维护的问题、没有复用的问题。

    配置创建子进程命令

    创建子进程的命令使用start关键字,与前置命令的配置一样,创建子进程的命令也是基于触发条件的,如下:

    # servicename:是第一步配置的子进程的名字
    on 触发条件
       start servicename
    

    模块化

    init:“我突然意思到一个严重的问题:被创建的子进程是很多的,这些配置信息都写在一个脚本文件里面,那这脚本文件会越来越膨胀的,最后导致维护性难度加大,不能复用等问题。这是亟需解决的问题”
    init沉思了一会儿,突然说我想到了一个办法:”咱们可以参考java语言,java语言在引入另外一个类的时候会使用import这个关键字,那就用这个关键字来引入别的脚本文件,具体语法格式如下。“

    import xxx/xxx/xx.rc
    

    小结

    被创建子进程的脚本配置信息如下:

    # 在xxx/xx.rc 脚本文件中配置子进程的基础信息
    service servicename  [  ]*
       
       
       ...
    
    
    # 在xxx/init.rc 脚本文件中引入上面的脚本文件,并配置对应的触发条件和前置命令
    import xxx/xx.rc
    
    on 触发条件
       command1
       command2
       ...
       start servicename
    
    

    上面的脚本配置信息所要表达的意思是:在某个触发条件达成的时候,需要先执行command1、command2命令,最后开始创建servicename子进程,聪明的大家肯定立马就能明白。这就是脚本语言的魅力,我也真是太有才了。”

    例子

    好了,这套脚本语言就定义好了,那我就趁热打铁给大家看下servicemanager的配置信息,带大家亲身的感受下。

    servicemanager子进程基础信息配置

    文件路径:frameworks/native/cmds/servicemanager/servicemanager.rc
    
    # 下面配置了servicemanager子进程
    # servicemanager是子进程的名字
    # /system/bin/servicemanager 是可执行文件的路径
    # onrestart critical等是其他的配置项
    service servicemanager /system/bin/servicemanager
        class core animation
        user system
        group system readproc
        critical
        onrestart restart apexd
        onrestart restart audioserver
        onrestart restart gatekeeperd
        onrestart class_restart main
        onrestart class_restart hal
        onrestart class_restart early_hal
        writepid /dev/cpuset/system-background/tasks
        shutdown critical
    

    init.rc文件中配置创建servicemanager子进程

    文件路径:system/core/rootdir/init.rc
    
    # 在init阶段触发 copy、symlink等这些命令后,开始创建servicemanager子进程
    on init
        # Mix device-specific information into the entropy pool
        copy /proc/cmdline /dev/urandom
        copy /system/etc/prop.default /dev/urandom
    
        symlink /proc/self/fd/0 /dev/stdin
        symlink /proc/self/fd/1 /dev/stdout
        symlink /proc/self/fd/2 /dev/stderr
        省略代码......
    
        # 下面命令代表创建servicemanager子进程
        start servicemanager
    

    脚本语言的升华

    脚本语言不单单为配置创建子进程服务,它还可以升华出更多的功能

    触发条件分类

    触发条件主要分为三类:event事件类型、property属性类型、event和property的组合类型
    event事件类型:比如上面介绍的init,early-init,late-init,late-fs等,此类触发条件就是一个字符串,用户完全可以自定义
    property属性类型:property属性会在后面介绍,它的格式是这样的:property:xxx.xxx=value,它的达成条件是某个xxx.xxx的属性与value相等的时候开始做一些事情

    property属性类型触发条件例子:

    # perf.drop_caches 为3的时候开始执行下面的命令
    on property:perf.drop_caches=3
        write /proc/sys/vm/drop_caches 3
        setprop perf.drop_caches 0
    

    event事件类型和property属性类型组合触发条件例子

    # 在init和property:ro.debuggable等于1的条件下,创建console子进程
    on init && property:ro.debuggable=1
        start console
    

    触发条件触发别的条件执行

    一个触发条件达成的时候,在执行它的前置命令的时候,有些前置命令是可以触发别的多个触发条件执行的,根据触发条件的分类依次来进行介绍。

    event事件类型的触发条件触发别的条件执行的时候需要使用trigger关键字,格式:trigger 触发条件。如下例子

    # 在userspace-reboot-resume条件达成的时候,执行下面的各种trigger命令,trigger命令主要作用是触发一个event类型的触发条件达成
    on userspace-reboot-resume
      trigger userspace-reboot-fs-remount
      trigger post-fs-data
      trigger zygote-start
      trigger early-boot
      trigger boot
    
    
    # boot条件达成的时候,开始执行下面的各种命令
    on boot
        # basic network init
        ifup lo
        hostname localhost
        domainname localdomain
    
        # IPsec SA default expiration length
        write /proc/sys/net/core/xfrm_acq_expires 3600
    
        # Memory management.  Basic kernel parameters, and allow the high
        # level system server to be able to adjust the kernel OOM driver
        # parameters to match how it is managing things.
        write /proc/sys/vm/overcommit_memory 1
        write /proc/sys/vm/min_free_order_shift 4
    
        # System server manages zram writeback
        chown root system /sys/block/zram0/idle
        省略其他的配置......
    

    property属性类型的触发条件触发别的条件执行需要使用setprop关键字,格式:setprop xxx.xx value。如下例子:

    # init触发条件达成的时候,执行setprop命令
    on init
       # 会触发 property:ro.debuggable=1 的命令执行
       setprop ro.debuggable 1
    
    # 属性类型的触发条件,property:ro.debuggable=1的时候开始执行它的命令
    on property:ro.debuggable=1
       mkdir xxxx
       ......
    

    触发条件下面可以配置各种各样的命令

    触发条件下也可以不用配置创建子进程的命令,它可以配置各种各样的命令,这样它的使用范围就更广了。

    脚本语言的解析工作

    脚本语言的语法规则已经定义完成了,脚本语言大家也看到了就是按语法规则写的一套文本内容,这些文本内容要想生效,是需要编写一套解析器的,关于解析器如何工作的就不赘述了,下面会介绍几个关键的类。

    Service

    Service类中存储了通过脚本文件配置的子进程的基础信息,并且还存储了子进程在创建成功以后的信息,比如pid,启动时间,崩溃次数等,如下代码:

    system/core/init/service.h
    
    class Service {
        // servicename
        const std::string name_;
        //配置的classname
        std::set classnames_;      
        unsigned flags_;
        //子进程创建成功后会保存它的pid
        pid_t pid_;
    
        //子进程开始运行的时间
        android::base::boot_clock::time_point time_started_;  // time of last start
        //子进程崩溃的时间
        android::base::boot_clock::time_point time_crashed_;  // first crash within inspection window
        //子进程崩溃的次数
        int crash_count_;   
    
        省略其他的属性......
    }
    

    ServiceList

    因为会有很多的子进程配置信息,因此会存在很多的Service实例,ServiceList的作用就是存储这些Service实例

    system/core/init/servicelist.h
    
    class ServiceList {
    
        //存储Service实例
        private:
        std::vector services_;
    
        bool post_data_ = false;
        std::vector delayed_service_names_;
    
        省略其他代码......
    }
    

    Action

    Action类包含了脚本配置信息中的触发条件和触发条件下的各种命令

    system/core/init/action.h
    
    class Action {
        private:
            //property类型的触发条件
            std::map property_triggers_;
            //event类型的触发条件
            std::string event_trigger_;
    
            //包含的所有命令
            std::vector commands_;
    
            //为true 则代表子进程死掉的话就不会被重新创建;否则重新创建
            bool oneshot_;
    
            省略其他属性......
    }
    
    

    ActionManager

    与ServiceList类似,它包含了解析出来的所有的Action实例

    system/core/init/action_manager.h
    
    class ActionManager {
        private:
            //所有的Action实例
            std::vector actions_;
        
            省略其他属性......
    }
    
    

    加载Service、Action

    init进程启动过程中会调用system/core/init/init.cppLoadBootScripts方法去加载所有的配置信息,加载成功后根据配置信息来创建子进程。

    LoadBootScripts方法的代码

    //这个方法会在init进程启动阶段执行
    static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
        //创建脚本解析器
        Parser parser = CreateParser(action_manager, service_list);
    
        std::string bootscript = GetProperty("ro.boot.init_rc", "");
        //若ro.boot.init_rc属性没有设置具体的init.rc文件,则进入下面逻辑
        if (bootscript.empty()) {
            
            //解析 /system/etc/init/hw/init.rc 文件,会把on关键字解析为Action对象,会把service关键字解析为Service对象
            parser.ParseConfig("/system/etc/init/hw/init.rc");
            if (!parser.ParseConfig("/system/etc/init")) {
                late_import_paths.emplace_back("/system/etc/init");
            }
            // late_import is available only in Q and earlier release. As we don't
            // have system_ext in those versions, skip late_import for system_ext.
            parser.ParseConfig("/system_ext/etc/init");
            if (!parser.ParseConfig("/vendor/etc/init")) {
                late_import_paths.emplace_back("/vendor/etc/init");
            }
            if (!parser.ParseConfig("/odm/etc/init")) {
                late_import_paths.emplace_back("/odm/etc/init");
            }
            if (!parser.ParseConfig("/product/etc/init")) {
                late_import_paths.emplace_back("/product/etc/init");
            }
        } else {
            parser.ParseConfig(bootscript);
        }
    }
    

    总结

    我采用被动创建的方式来创建子进程:哪些子进程需要创建,那就使用脚本语言来配置相应的信息,我会在LoadBootScripts方法中把所有的配置好信息都收集起来,当init进程启动后会根据配置信息来创建子进程。

    需要用init脚本语言来配置创建子进程的步骤如下

  • 首先 子进程在以.rc的脚本文件中,使用service关键字来配置子进程相关的信息
  • 其次 在init.rc文件中(init.rc文件到底是在哪个目录这个是不确定的)使用import关键字引入脚本文件,使用on关键字来配置子进程的触发条件,
  • 触发条件配置完毕后,如若子进程在创建之前需要配置一些前置操作或命令,则基于触发条件下配置这些信息
  • 最后 使用start关键字来配置创建子进程的命令。
  • 子进程的基础信息会被解析到Service实例中,所有的Service实例会存放到ServiceList对象中。触发条件和它包含的命令会被解析到Action实例中,所有的Action实例会存放到ActionManager对象中。

    我的子进程的善后工作

    上面谈到了子进程”生“的问题,那现在咱们聊聊子进程”死“的问题。作为Android用户空间所有进程的鼻祖,从生物学的角度来看,我肯定比我的孩子、孙子们要先死掉,但是在Android系统却恰恰相反,我的生命周期尽然是最长的,我的很多子子孙孙都死了很多次了,我还依然活着,因此我创建的子进程万一死掉的话,那它的善后事情需要我来处理(真是白发人送黑发人啊)。

    那有人就会问了,你是如何知道你创建的子进程死掉的,这是个好问题,那我就来讲给大家听。

    监听子进程死掉

    监听子进程死掉非常的简单,主要是用到了Linux的以下知识点:
    signal机制

    它的主要作用是实现进程之间的通信,signal机制是最"吝啬"的、但是是最简单的进程之间的通信方式。为啥要说它是最“吝啬”呢?主要原因是signal对进程之间传递的数据仅且只能只能传递一个int类型的信号,但是像socket等通信方案对传递的数据并没有这样的限制,你说它“吝啬”不。简单主要体现在:若对哪个信号有兴趣,可以使用sigaction函数注册这个信号,当这个信号发生时,注册的函数就会被调用。
    这里一直在提信号,监听子进程状态有一个信号SIGCHLD,父进程可以注册这个SIGCHLD来监听子进程的状态,状态主要包括死掉(死掉包含正常死掉或者异常死掉比如crash)、停止、继续等。

    epoll机制

    epoll是“多路复用“技术最好的实现方案,"多路复用"看到这种专业性的词是不是一头雾水啊,咱们举个例子:正常咱们进行阻塞类型的IO读操作(比如从一个socket中读取数据),是不是都会创建一个单独的线程来监听是否有数据到达,如果没有数据到达则线程进入阻塞状态,有的话则线程就开始读取数据。那假如有20个甚至更多的阻塞IO读操作,是不是需要创建对应个数的线程。这些线程如果大部分都没有可读数据的情况下是不是都处于阻塞状态,这难道不是大大得浪费吗?因此“多路复用“技术就出现了,它的设计理念是:启动一个线程,谁有需要监听IO是否有可读数据到达的操作都可以交给这个线程。这里的“多路”指的就是上面例子中创建的多个线程,“复用”指的就是指用一个线程来进行监听操作。

    epoll机制在Android中使用非常的广泛,比如Handler的MessageQueue在没有Message的情况下进入阻塞,以及input事件从systemserver进程传递到app进程,甚至vsyn信号从surfaceflinger传递到app进程都用到了epoll机制。

    好了,有了上面的知识,那我就来介绍下我监听子进程死掉的思路:

  • 首先我先使用sigaction函数来注册SIGCHLD信号,这样就可以监听到子进程的状态了
  • 其次使用signalfd函数为SIGCHLD信号生成一个fd(文件描述符)
  • 再次使用epoll来见监听上一步生成的fd是否有可读数据
  • 如监听到fd上有可读数据,则证明子进程的状态发生了变化,还需要使用waitpid函数来获取是哪个子进程死掉了
  • 如上4步就可以监听到子进程死掉了

    下面是具体的代码,有兴趣的同学可以看下

    system/core/init/init.cpp

    static void InstallSignalFdHandler(Epoll* epoll) {
    //初始化sigaction,SIG_DFL:代表使用默认的信号处理行为。
    const struct sigaction act { .sa_handler = SIG_DFL, .sa_flags = SA_NOCLDSTOP };
    //注册SIGCHLD信号
    sigaction(SIGCHLD, &act, nullptr);

    //声明mask信号集
    sigset_t mask;
    //初始化并清空一个信号集,使其不包含任何信号
    sigemptyset(&mask);
    //把SIGCHLD信号加入mask信号集中
    sigaddset(&mask, SIGCHLD);

    省略代码......

    //SIG_BLOCK:代表将mask添加到当前的信号屏蔽集中
    if (sigprocmask(SIG_BLOCK, &mask, nullptr) == -1) {
    PLOG(FATAL)

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论