文章内容
素材来源:AgentWorkFlow「02-开机启动 Init 进程」
一、从 1 号进程说起:init 要回答的三个问题
Android 启动后执行 adb shell ps,能看到 init 的 PID 是 1;杀掉一个由 init 管理、允许重启的 native service,新 PID 很快又会出现。这两个现象指向三个问题:init.rc 明明只是文本,怎样变成内存中的配置?配置里的命令怎样变成 zygote、servicemanager、surfaceflinger 等进程?子进程退出后,PID 1 怎样获知并决定是否重启?
Android 6.0 的答案可以压缩成一条主链:init 解析 init.rc,把 service、on、import 组织为链表;trigger 对应的命令进入 action 队列;主循环通过函数指针执行命令;服务最终由 fork 和 execve 启动;socketpair、epoll 与 SIGCHLD 把退出事件送回主循环,init 再依据 SVC_* 标志位处理。静态脚本由此成为可运行、可恢复的进程体系。
二、开机链路概览:上电到 init
从全局位置看,bootloader 阶段由 U-Boot 完成硬件和内核装载;内核建立 idle 0 号进程,随后创建用户空间的 init 1 号进程。素材中的 switchTo 描述上下文切换,idle 的优先级标作 -1000。init 最终执行 /init 这个 ELF 可执行文件,从 main() 进入 Android 用户空间初始化。

PID 1 有双重身份。它是后续用户进程树的源头,也是俗称的“孤儿院”:某个父进程退出后,失去父进程的后代会被重新托管,init 负责回收退出状态,避免僵尸进程长期残留。这里的“所有用户进程祖先”描述的是进程树与托管关系,不等于每个进程都由 init 直接 fork。
init 的任务可以归纳成四块:创建目录和挂载分区、解析启动脚本、启动脚本描述的服务、守护这些服务。前两块偏系统准备,后两块是本文主线。这里说的是对 init 行为的归纳,不是 main() 里四个带名字的阶段。main() 其实是一条顺序执行的流水线,中间还穿插属性服务、SELinux 等工作。前三块让系统跑起来,最后一块让关键 native service 在异常退出后恢复。

三、init 入口:读 init.rc 之前做了什么
Android 6.0.0_r1 的 /system/core/init/init.cpp 中,main() 位于 989—1138 行。第一阶段挂载 /dev、/proc、/sys 等基础文件系统,初始化标准输入输出和内核日志;klog_init 打开日志通道,klog_set_level 设置级别,log.h 中的宏经 klog_write 把 init 日志送往 klog。加载 SELinux policy 后,init 通过 execv 进入 second stage,并为 /dev、socket 和属性区域执行 restorecon。
第二阶段创建 epoll 实例,调用 signal_handler_init,加载默认属性并启动 property service,接着才执行 init_parse_config_file("/init.rc")。解析和服务启动依赖文件系统、SELinux context、属性以及事件循环;这些基础能力缺失时,脚本即使读到了也无法安全执行。
四、解析启动脚本:init.rc 解析
init.rc 的顶层语法有三类 section:
import引入其他 rc;on声明由 trigger 激活的 action;service保存进程名、可执行文件、参数及运行选项。
为什么要转成链表?脚本描述的是延迟发生的事件,解析时环境往往尚未满足。结构化保存后,init 可以按 trigger 和 class 选择性调度,也能在服务退出时复用原定义。

先看一段最小 init.rc,因为它同时展示引入、trigger 和内建命令的形状:
# 引入环境变量脚本import /init.environ.rc# 引入由 ro.zygote 属性决定的 zygote 配置import /init.${ro.zygote}.rc # 声明 early-init 触发段on early-init# 把 PID 1 及其后代的 OOM 调整值写为 -1000 write /proc/1/oom_score_adj -1000 # 声明 init 触发段on init# 建立兼容旧路径的符号链接 symlink /system/etc /etc# 创建 cpu accounting 挂载点 mkdir /acct# 挂载 cpuacct cgroup mount cgroup none /acct cpuacct# 创建 data 目录并指定权限与属主 mkdir /data 0771 system system解析器把文本行变成 import 记录与 action/command 结构;此刻 mount、mkdir、write、symlink 仍未执行。代码证明 section 与命令属于两层结构,trigger 决定命令何时进入队列。
zygote 的定义进一步展示 service 保存了什么:
# 定义服务名、可执行文件和启动参数service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server# 把 zygote 归入 main class class main# 声明名为 zygote 的 UNIX stream socket 及权限 socket zygote stream 660 root system# 服务重启时唤醒系统 onrestart write /sys/android_power/request_state wake# 服务重启时打开电源状态 onrestart write /sys/power/state on# 服务重启时联动重启 media onrestart restart media# 服务重启时联动重启 netd onrestart restart netd解析结果里的 svc->args[0] 是 /system/bin/app_process64,其余字段进入 class、socket 和 onrestart 命令链。

