332 Android init 进程:用户态的第一个进程

系列导航330-启动总览 | 331-Bootloader | 333-Zygote

上上篇我们讲了 Bootloader 如何加载内核。上篇我们讲了内核如何解压、初始化硬件并跳转执行用户态代码。

现在我们来到 Android 启动链中一个承上启下的关键角色——init

init 是 Linux 内核启动后第一个运行的用户态进程(PID 1)。在 Android 里,它远比标准 Linux 的 systemd/sysvinit 简单,但做了几件高度定制化的事:

  1. 解析并执行 init.rc(配置语言)
  2. 管理属性服务(property service)
  3. 加载 SELinux 安全策略
  4. 孵化 Zygote

init 是什么

在 Linux 中,PID 1 是所有进程的祖先。内核在完成初始化后,会尝试运行 /sbin/init(或内核命令行指定的 init 程序)。如果找不到,内核会 panic。

Android 的 init 是 system/core/init/ 目录下的一个独立项目,用 C++ 编写,直接被编译成可执行文件:

Bash
# Android 设备上的 init
/system/bin/init
# 或者
/sbin/init

它的源代码结构:

Bash
system/core/init/
├── init.cpp              # 主入口,解析命令行、调用 Parser
├── parser.cpp           # init.rc 解析器
├── ast.cpp              # 抽象语法树(Section/Trigger/Service)
├── builtins.cpp         # 内置命令实现(mount、mkdir、write...)
├── property_service.cpp # 属性服务的服务端实现
├── selinux.cpp          # SELinux 策略加载
├── devices.cpp          # 设备节点管理(ueventd)
├── keychords.cpp        # 键盘 chord 检测(解锁界面)
└── init.rc              # 主配置文件

init.rc 是什么

init.rc 是 init 的配置文件,用一种声明式的领域语言(DSL)描述了启动过程中的所有行为。

Linux 的 init 系统(systemd)用的是单元文件(INI 格式),Android 发明了自己的格式——一种基于 trigger 的配置语言。

init.rc 的基本结构

Rc
# 第一部分:导入其他配置文件
import /init.usb.configfs.rc
import /init.${ro.hardware}.rc     # 设备特定配置
import /init.trace.rc
import /init.kernel.modules.rc

# 第二部分:on 触发器(事件驱动的服务启动)
on late-init
    # 触发时机:late_initcall,内核已经准备好
    trigger late-init

on late-init
    # 挂载文件系统
    mount tmpfs tmpfs /sys/fs/cgroup nosuid nodev noexec
    chmod 0775 /sys/fs/cgroup
    mkdir /sys/fs/cgroup/freezer 0755 root root
    mount cgroup cgroup /sys/fs/cgroup/freezer freezer

    # 恢复用户数据(如果需要)
    exec_background /system/bin/e2fsck -f -y /dev/block/bootdevice/by-name/userdata

    # 设置内核变量
    write /proc/sys/kernel/panic 3
    write /proc/sys/kernel/panic_on_oops 1
    write /proc/sys/kernel/panic_on_warn 1

on boot
    # 基础环境设置
    # 恢复 SELinux 豁免规则
    restorecon /sys/kernel/security
    restorecon /sys/kernel/debug

    # 启动属性服务
    start property_writer

on property:sys.boot_completed=1
    # boot_completed 后做最后清理
    trigger post-fs-data
    trigger early-boot

Service(服务)定义

Service 是 init 启动的进程。每个 Service 有自己的名字、程序路径、权限:

Rc
# 定义一个服务:servicemanager
service servicemanager /system/bin/servicemanager
    # 运行在 cgroup 的什么位置
    class core animation
    # 用户和组
    user system
    group system readproc
    # 关键服务:init 退出它也要退
    critical
    # SELinux 上下文
    seclabel u:object_r:servicemanager_exec:s0
    # 超时配置(如果 20 秒内没启动就重启)
    timeoutperiod 20

