我是init进程

为何要写系列文章

自己从事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文件)

objectivec 复制代码
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文件)

csharp 复制代码
//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++来定义一套数据结构/实体类(数据结构/实体类是用来存放配置信息的),有需求的模块把自己的配置信息配置好后,需要调用某个方法或者某个类的方法把这些配置信息保存到内存中。 下面是伪代码:

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

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

......

其他子进程的配置信息

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

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

通过脚本语言来配置

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

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

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

定义脚本语言

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

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

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

那就按上面的4个步骤依次来制定语法规则

配置子进程基础信息

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

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

配置触发条件

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

csharp 复制代码
# 这个语法就非常的简单明了,在什么条件下做哪些事情
on 触发条件

# 下面是一些简单的例子
# 在init启动时候做哪些事情
on init

# 在init启动前期做哪些事情
on early-init

# 在init启动后期做哪些事情
on late-init

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

配置前置命令

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

perl 复制代码
# 在触发条件达成的时候,分别执行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关键字,与前置命令的配置一样,创建子进程的命令也是基于触发条件的,如下:

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

模块化

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

arduino 复制代码
import xxx/xxx/xx.rc

小结

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

shell 复制代码
# 在xxx/xx.rc 脚本文件中配置子进程的基础信息
service servicename <pathname> [ <argument> ]*
   <option>
   <option>
   ...


# 在xxx/init.rc 脚本文件中引入上面的脚本文件,并配置对应的触发条件和前置命令
import xxx/xx.rc

on 触发条件
   command1
   command2
   ...
   start servicename

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

例子

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

servicemanager子进程基础信息配置

perl 复制代码
文件路径: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子进程

bash 复制代码
文件路径: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属性类型触发条件例子:

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

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

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

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

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

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

bash 复制代码
# 在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。如下例子:

csharp 复制代码
# 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,启动时间,崩溃次数等,如下代码:

c 复制代码
system/core/init/service.h

class Service {
    // servicename
    const std::string name_;
    //配置的classname
    std::set<std::string> 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实例

c 复制代码
system/core/init/servicelist.h

class ServiceList {

    //存储Service实例
    private:
    std::vector<std::unique_ptr<Service>> services_;

    bool post_data_ = false;
    std::vector<std::string> delayed_service_names_;

    省略其他代码......
}

Action

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

c 复制代码
system/core/init/action.h

class Action {
    private:
        //property类型的触发条件
        std::map<std::string, std::string> property_triggers_;
        //event类型的触发条件
        std::string event_trigger_;

        //包含的所有命令
        std::vector<Command> commands_;

        //为true 则代表子进程死掉的话就不会被重新创建;否则重新创建
        bool oneshot_;

        省略其他属性......
}

ActionManager

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

c 复制代码
system/core/init/action_manager.h

class ActionManager {
    private:
        //所有的Action实例
        std::vector<std::unique_ptr<Action>> actions_;
    
        省略其他属性......
}

加载Service、Action

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

LoadBootScripts方法的代码

scss 复制代码
//这个方法会在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脚本语言来配置创建子进程的步骤如下

  1. 首先 子进程在以.rc的脚本文件中,使用service关键字来配置子进程相关的信息
  2. 其次 在init.rc文件中(init.rc文件到底是在哪个目录这个是不确定的)使用import关键字引入脚本文件,使用on关键字来配置子进程的触发条件,
  3. 触发条件配置完毕后,如若子进程在创建之前需要配置一些前置操作或命令,则基于触发条件下配置这些信息
  4. 最后 使用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机制。

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

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

如上4步就可以监听到子进程死掉了

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

system/core/init/init.cpp

arduino 复制代码
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) << "failed to block signals";
    }

    // Register a handler to unblock signals in the child processes.
    //在子进程创建成功后,恢复SIGCHLD为非屏蔽
    const int result = pthread_atfork(nullptr, nullptr, &UnblockSignals);
    if (result != 0) {
        LOG(FATAL) << "Failed to register a fork handler: " << strerror(result);
    }

    //调用signalfd函数为mask生成一个fd
    signal_fd = signalfd(-1, &mask, SFD_CLOEXEC);
    if (signal_fd == -1) {
        PLOG(FATAL) << "failed to create signalfd";
    }

    constexpr int flags = EPOLLIN | EPOLLPRI;
    //使用epoll来监听signal_fd上的数据
    if (auto result = epoll->RegisterHandler(signal_fd, HandleSignalFd, flags); !result.ok()) {
        LOG(FATAL) << result.error();
    }
}