五、解析器实现:从字符到三条链表
入口要看 read_file 和 token 循环,因为它界定了解析器的输入、输出与错误边界:
// 代码位置 /system/core/init/init_parser.cpp 第 440—453 行// 声明读取并解析单个配置文件的入口int init_parse_config_file(const char* path) {// 保存读取到的完整文本 std::string data;// 读取失败时终止本文件解析 if (!read_file(path, &data)) return -1;// 交给通用配置解析器 parse_config(path, data);// 返回解析成功 return 0;// 结束函数} // 代码位置 /system/core/init/init_parser.cpp 第 384—436 行// 声明配置文本解析函数static void parse_config(const char* fn, const std::string& data) {// 保存一行拆出的参数 char* args[INIT_PARSER_MAXARGS];// 保存当前行参数数量 int nargs = 0;// 声明解析状态 parse_state state;// 记录当前文件名 state.filename = fn;// 从第零行开始计数 state.line = 0;// 复制文本并让扫描指针指向副本 state.ptr = strdup(data.c_str());// 清空预读 token state.nexttoken = 0;// 默认忽略尚未归属 section 的配置行 state.parse_line = parse_line_no_op;// 收集 import 表达式的链表 struct listnode import_list;// 初始化 import 链表头 list_init(&import_list);// 让解析状态持有该链表,供 import 分支使用 state.priv = &import_list;// 持续获取 token for (;;) {// 根据 token 类型处理 switch (next_token(&state)) {// 文件结束时收尾 case T_EOF: goto parser_done;// 一行结束时进入关键字识别与分发 case T_NEWLINE: break;// 文本 token 进入当前行参数数组 case T_TEXT: args[nargs++] = state.text; break;// 结束分支选择 }// 结束循环 }// 解析结束标签,真实源码随后遍历 import_listparser_done:// 保留空语句作为节选终点 ;// 结束函数}这里为聚焦"如何从字符变成结构体"保留了循环骨架;T_NEWLINE 触发的语义分发发生在 parse_new_section(见下),import 则在本文件解析完成后递归处理。状态变化发生在 parse_state.ptr、args 和各结构链表中。next_token 返回值 T_NEWLINE 才触发一次语义分发。
这里单独看 next_token。它是逐字符状态机,空白、注释、换行和 EOF 都在这里获得明确语义:
// 代码位置 /system/core/init/parser.cpp 第 26—141 行// 从当前解析状态取下一个 tokenint next_token(struct parse_state* state) {// x 指向尚未消费的字符 char* x = state->ptr;// 持续扫描字符 for (;;) {// 按当前字符切换状态 switch (*x) {// 字符串终止符代表文件结束 case 0: state->ptr = x; return T_EOF;// 换行符结束当前逻辑行 case '\n': x++; state->ptr = x; return T_NEWLINE;// 空格属于可跳过分隔符 case ' ':// 制表符属于可跳过分隔符 case '\t':// 回车属于可跳过分隔符 case '\r': x++; continue;// 井号开始注释 case '#':// 消费到换行或文件结束 while (*x && *x != '\n') x++;// 回到外层继续判定换行或 EOF continue;// 其他字符进入文本读取状态 default: goto text;// 结束字符分支 }// 结束扫描循环 }// text 标签后的 AOSP 代码继续处理引号与转义,最终返回 T_TEXTtext:这里的代码截到 text 标签,标签后的公开源码负责引号、反斜杠和属性文本等细节,并在文本结束时返回 T_TEXT。
一个容易忽略的细节是 state->nexttoken 预读:文本 token 在行尾遇到换行时,text 分支会把 T_NEWLINE 先存进 nexttoken,留到下次调用 next_token 时再吐出。这样一次扫描能在一个逻辑行里先后输出 T_TEXT 和 T_NEWLINE 两个 token,换行分发才能拿到本行参数再触发语义判断。
它消费 state->ptr 指向的字符,只产出 T_EOF、T_NEWLINE、T_TEXT 这几类 token,指针随消费向前移动。它证明 init rc 并非交给 shell 执行,所谓 bash 标记只是为了展示 rc 的脚本式语法。
一行结束后,section 关键字由 parse_new_section 分发:
// 代码位置 /system/core/init/init_parser.cpp 第 357—381 行// 分发一个新的 sectionstatic void parse_new_section(struct parse_state* state, int kw, int nargs, char** args) {// 根据关键字类型选择解析器 switch (kw) {// service 进入服务解析器 case K_service:// 解析 service 头部,返回该节上下文 state->context = parse_service(state, nargs, args);// 上下文非空时切换后续行解析回调 if (state->context) {// 之后的行交给 service 专用解析器 state->parse_line = parse_line_service; return;// 结束上下文分支 } break;// on 进入 action 解析器 case K_on:// 解析 action 头部,返回该节上下文 state->context = parse_action(state, nargs, args);// 上下文非空时切换后续行解析回调 if (state->context) {// 之后的行交给 action 专用解析器 state->parse_line = parse_line_action; return;// 结束上下文分支 } break;// import 进入导入解析器 case K_import: parse_import(state, nargs, args); break;// 结束分支选择 }// 兜底:未匹配任何 section 时忽略后续行 state->parse_line = parse_line_no_op;// 结束函数}keywords.h 的 KEYWORD 宏还会把命令名映射到 do_xxx 函数。关键点在于 state->parse_line 这个函数指针:parse_new_section 完成头部解析后把它从通用的 parse_line_no_op 切到 parse_line_service 或 parse_line_action,后续的缩进行才会被路由到刚才建立的节点上,而不是被当作游离文本丢弃。解析器因而同时保留了语法上下文与执行入口。
service 的落表过程最能说明“文本变结构体”的含义:
// 代码位置 /system/core/init/init_parser.cpp 第 728—760 行// 解析 service 头部static void* parse_service(struct parse_state* state, int nargs, char** args) {// 按服务名查重 service* svc = (service*) service_find_by_name(args[1]);// 重名时报告错误并忽略定义 if (svc) return 0;// 去掉 service 关键字与服务名两个参数 nargs -= 2;// 为结构体及参数指针分配连续空间 svc = (service*) calloc(1, sizeof(*svc) + sizeof(char*) * nargs);// 复制服务名,摆脱输入缓冲区生命周期 svc->name = strdup(args[1]);// 设置默认 class svc->classname = "default";// 复制可执行文件与参数指针 memcpy(svc->args, args + 2, sizeof(char*) * nargs);// 在末尾补空指针 svc->args[nargs] = 0;// 保存参数数量 svc->nargs = nargs;// 为 onrestart 触发器申请一个节点 trigger* cur_trigger = (trigger*) calloc(1, sizeof(*cur_trigger));// 把该触发器命名为 onrestart cur_trigger->name = "onrestart";// 初始化 onrestart trigger 链表 list_init(&svc->onrestart.triggers);// 把 onrestart trigger 节点挂进服务 list_add_tail(&svc->onrestart.triggers, &cur_trigger->nlist);// 初始化 onrestart command 链表 list_init(&svc->onrestart.commands);// 把服务挂到全局 service_list 尾部 list_add_tail(&service_list, &svc->slist);// 返回当前 section 的上下文 return svc;// 结束函数}名字、参数和后续 option 被聚合进 service 结构,再以新节点的身份挂到 service_list;state->context 切到该节点,让接下来的缩进行继续补充它。on 与 import 采用对应结构,三条逻辑链表由此建立。