注意:init.rc 里的 service 并不是”守护进程”的意思——init 会负责启动这些进程,如果它们挂了,init 可以选择重启它们。

Trigger 机制

init.rc 使用 trigger 机制决定什么时候启动什么:

Bash
# 系统启动后触发
on boot
    # boot trigger 触发后启动一些基础服务
    start ueventd
    start healthd

# 某个属性变化后触发
on property:ro.build.product=gpu
    # 特定产品变体做一些额外配置
    setprop debug.sf.mgpu 1

# late-init 后触发
on late-init
    trigger other-late-init

init 的启动流程

Bash
内核执行 /sbin/init(PID 1)
        │
        ▼
┌──────────────────────────────────────────────────────────────┐
│  init.cpp main()                                            │
│    1. 创建 mount namespace(CLONE_NEWNS)                  │
│    2. 挂载基础文件系统(/proc, /sys, /dev)                 │
│    3. 初始化日志(logcat)                                  │
│    4. 解析 init.rc                                          │
│    5. 触发 early-init trigger                              │
│    6. 初始化属性服务                                        │
│    7. 加载 SELinux 策略(sepolicy)                         │
│    8. 触发 late-init trigger                               │
│    9. 进入 service 循环(fork/exec/restart)               │
└──────────────────────────────────────────────────────────────┘

步骤 1-2:Mount Namespace

init 一开始就创建了 mount namespace。这意味着 init 挂载的文件系统对其他进程不可见(除非共享挂载)。

Cpp
// init.cpp 简化逻辑
namespace pid1_ns {};
clone(CLONE_NEWNS | CLONE_NEWPID, ...);
// 之后的 mount 只影响 pid1_ns
mount("tmpfs", "/dev", "tmpfs", ...);

步骤 3-4:解析 init.rc

init 用手写的递归下降解析器解析 init.rc:

Bash
init.rc 文本 → Lexer → AST(抽象语法树)→ Parser → Action/Service 对象

AST 有三种节点类型:

  • Section:顶层块(import、on、service)
  • Trigger:on 块里的触发条件(boot、late-init、property:*)
  • BuiltinCommand:内置命令(exec、mount、write、chmod)

步骤 5-6:属性服务

init 同时是属性服务的服务端。属性服务是一个共享内存区域,所有进程可以读写系统属性。

Cpp
// property_service.cpp
// 共享内存 key = "android_prop_service"
// 原子读写,避免竞争条件

// 属性来源(优先级从高到低):
// 1. cmdline(内核传参)
// 2. /data/local.prop(调试用,已被 Google 禁用)
// 3. property 空间(init 设置的默认值)
// 4. init.rc property trigger 触发的

步骤 7:SELinux 策略加载

Android 5.0+ 强制开启 SELinux。init 在触发 late-init 之前加载 sepolicy:

Cpp
// selinux.cpp
SelinuxInitialize("selinux_prop");
// 加载 /system/etc/selinux/plat_sepolicy.cil(or .bin)
// 恢复每个文件的 SELinux 上下文
restorecon("/sys/kernel/security", RESTORECON_RECURSE);

步骤 8:Service 循环

init 进入了主循环——一个类似事件循环的结构:

Bash
for (;;) {
    // 1. 等待下一个属性变化(epoll_wait)
    // 2. 处理 property 触发的 action
    // 3. 重启挂掉的关键服务(critical)
    // 4. 重新触发触发器(trigger)
}

init 的三大职责

1. 挂载文件系统

init 在 boot trigger 里挂载了 Android 需要的基础文件系统:

Rc
on boot
    # /proc(进程信息)
    mount proc proc /proc nosuid nodev noexec

    # /sys(内核信息)
    mount sysfs sysfs /sys nosuid nodev noexec

    # /dev(设备节点)
    mount tmpfs tmpfs /dev mode=0755

    # /dev/pts(pty)
    mkdir /dev/pts 0755 root system

    # /dev/kmsg(内核日志)
    touch /dev/kmsg
    chmod 0600 /dev/kmsg

    # /sys/fs/selinux(SELinux 文件系统)
    mount selinuxfs selinuxfs /sys/fs/selinux