//如果fd上有数据就会调用这个方法
static void HandleSignalFd() {

    //读取到siginfo信息
    signalfd_siginfo siginfo;
    ssize_t bytes_read = TEMP_FAILURE_RETRY(read(signal_fd, &siginfo, sizeof(siginfo)));
    if (bytes_read != sizeof(siginfo)) {
        PLOG(ERROR) << "Failed to read siginfo from signal_fd";
        return;
    }

    //判断当前的ssi_signo
    switch (siginfo.ssi_signo) {
        case SIGCHLD:
            //只看SIGCHLD
            ReapAnyOutstandingChildren();
            break;

        省略无关代码......
    }
}


system/core/init/sigchld_handler.cpp

void ReapAnyOutstandingChildren() {
    while (ReapOneProcess() != 0) {
    }
}


static pid_t ReapOneProcess() {
    siginfo_t siginfo = {};
    //调用waitpid方法来获取死掉的子进程的信息
    if (TEMP_FAILURE_RETRY(waitid(P_ALL, 0, &siginfo, WEXITED | WNOHANG | WNOWAIT)) != 0) {
        PLOG(ERROR) << "waitid failed";
        return 0;
    }

    
    const pid_t pid = siginfo.si_pid;
    if (pid == 0) {
        DCHECK_EQ(siginfo.si_signo, 0);
        return 0;
    }

    省略无关代码......
}

处理子进程善后工作

如上面监听到子进程死掉的时候会通过waitpid方法获取到子进程的pid,还记得在创建子进程的时候,子进程的配置信息都会放在Service类中,根据pid可以找到对应的Service,Service持有了创建子进程的时候持有的各种socket等资源,这些资源会被清除掉,并且会通知kernel层杀掉子进程,进而在kernel层清除掉子进程占据的各种资源

让子进程起死回生

有些子进程在死掉的时候希望能被重新创建。在子进程的善后工作结束后,我会重新创建这些子进程。

我的信息中心--属性服务

为什么突然要给大家介绍属性服务 ,那是因为属性服务在init进程的工作中有非常重要的作用。为了让大家知道属性服务有一个了解,先来看段代码

vbnet 复制代码
//通过调用java层api的方式设置和存储key value

//调用SystemProperties的set方法可以把 key value存储起来
SystemProperties.set(key,value);
//调用SystemProperties的get方法可以把key对应的valu值获取到
SystemProperties.get(key)


//命令行也可以设置和获取key value
//获取root权限后,进入shell
adb root
adb shell

//设置 xxx.xx 为value
setprop xxx.xx value
//获取xxx.xx的值
getprop xxx.xx

上面代码中不管是通过SystemProperties的api方式还是通过命令行的方式 设置和获取key value的功能都是由属性服务 提供的。SharedPreferences大家都应该用过,没用过也没事,它的主要作用是为进程内提供字典类型数据的存取(字典类型数据是以key value的键值对)。那在进程之间其实也需要能有这样的功能:一个进程设置了key value的数据,其他的进程是可以获取到的,那属性服务就提供了这种功能。属性服务提供了系统级的存取key value键值对的功能

属性服务 会在init进程启动的阶段启动,属性服务为啥要常驻于init进程中,而没有选择常驻于其他进程中呢?其主要原因还是由于init进程在整个用户空间的所有进程中它的生命周期是最长的,属性服务常驻于此可以为别的进程提供更稳定的服务。不管在哪个进程中存储key value数据,最终的数据都是保存在属性服务 中的。其他进程存储key value数据时候,数据会通过socket传递到属性服务(关于为啥选用socket后面章节会有详细的介绍)。

属性服务 对于init进程来说太重要了:首先属性服务解决了子进程之间或者子进程与init进程之间的通信问题;其次上面提到的触发条件为property属性类型的Action,有了属性服务后这类Action才会被执行。因此属性服务之于init进程犹如饭之于人的关系,人不吃饭就干啥啥不行,没有属性服务init进程根本不能正常的不断运行。这节只是先简单介绍下,后面会有章节对属性服务进行详细的介绍。

我的工作内容

经过前面小节的铺垫后,init脚本文件的内容分别解析到了Service和Action实例中,所有的Service实例被保存在了ServiceList实例中,所有的Action实例保存在了ActionManager实例中;init进程也可以处理子进程的善后工作了;属性服务也准备好了;当然还有其他的铺垫工作比如:创建挂载各种关键目录,Selinux设置等(这里不在赘述了)。这些铺垫工作结束后,我就进入了循环工作模式。