六、把解析结果变成动作队列
解析完成后,action_for_each_trigger("early-init", action_add_queue_tail) 找到匹配 action 并入队,多个 queue_builtin_action 又把 coldboot、console、property trigger 等 C/C++ 内建动作包装成同一种队列节点。随后 init、late-init 等 trigger 依条件继续入队。
为什么解析完不直接执行?启动存在时序依赖,设备节点、属性与挂载点会在不同事件后就绪;队列还能让主循环每轮只执行一条命令,及时响应 fd 事件,也方便安排服务重启的时机。execute_one_command 最终调用 cur_command->func(cur_command->nargs, cur_command->args),统一执行 rc 命令与 builtin action。
七、启动解析的服务:从命令回调到进程
当 action 中出现 class_start main,这条命令经 keywords.h 的映射落到 do_class_start。这段代码要看的重点是遍历器与回调:
// 代码位置 /system/core/init/builtins.cpp 第 112—119 行// 定义 class_start 命令处理函数int do_class_start(int nargs, char** args) {// 遍历指定 class,并对每个服务调用启动过滤器 service_for_each_class(args[1], service_start_if_not_disabled);// 返回命令成功 return 0;// 结束函数}args[1] 是 class 名;遍历不产生新集合,状态变化落在回调触及的每个 service 上。过滤器处理显式禁用状态:
// 代码位置 /system/core/init/builtins.cpp 第 103—109 行// 接收 class 遍历到的一个服务static void service_start_if_not_disabled(struct service* svc) {// 仅启动未设置 SVC_DISABLED 的服务 if (!(svc->flags & SVC_DISABLED)) {// 进入通用服务启动函数 service_start(svc, NULL);// 显式禁用时进入另一分支 } else {// 记录 class 曾请求启动该服务 svc->flags |= SVC_DISABLED_START;// 结束条件分支 }// 结束函数}过滤通过后,service_start 才将配置变成进程。保留的核心段覆盖进程组、身份、SELinux context 和父进程状态:
// 代码位置 /system/core/init/init.cpp 第 187—399 行// 启动一个已解析的服务void service_start(struct service* svc, const char* dynamic_args) {// 创建子进程 pid_t pid = fork();// fork 返回 0 时位于子进程 if (pid == 0) {// 让服务成为新进程组组长 setpgid(0, getpid());// 服务配置了 gid 时进入设置 if (svc->gid) {// 设置主组,失败则退出子进程 if (setgid(svc->gid) != 0) _exit(127);// 结束 gid 分支 }// 服务配置了附加组时进入设置 if (svc->nr_supp_gids) {// 设置附加组,失败则退出子进程 if (setgroups(svc->nr_supp_gids, svc->supp_gids) != 0) _exit(127);// 结束附加组分支 }// 服务配置了 uid 时进入设置 if (svc->uid) {// 设置用户,失败则退出子进程 if (setuid(svc->uid) != 0) _exit(127);// 结束 uid 分支 }// 服务配置了 seclabel 时进入设置 if (svc->seclabel) {// SELinux 启用时设置 exec context,失败则退出 if (is_selinux_enabled() > 0 && setexeccon(svc->seclabel) < 0) _exit(127);// 结束 seclabel 分支 }// 用 rc 保存的程序和参数替换子进程映像 execve(svc->args[0], (char**) svc->args, (char**) ENV);// execve 失败才会走到退出 _exit(127);// 结束子进程分支 }// 父进程保存实际 PID svc->pid = pid;// 标记服务正在运行 svc->flags |= SVC_RUNNING;// 发布服务运行状态 svc->NotifyStateChange("running");// 结束函数}输入是完整 service 结构,子进程侧被 execve 替换为目标 ELF,父进程侧则记录 PID、设置 SVC_RUNNING 并发出状态变化。需要区分两套 SELinux 机制:子进程里的 setexeccon(svc->seclabel) 只对显式配置了 seclabel 的服务生效;
而 fork 前若服务没配 seclabel 且内核强制模式开启,init 会走 getcon/getfilecon/security_compute_create 为它动态求一个进程域,再给动态创建的 socket 打标签。完整源码还会创建 service socket、检查可执行文件、处理动态参数与错误;上面的最小段足以证明配置到进程的边界就在 fork+execve。
八、fork 与 exec:为什么分成两步
fork 复制调用进程的执行上下文,父进程得到子 PID,子进程得到 0;资源采用写时复制,读取时可共享物理页,写入后才复制。exec 用另一个可执行文件替换当前进程映像,成功时不会返回。两步组合让子进程在替换前完成 uid、gid、进程组、socket 与 SELinux 设置。

