Android 系统启动之 Init 进程启动分析四

本文基于 AOSP android-10.0.0_r41 版本讲解,内核版本 android-goldfish-4.14-gchips

second_stage 接下来就会开始解析执行 init.rc 文件,在看代码之前我们先了解一下 init.rc 文件的规则

1. init.rc 文件格式

init.rc 文件是以块 (section)为单位的,,一个块可以包含多行。块分成两大类:一类称为动作(action) ,另一类称为服务(service)

  • 动作(action):以关键字 on 开头,表示在某个时候执行一堆命令
  • 服务(service):以关键字 service 开头,表示启动某个进程的方式和参数

块 (section)以关键字 on 或者 service 开始,直到下一个 on 或者 service 结束,中间所有行都属于这个块 (section)

接着我们看两个示例:

bash 复制代码
on init
    sysclktz 0

    # Backward compatibility.
    symlink /system/etc /etc
    symlink /sys/kernel/debug /d

    # ......

这个 section 表示在系统启动(init)时,执行下面的一堆命令。

bash 复制代码
service adbd /sbin/adbd  
   user adb  
   group adb 

这里定义了一个服务 adbd,对应的执行文件是 /sbin/adbd,接着定义了服务的两个属性 user 和 group

另外,init.rc 中的注释以 # 开头。

2. init.rc 脚本语法

2.1 动作(action)

动作(action)的一般格式如下:

bash 复制代码
on  <trigger>         # 触发条件
    <command>         # 执行命令
    <command1>        # 可以执行多个命令

在动作(action)里面的,on 后面跟着的字符串是触发器(trigger),trigger 是一个用于匹配某种事件类型的字符串,当事件发生时,就会执行 trigger 下面的 command

触发器(trigger)有几种格式,最简单的一种是一个单纯的字符串。比如on boot,表示系统启动时。还有一种常见的格式是 on property<属性>=<值>。如果属性值在运行时设成了指定的值,则action 中的命令列表就会执行。

常见的 trigger 有以下几种:

  • on early-init:在初始化早期阶段触发
  • on init:在初始化阶段触发
  • on late-init:在初始化晚期阶段触发
  • on boot/charger:当系统启动/充电时触发
  • on property:当属性值满足条件时触发

command 是 action 的命令列表中的命令,或者是 service 中的 onrestart 选项的参数命令。

命令将在所属事件发生时被一个个地执行

常见命令有以下这些:

bash 复制代码
exec <path> [ <argument> ]*:运行指定路径下的程序,并传递参数
export <name> <value>:设置全局环境参数。此参数被设置后对全部进程都有效
ifup <interface>:使指定的网络接口"上线",相当激活指定的网络接口
import <filename>:导入一个额外的 rc 配置文件
hostname <name>:设置主机名
chdir <directory>:改变工作文件夹
chmod <octal-mode> <path>:设置指定文件的读取权限
chown <owner> <group> <path>:设置文件所有者和文件关联组
chroot <directory>:设置根文件夹
class_start <serviceclass>:启动指定类属的全部服务,假设服务已经启动,则不再反复启动
class_stop <serviceclass>:停止指定类属的全部服务
domainname <name>:设置域名
insmod <path>:安装模块到指定路径
mkdir <path> [mode] [owner] [group]:用指定参数创建一个文件夹
mount <type> <device> <dir> [ <mountoption> ]*:类似于linux的mount指令
setprop <name> <value>:设置属性及相应的值
setrlimit <resource> <cur> <max>:设置资源的rlimit
start <service>:假设指定的服务未启动,则启动它
stop <service>:假设指定的服务当前正在执行。则停止它
symlink <target> <path>:创建一个符号链接
sysclktz <mins_west_of_gmt>:设置系统基准时间
trigger <event>:触发另一个时间
write <path> <string> [ <string> ]*:往指定的文件写字符串

2.2 服务(service)

服务(service)的一般格式如下:

bash 复制代码
service <name><pathname> [ <argument> ]*
    <option>
    <option>
  • name:表示此服务的名称
  • pathname:可执行文件对应的路径
  • argument:执行可执行文件时传入的参数
  • option:服务的选项