我进入循环工作模式

经常能听到你们这些程序员说自己加班多狠,什么996、10107了,你们在狠能有我狠吗?我是无休止的一直不停的工作,上图可以看出我会不断的循环执行下面的操作:

  1. 若是关机或重启的消息,则执行关机或重启操作,后面的操作不会在执行,关机或重启的优先级最高的
  2. 若不是关机或重启的状态,则调用ActionManager的ExecuteOneCommand方法
  3. 若不是关机或重启的状态,若有control类型的message,则把对应的control message传递给对应的子进程
关机或重启

以低电量的情况来分析下关机或重启的流程(其他的关机或重启的流程也类似,比如按关机键):

  1. 在低电量情况下,PowerManagerService服务(这个服务位于systemserver进程)的对应方法lowLevelShutdown或者lowLevelReboot方法被调用,这俩方法都会调用SystemProperties的set方法来为sys.powerctl的key设置"shutdown," + reason或者"reboot," + reason
  2. 属性服务 会监听到sys.powerctl的property属性信息,把这个属性信息保存下来,在每次循环的时候都会检查是否有关机或重启的属性信息,有的话就开始执行关机或重启操作。

大家发现没有,关机或重启的核心关键原理就是进程之间的通信,systemserver进程为sys.powerctl的key设置"shutdown," + reason或者"reboot," + reason属性服务 可以监听到此sys.powerctl的属性信息,进而执行关机或重启操作。属性服务在此起了关键作用。

调用ActionManager的ExecuteOneCommand方法

在介绍ActionManager的ExecuteOneCommand方法做了哪些事情之前,我再给大家来介绍下Action、Service,因为它们实在是太重要了,init进程的工作内容都是在围绕着它们进行,如果没有它们init进程也就"无事可做"了。

再次介绍Action

脚本文件中用关键字on配置的触发条件和触发条件包含的所有命令都会被解析到Action实例中,而所有的Action实例存放于ActionManager对象,当然也可以调用ActionManager的方法添加Action,在init进程进入循环工作状态的时候,ActionManager会收集各种各样非常多的Action实例。

有些Action之间其实会形成一个依赖关系(比如触发条件为"late-init"的Action依赖于触发条件为"init"的Action,"init"的Action又依赖于触发条件为"early-init"的Action),Action之间的形成的依赖关系犹如一串鞭炮,放一串鞭炮需要从头使用打火机点燃,这样鞭炮才会噼里啪啦的响。同理ActionManager中的头Action(触发条件为"early-init"的Action是头Action)需要先执行,只有它执行了,依赖于它的Action才会执行。因此在init进程进入不断循环工作之前时需要先依次触发"early-init"、"init"、"later-init"这三个Action执行的,代码如下:

c 复制代码
system/core/init/init.cpp

int SecondStageMain(int argc, char** argv) {
    省略代码......

    //触发器触发 "early-init" Action开始执行
    am.QueueEventTrigger("early-init");

    省略代码......

    //触发器触发 "init" Action开始执行
    am.QueueEventTrigger("init");

    std::string bootmode = GetProperty("ro.bootmode", "");
    if (bootmode == "charger") {
        am.QueueEventTrigger("charger");
    } else {
        //触发器触发 "late-init" Action开始执行
        am.QueueEventTrigger("late-init");
    }
}  

当然"early-init"、"init"、"later-init"这三个Action是串行执行(如果想了解这三个Action都做了哪些事情可以参考system/core/rootdir/init.rc),ueventd子进程是在"early-init"阶段创建的。logdlmkdservicemanager等子进程是在"init"阶段创建的。zygote子进程是在"later-init"阶段创建的

ActionManager中确实收集了非常多的Action实例,但是如果不通过ActionManager添加触发器,则这些Action实例就如"一潭死水",触发器可以触发一个Action的所有命令开始执行。触发器根据Action的触发条件不同而有所区别,Action的触发条件分为三类:event事件类型、property属性类型、event和property组合。触发event类型Action执行很简单直接调用ActionManager的QueueEventTrigger方法就可以做到,触发property属性类型的Action就要用到属性服务 ,别的进程或者init进程调用属性服务 的api了把触发条件中的key设置为对应的value值后,属性服务就可以监听到key value的属性信息,进而把这个属性信息交给ActionManager。

再次介绍Service

通过脚本文件配置的子进程的基础信息会被解析到Service实例,所有的Service实例存放于ServiceList对象。

Action与Service是如何发生关系的