简单示例:
// 创建子进程int pid = fork();// 父子进程都会执行到这里,所以会打印两次printf("after fork\n");// 只有子进程满足 pid 等于 0if (pid == 0) {// 用目标程序替换子进程,成功后不返回 execl("/system/bin/app_process64", "app_process64", NULL);// 结束子进程分支}为什么打印两次?fork 返回后有两个执行流,它们从同一位置继续;父子各执行一次 printf。zygote 后续孵化应用也利用类似的继承特性:预加载虚拟机与 framework 资源后 fork,共享只读页面并以写时复制隔离修改,从而减少启动成本。
九、守护解析的服务:关键服务死后如何复活
Android 4.4/5.0 之后更强调以进程组清理 native service。让各服务互相监听并唤醒会形成复杂依赖,PID 1 已天然接收子进程退出信号,集中处理更可靠。初始化时建立的 socketpair 把异步 signal 转换成 epoll 可消费的 fd 事件:
// 代码位置 /system/core/init/signal_handler.cpp 第 164—185 行// 初始化子进程退出信号处理void signal_handler_init() {// 保存 socketpair 两端 int s[2];// 创建非阻塞且 exec 时关闭的 UNIX socketpair if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) == -1) {// 创建失败时终止 init exit(1);// 结束错误分支 }// 保存 signal handler 使用的写端 signal_write_fd = s[0];// 保存主循环监听的读端 signal_read_fd = s[1];// 声明信号动作 struct sigaction act;// 清零动作结构 memset(&act, 0, sizeof(act));// 注册 SIGCHLD_handler act.sa_handler = SIGCHLD_handler;// 忽略子进程暂停,仅关心退出 act.sa_flags = SA_NOCLDSTOP;// 安装 SIGCHLD 动作 sigaction(SIGCHLD, &act, 0);// 回收初始化前可能已经退出的子进程 reap_any_outstanding_children();// 把读端及处理函数注册到 epoll register_epoll_handler(signal_read_fd, handle_signal);// 结束函数}它把内核发来的 SIGCHLD 转成一对 fd、一个 signal action 和一项 epoll 注册。写端给 signal handler,读端归主循环,职责被明确隔开。
往 socket 写一个字符 1 有什么意义?这个字符不编码 PID,也不携带退出原因,只负责唤醒 epoll_wait。真正的信息由 waitpid 从内核读取:
// 代码位置 /system/core/init/signal_handler.cpp 第 158—162 行// 在异步信号上下文响应 SIGCHLDstatic void SIGCHLD_handler(int) {// 写一个字节,只用于唤醒事件循环 TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1));// 结束信号处理函数} // 代码位置 /system/core/init/signal_handler.cpp 第 150—155 行// 在正常主循环上下文处理可读事件static void handle_signal() {// 准备接收唤醒字节 char buf[32];// 清空 socketpair 读端的待处理数据 read(signal_read_fd, buf, sizeof(buf));// 循环回收所有已退出子进程 reap_any_outstanding_children();// 结束事件处理函数}这样做把 signal handler 限制在简单、异步信号安全的 write 上,复杂链表和服务状态处理留在普通执行上下文。若多个子进程接连退出,一次读事件后循环 waitpid(-1, ..., WNOHANG) 便可批量回收。
最后看决策分支,它决定“复活”并非无条件行为:
// 代码位置 /system/core/init/signal_handler.cpp 第 53—143 行// 回收一个已退出的子进程static bool wait_for_one_process() {// 保存 waitpid 返回的退出状态 int status;// 非阻塞等待任意子进程 pid_t pid = TEMP_FAILURE_RETRY(waitpid(-1, &status, WNOHANG));// 没有子进程退出时直接返回循环 if (pid == 0) return false;// 出错时同样返回循环 if (pid == -1) return false;// 按 PID 查找对应服务 service* svc = service_find_by_pid(pid);// 该 PID 未被 init 跟踪时只回收退出状态 if (!svc) return true;// 非一次性服务、或被显式 restart 的服务需清理其进程组 if (!(svc->flags & SVC_ONESHOT) || (svc->flags & SVC_RESTART)) {// 向进程组发送 SIGKILL kill(-pid, SIGKILL);// 结束清理分支 }// 清空服务记录里的运行 PID 与运行标志 svc->pid = 0; svc->flags &= ~SVC_RUNNING;// SVC_ONESHOT 且没有显式 SVC_RESTART 时转为禁用 if ((svc->flags & SVC_ONESHOT) && !(svc->flags & SVC_RESTART)) svc->flags |= SVC_DISABLED;// 已禁用或 reset 的服务停在 stopped if (svc->flags & (SVC_DISABLED | SVC_RESET)) {// 通知 stopped 状态 svc->NotifyStateChange("stopped");// 本次回收处理完成 return true;// 结束停止分支 }// 取得当前单调时间用于崩溃窗口判断 time_t now = gettime();// critical 服务进入崩溃窗口统计 if ((svc->flags & SVC_CRITICAL) && !(svc->flags & SVC_RESTART)) {// 当前崩溃仍位于统计窗口时累加次数 if (svc->time_crashed + CRITICAL_CRASH_WINDOW >= now) {// 超过阈值时进入恢复模式并直接返回 if (++svc->nr_crashed > CRITICAL_CRASH_THRESHOLD) {// 请求重启到 recovery android_reboot(ANDROID_RB_RESTART2, 0, "recovery");// 进恢复模式后不再给该服务安排普通重启 return true;// 结束阈值分支 }// 崩溃落在窗口之外时重置计时与计数 } else {// 记录本次崩溃时间作为新窗口起点 svc->time_crashed = now;// 崩溃计数重新从 1 起算 svc->nr_crashed = 1;// 结束重置分支 }// 结束 critical 分支 }// 清除一次性 restart 请求 svc->flags &= ~SVC_RESTART;// 标记等待重启 svc->flags |= SVC_RESTARTING;// 发布 restarting 状态 svc->NotifyStateChange("restarting");// 本次回收处理完成 return true;// 结束函数}进程组清理(上面代码的 kill(-pid, SIGKILL))只针对非一次性服务或被显式 restart 的服务;此外完整实现还会 unlink init 为该服务创建的 socket,并处理 SVC_EXEC 这类一次性 exec 内建命令的收尾;SVC_CRITICAL 的崩溃窗口边界同样由 android_reboot 后立即 return 收口。状态由 SVC_RUNNING 转向 stopped 或 SVC_RESTARTING。真正重启由主循环里的 restart_processes() 在计划时间重新调用 service_start,复用 rc 中保存的命令,不需要另造一套启动方法。