2. 属性服务(Property Service)

Android 的系统属性是一个全局 key-value 存储,所有进程共享。例如:

Bash
adb shell getprop ro.build.display.id
# PQ3A.190801.002 eng.root.20190801.020758

adb shell getprop sys.boot_completed
# 1

adb shell setprop debug.myapp.feature 1
# 设置自定义属性

属性服务通过 socket 通信(不像 Linux 的环境变量,每个进程都fork)。init 维护一块共享内存,每次读写都是原子的(通过 futex 原子锁)。

3. Zygote 启动

Zygote 是 init 启动的最重要的服务。它是所有 APP 进程的父进程。

Rc
# init.zygote64_32.rc(64位为主,32位作为次)
service zygote /system/bin/app_process64
    class main
    socket zygote stream 660 root system
    socket usap_pool_min_streaming_native_pool stream 660 root system
    onexec /system/bin/linker64 /system/bin/app_process64
    priority 1000
    priority -20
    limit 16384 1000000 1000000 10 1000000
    group root readproc reserved_disk
    seclabel u:object_r:zygote_exec:s0

深入解析 init.rc 配置语言

init.rc 不是标准的配置文件格式,它有自己的语法。

完整语法示例

Rc
# 注释
# 导入
import /init.power.rc

# Section: on trigger
on <trigger>
    <builtin-command>

# Section: service
service <name> <pathname> [ <argument> ]*
    <option>

# 选项(option)
user <username>        # 以哪个用户运行
group <groupname>     # 以哪个组运行
class <classname>     # 属于哪个服务类
capabilities <caps>   # Linux capabilities
seclabel <label>      # SELinux 上下文
oneshot               # 只运行一次,不重启
disabled             # 默认不启动,需要 trigger 显式启动
socket <name> <type> <perm> <uid> <gid>  # 创建 Unix Domain Socket
critical             # 关键服务,退出会导致重启
timeoutperiod <secs> # 超时时间
onrestart <command>  # 重启时执行的命令

实际例子:servicemanager

Rc
service servicemanager /system/bin/servicemanager
    user system
    group system readproc
    critical
    seclabel u:object_r:servicemanager_exec:s0
    timeoutperiod 20

解析器的限制

init.rc 解析器是纯手写的,没有用 lex/yacc 或正则表达式。它只能解析 init.rc 语法,不能做复杂逻辑。这也意味着:init.rc 里不能写 if-else、循环、变量展开。

如果你在 init.rc 里写:

Rc
on boot
    ${myvar}  # 不支持变量展开

这会报语法错误。init.rc 只有 trigger 和 builtin command,没有变量概念。


属性服务的工作原理

Android 属性服务是一个共享内存环形缓冲区实现的服务:

Bash
┌──────────────────────────────────────────────────────────────┐
│  Shared Memory(ashmem)                                      │
│    prop_info[] 数组(每个属性 92 字节)                       │
│    通过 fcntl(F_SETLK) 锁保护                                 │
└──────────────────────────────────────────────────────────────┘
            ▲▽
    ┌──────────┴──────────┐
    │ init 进程(服务端)   │          监听 property socket
    │ /dev/socket/property │          接收 setprop/getprop 请求
    └──────────┬──────────┘
               │
    ┌──────────┴──────────┐
    │ 其他进程(客户端)   │
    │ libcutils API       │
    └─────────────────────┘

关键 API:

Cpp
// 读取属性
property_get("ro.build.id", value, "default");

// 设置属性(会触发 property:xxx=yyy trigger)
property_set("sys.boot_completed", "1");

// 监听属性变化
property_add_change_listener("sys.boot.*", callback);