Action的Command中若有start、stop、restart等control类型的command,则会根据后面的servicename去ServiceList中找到对应的Service,从而把对应的control信息发送给Service,Service依据control信息再决定是创建子进程、还是停止子进程、或者重新启动子进程。

每时每刻都会有各种各种的触发器放入ActionManager中,这些触发器只是单纯的放入了ActionManager中,对应的Action根本就没有执行,因此init进程需要不断的调用ActionManager的ExecuteOneCommand方法,这个方法的主要作用就是从ActionManager中取出触发器,进而触发对应的Action执行。同时也会不断的有Action通过调用ActionManager的方法添加到ActionManager中。

看到了吧属性服务在触发property属性类型的Action执行的时候起到了非常重要的作用。

处理control类型的message

ontrol类型的message是啥

先看一个例子

arduino 复制代码
adb root
adb shell
//启动开机动画子进程,在屏幕上会显示开机动画
setprop ctl.start bootanim

上面例子setprop ctl.start bootanim就可以使开机动画显示于屏幕之上,ctl.start bootanim就是control类型的message,control类型的message其实就是key value的property,它的主要作用就是控制Service的行为

control类型的message它的key只能从下面7个中取(也分别代表了可以控制子进程的哪些行为)

  1. "ctl.sigstop_on":代表在启动的时候发送SIGTOP信号
  2. "ctl.sigstop_off":与sigstop_on相反
  3. "ctl.oneshot_on":代表子进程死掉的时候需要重新启动
  4. "ctl.oneshot_off"与oneshot_on相反
  5. "ctl.start":创建子进程
  6. "ctl.stop":停止子进程
  7. "ctl.restart":先停止子进程在创建子进程

control类型的message它的value是子进程基础信息的子进程的名字,比如zygote、bootanim(开机动画子进程的名字),control类型的message只能是具有相应权限的进程才可以设置。

control类型的message在调用属性服务的api设置后,属性服务 会监听到它,根据value(servicename)从ServiceList中找到对应的Service,从而把对应的行为通知到Service。看到没又又是属性服务起了非常关键的作用。

小结

我会默默的进入后台,不断的重复着上面的工作内容,为Android系统保驾护航。

总结

我带大家认识了我的"父亲"--swapper进程,它是Android系统所有进程的鼻祖。我的"父亲"又创建了我的"兄弟"--kthreadd进程和我--init进程kthreadd进程是内核空间的所有进程的鼻祖。而我是用户空间所有进程的鼻祖,并且我是用户空间所有进程中最长寿的进程没有之一。

我还带大家认识了我的子进程--zygote进程servicemanager进程lmkd进程等,它们在Android系统中发挥着各自重要的作用。

由于我的子进程很多,并且不同子进程被创建的时机条件都不同,因此我没有采用"主动创建"的模式(否则我关心的事情太多了,会累死的),我采用了"被动创建"的模式,需要创建的子进程通过Android Init Language脚本文件进行配置,配置信息会分别解析到ServiceAction对象中,所有的Action对象会存放在ActionManager对象中,在init进程启动的时候把这些配置信息收集起来。在我进入"不断循环工作模式"后,会不断的通知ActionManager去根据是否有对应的触发器进而触发创建子进程。

同样我的子进程也有你们生物的特性--生老病死,我的子进程死掉的话,是需要我来处理它们的善后工作的。有些子进程在死掉的时候会希望能"重生",那我就会根据它的要求让它"起死回生"。

在我进入"不断循环工作模式"后,我会默默的进入后台,不断的做着下面的工作: 1.是否有关机或重启的消息,有的话执行关机或重启 2.调用ActionManager的ExecuteOneCommand方法,ActionManager会检查是否有触发器,有的话触发对应的Action执行,有些Action会包含创建子进程的start命令,根据start命令后面的servicename,开始创建对应的子进程 3.若有control类型的message,则把它交给对应的Service

在我整个工作内容中,我都是在围绕着各种ActionService工作,ActionService对我来说就是我的"生产资料",如果没有它们我将"无事可做"。

属性服务 起了非常大的作用,子进程与init进程进行通信用到了属性服务 (比如关机或重启操作),属性服务还触发相应的Action执行。

谢谢大家认识我,我就是init进程,在Android系统中独一无二的存在。

相关推荐
拭心11 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王13 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡14 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道14 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库15 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道16 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe16 小时前
Android Hook - 动态加载so库
android
居居飒16 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He19 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗20 小时前
Android笔试面试题AI答之Android基础(1)
android