zygote、servicemanager、surfaceflinger 承担应用孵化、Binder 服务注册和图形合成等基础职责,它们一旦异常退出,上层大量功能随之失效。因此相应 service 通常允许 init 重新拉起,并借助 onrestart 修复依赖关系;SVC_CRITICAL 则为持续崩溃设置系统级止损线。
十、总结与验证
init_parse_config_file→parse_config→next_token→parse_new_section把init.rc变成 service/on/import 链表;- trigger 与 builtin action 进入队列,
execute_one_command→do_class_start→service_start_if_not_disabled→service_start经fork+execve创建进程; - 主循环在执行命令与
restart_processes之外等待epoll,SIGCHLD通过socketpair唤醒它,waitpid回收子进程并依据SVC_ONESHOT、SVC_RESTART、SVC_DISABLED、SVC_CRITICAL等状态作决定。
可以在 Android 6.0 userdebug/eng 设备或模拟器上复现:
- 执行
adb shell ps -A -o USER,PID,PPID,NAME | grep -E 'init|zygote|servicemanager|surfaceflinger';旧版 toybox 不支持这些参数时用adb shell ps,再读/proc/<pid>/status的PPid。确认 native service 与 init 的父子关系,同时留意 zygote 派生的应用并非 init 直接子进程。 - 用
adb shell su 0 kill -9 $(pidof surfaceflinger)结束 surfaceflinger,紧接着重复pidof surfaceflinger。新 PID 出现即证明 init 完成了退出回收和重启。该操作会短暂打断显示,只应在测试设备执行。 - 运行
adb logcat -b all | grep -E 'init|surfaceflinger|restarting',对照Service ... killed、restarting、running等状态;若没有重启,检查 rc 中的disabled、oneshot、class 和 SELinux denial。 - 源码排查从
/system/core/init/init.cpp、init_parser.cpp、parser.cpp、builtins.cpp、signal_handler.cpp与/system/core/rootdir/init*.rc入手。本文结论限定于 Android 6.0.0_r1;新版本 init 已重构为 C++ 类与不同目录结构,机制相近,符号和文件位置无法直接套用。
本文由 AgentWorkFlow 生成