服务的选项主要有:

  • class <name> [ <name>\* ]:为服务指定 class 名字。 同一个 class 名字的服务会被一起启动或退出, 默认值是 default

  • console [<console>]:这个选项表明服务需要一个控制台。 第二个参数 console 的意思是可以设置你想要的控制台类型,默认控制台是 /dev/console, /dev 这个前缀通常是被省略的, 比如你要设置控制台 /dev/tty0, 那么只需要设置为console tty0 即可。

  • critical:表示服务是严格模式。 如果这个服务在4分钟内或者启动完成前退出超过4次,那么设备将重启进入 bootloader 模式

  • disabled:这个服务不会随着 class 一起启动。只能通过服务名来显式启动。比如 foobar 服务的 class 是 core, 且是 disabled 的,当执行 class_start core 时,foobar 服务是不会被启动的。 foobar 服务只能通过 start foobar 这种方法来启动。

  • file <path> <type> 根据文件路径 path 来打开文件,然后把文件描述符 fd 传递给服务进程。type 表示打工文件的方式,只有三种取值 r, w, rw。对于 native 程序来说,可以通过 libcutils 库提供的 android_get_control_file() 函数来获取传递过来的文件描述符。举个例子, logd.rc 部分内容如下:

bash 复制代码
service logd /system/bin/logd
    socket logd stream 0666 logd logd
    socket logdr seqpacket 0666 logd logd
    socket logdw dgram+passcred 0222 logd logd
    file /proc/kmsg r
    file /dev/kmsg w
    user logd

其中通过 file /proc/kmsg r 以只读方式打开了设备文件 /proc/kmsg, 然后在代码中这么获取打开的文件, 见 sytem/core/logd/main.cpp main 函数:

cpp 复制代码
static const char dev_kmsg[] = "/dev/kmsg";
fdDmesg = android_get_control_file(dev_kmsg);
if (fdDmesg < 0) {
    fdDmesg = TEMP_FAILURE_RETRY(open(dev_kmsg, O_WRONLY | O_CLOEXEC));
}

这里可能有点奇怪,为什么要通过 file 这个选项来打开文件,而不直接在代码里面通过 open() 函数来打开呢?我觉得主要是权限问题,还是以 logd 为例子。/dev/kmsg 的权限是:

cpp 复制代码
generic_x86_64:/ # ls -l /dev/kmsg
crw------- 1 root root 1,  11 2023-12-04 12:14 /dev/kmsg

只有 root 用户可读写。而 logd 服务是以 user logd 选项来启动的,自然没有权限用 open() 函数来打开 /dev/kmsg 这个设备文件。file 选项则可以通过 init 进程把文件打开,然后把文件描述符传递给子进程, 从而解决了权限的问题。

  • group <groupname> [ <groupname>\* ]:

在启动 Service 前,将 Service 的用户组改为第一个 groupname, 第一个 groupname 是必须有的, 第二个 groupname 可以不设置,用于追加组(通过setgroups)。目前默认的用户组是 root 组。

  • oneshot:当服务退出的时候,不自动重启。适用于那些开机只运行一次的服务。

  • onrestart:在服务重启的时候执行一个命令

  • seclabel <seclabel>:在启动 Service 前设置指定的 seclabel,默认使用init的安全策略。 主要用于在 rootfs 上启动的 service,比如 ueventd, adbd。 在系统分区上运行的 service 有自己的 SELinux安全策略。

  • setenv <name> <value>:设置进程的环境变量

  • socket <name> <type> <perm> [ <user> [ <group> [ <seclabel> ] ] ]

创建一个 unix domain socket, 路径为 /dev/socket/name , 并将 fd 返回给 Service。 type 只能是 dgram, stream or seqpacket。user 和 group 默认值是 0。 seclabel 是这个 socket 的 SELinux security context, 它的默认值是 service 的 security context 或者基于其可执行文件的 security context。

  • user <username> 在启动 Service 前修改进程的所属用户, 默认启动时 user 为 root

最后,我们看个例子:

bash 复制代码
service bootanim /system/bin/bootanimation
    class core  //给服务指定一个类属,这样方便操作多个服务同时启动或停止
    user graphics //在执行此服务之前先切换用户名
    group graphics audio
    disabled  //这个服务不会随着 class 一起启动
    oneshot  //当此服务退出时不会自动重启

本文介绍了 init.rc 的基本结构与常用的一些配置选项,日常工作中遇到不懂的一些配置,可以查阅源码中的文档 system/core/init/README.md

参考资料

关于

我叫阿豪,2015 年本科毕业于国防科学技术大学指挥信息系统专业,毕业后从事信息化装备的研发工作,工作内容主要涉及 Android Framework 与 Linux Kernel。

如果你对 Android Framework 感兴趣或者正在学习 Android Framework,可以关注我的微信公众号和抖音,我会持续分享我的学习经验,帮助正在学习的你少走一些弯路。学习过程中如果你有疑问或者你的经验想要分享给大家可以添加我的微信,我拉你进技术交流群。

相关推荐
阿巴斯甜18 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker18 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952719 小时前
Andorid Google 登录接入文档
android
黄林晴21 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android