SELinux 在 init 中的角色

SELinux 对 init 进程特别严格。init 本身运行在 u:object_r:init:s0 上下文,所有它启动的服务必须有对应的 type 定义。

Bash
# 查看当前进程的 SELinux 上下文
adb shell id -Z
# u:r:shell:so0 permissive

# 查看 init 进程的上下文
adb shell cat /proc/1/status | grep selinux
# 从 dmesg 可以看到 SELinux 日志
dmesg | grep SELinux

init.rc 里的 seclabel 选项覆盖默认的 SELinux 类型:

Rc
service surfaceflinger /system/bin/surfaceflinger
    class core animation
    seclabel u:object_r:surfaceflinger_exec:s0

如果 sepolicy 里没有定义这个 type,init 会在启动时拒绝执行。


init 进程的 Ueventd

init 还负责管理 /dev 目录下的设备节点。这是通过 ueventd 实现的——它监听 netlink socket,接收内核发出的 uevent(设备插拔、权限变更),自动创建或修改设备节点。

Bash
内核 uevent(通过 netlink)
        ↓
ueventd 接收
        ↓
根据 ueventd.rc 配置创建设备节点
        ↓
设置权限(Owner/Group/Mode)

和标准 Linux init 的对比

维度 标准 Linux (systemd) Android init
配置文件 Unit 文件(INI 格式) init.rc (DSL)
服务管理 Unit + dependency Trigger + service
属性管理 Env variables Property service
日志 journald logcat (ring buffer)
安全策略 AppArmor / SELinux SELinux (强制)
Socket 激活 Yes (socket unit) No
定时器 timer unit No

核心区别:Android init 没有依赖解析能力,所有启动顺序靠 trigger 的嵌套顺序控制。这比 systemd 的依赖图简单,但灵活性差很多。


动手环节

1. 查看 init.rc

Bash
# 在设备上查看
adb shell cat /init.rc

# 或者查看设备特定的
adb shell cat /init.$(getprop ro.hardware).rc

# 查看 zygote 配置
adb shell cat /init.zygote64_32.rc

2. 分析 init 日志

Bash
# 查看 init 进程的 logcat 输出
adb logcat -b events | grep init
adb logcat -b system | grep "sys.boot"

# 查看属性变化
adb shell dumpsys property

# 查看服务状态
adb shell service list

3. 测试属性触发

Bash
# 设置一个属性,触发 init.rc 里的 trigger
adb shell setprop debug.init.test 1
# 然后查看 init.rc 是否有 on property:debug.init.test=1 的处理

4. Fork 一个 wandos init

Wandos 的 init 实现比 Android 简单得多——它只是循环执行 builtin 命令。尝试 fork wandos,添加一个简化的 property service。


Wandos 对比

维度 Android init Wandos init
配置语言 init.rc (DSL) JSON
进程管理 fork/exec/restart simple spawn
属性服务 property service (socket) global map
安全 SELinux
日志 logcat stdout

Wandos init 的目标是让学习者理解 init 的核心逻辑(解析配置→fork→exec→监控),而不是完整的 Android init 复杂性。


系列导航


附录:常见 init 错误排查

症状 原因 解决
init 卡在 Waiting for /dev/.coldboot ueventd 没有正确发出事件 检查内核 uevent 配置
SELinux policy deny 导致服务启动失败 seclabel 写错或 sepolicy 缺失 添加 type 或用 permissive 测试
属性 sys.boot_completed 永远是空 init.rc 没有执行到 late-init 检查 logcat init 日志
服务启动后立即退出 程序路径错误或缺少 so adb shell run-as 调试
property_set 报错 只读属性不能被覆盖 只能用 ro.* 属性(只读)

作者:公众号「解码日记」,专注于操作系统、虚拟机、内核级技术深度解析。每周三更新。

最后修改: 2024年4月20日

作者

评论

发表评论

您的邮箱地址不会被公开。