SIGCHLD处理
本文面向已经读过 Epoll事件循环 和 Init主循环 的读者。前两篇说明了 signalfd 如何注册、Epoll::Wait() 为什么先执行 child-reap 回调;本文从一个子进程退出开始,追踪它如何由“内核中的僵尸”变成 Service 的 stopped 或 restarting 状态。
本文不把“收到 SIGCHLD”简化成一次 waitpid(),也不提前展开服务启动和 cgroup 创建。它要回答的是:为什么先用 waitid(WNOWAIT) 观察、为什么必须在真正 reap 前调用 Service::Reap()、谁负责杀残留进程组和删除 socket、exec 为什么会解除主循环阻塞、oneshot 与普通服务如何分流,以及 critical 服务究竟在第几次崩溃时触发保护。
读完后,读者应能从 Service::GetSigchldFd() 走到 ReapOneProcess() 和 Service::Reap(),根据 siginfo_t、flags 与属性判断退出结果,并用测试和设备日志区分“信号未消费”“进程已退出但状态未更新”“服务正在等待重启”三种现象。
1. 信号契约
1.1 Owner
SIGCHLD signalfd 的创建入口不是 InstallSignalFdHandler(),而是 Service::GetSigchldFd() 内的函数静态对象。第一次调用时,CreateSigchldFd() 要求进程只有一个线程,然后阻塞 SIGCHLD 并创建 SFD_CLOEXEC fd。
源码文件:system/core/init/service.cpp
static int GetSigchldFd() {
static int sigchld_fd = CreateSigchldFd().release();
return sigchld_fd;
}
unique_fd Service::CreateSigchldFd() {
CHECK_EQ(ThreadCount(), 1);
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &mask, nullptr) < 0) {
PLOG(FATAL) << "Failed to block SIGCHLD";
}
return unique_fd(signalfd(-1, &mask, SFD_CLOEXEC));
}“必须在线程创建前调用”是信号掩码的所有权约束。sigprocmask() 只改变调用线程;若先创建 property 等线程再阻塞,SIGCHLD 可能被其他线程按不同掩码接收,破坏统一由 init 主线程消费的模型。
1.2 子进程
init 自己阻塞 SIGCHLD,不代表它启动的服务也应该继承该状态。InstallSignalFdHandler() 注册 pthread_atfork 的 child hook;fork 后的子进程恢复默认 SIGCHLD action,并解除 SIGCHLD、SIGTERM 阻塞。
源码文件:system/core/init/init.cpp
static void UnblockSignals() {
const struct sigaction act{.sa_handler = SIG_DFL};
sigaction(SIGCHLD, &act, nullptr);
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigaddset(&mask, SIGTERM);
if (sigprocmask(SIG_UNBLOCK, &mask, nullptr) == -1) {
PLOG(FATAL) << "failed to unblock signals for PID " << getpid();
}
}这里的 consumer 是 fork 后的子进程环境,而不是 init 主循环。没有这个 hook,服务自身创建子进程时也会继承 SIGCHLD 阻塞,行为取决于它是否主动重设掩码。
1.3 Stop事件
init 为 SIGCHLD 设置默认 handler 和 SA_NOCLDSTOP,避免子进程 stop/continue 也产生需要主循环消费的 SIGCHLD。
源码文件:system/core/init/init.cpp
const struct sigaction act{
.sa_flags = SA_NOCLDSTOP,
.sa_handler = SIG_DFL,
};
sigaction(SIGCHLD, &act, nullptr);因此本文主线只处理退出、被信号终止等 WEXITED 结果,不把调试器造成的暂停和继续当成服务死亡。
2. SignalFD入口
2.1 注册
SIGCHLD fd 注册到 init 主线程 Epoll,监听 EPOLLIN | EPOLLPRI。如果注册失败,第二阶段直接 fatal,因为 PID 1 不能在没有可靠子进程回收路径时继续启动系统。
源码文件:system/core/init/init.cpp
static Result<void> RegisterSignalFd(Epoll* epoll, int signal, int fd) {
return epoll->RegisterHandler(
fd, [signal]() { HandleSignalFd(signal); },
EPOLLIN | EPOLLPRI);
}
Result<void> result = RegisterSignalFd(
epoll, SIGCHLD, Service::GetSigchldFd());
if (!result.ok()) {
PLOG(FATAL) << result.error();
}2.2 读取
fd 就绪后,HandleSignalFd(SIGCHLD) 读取一个完整的 signalfd_siginfo,再调用 ReapAnyOutstandingChildren()。
源码文件:system/core/init/init.cpp
static void HandleSignalFd(int signal) {
signalfd_siginfo siginfo;
const int signal_fd = signal == SIGCHLD
? Service::GetSigchldFd() : sigterm_fd;
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;
}
switch (siginfo.ssi_signo) {
case SIGCHLD:
ReapAnyOutstandingChildren();
break;
case SIGTERM:
HandleSigtermSignal(siginfo);
break;
default:
LOG(ERROR) << "signal_fd: received unexpected signal "
<< siginfo.ssi_signo;
break;
}
}signalfd 中的记录只负责唤醒和分类,退出状态的权威来源仍是后续 waitid() 得到的 siginfo_t。多个子进程接近同时退出时,代码不能假设一次 fd read 就等于一个待处理子进程,所以必须循环查询所有 zombie。
3. 回收时序
一次受 init 管理的 service 退出会经过下面的顺序。关键屏障是真正的 waitpid() 被延迟到 Service::Reap() 之后。
这不是多余的两次等待。waitid(WNOWAIT) 让 init 获得退出信息但保留 zombie PID;Service::Reap() 仍可能调用 kill(-pid, ...) 清理同一进程组,源码要求这段期间 PID 继续有效。最后 scope guard 才完成内核回收。
4. Zombie观察
4.1 WNOWAIT
ReapOneProcess() 使用 P_ALL 查找任意已退出子进程,WNOHANG 保证没有 zombie 时立即返回,WNOWAIT 保证只观察不消费。
源码文件:system/core/init/sigchld_handler.cpp
static pid_t ReapOneProcess() {
siginfo_t siginfo = {};
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;
}
DCHECK_EQ(siginfo.si_signo, SIGCHLD);
auto reaper = make_scope_guard([pid] {
TEMP_FAILURE_RETRY(waitpid(pid, nullptr, WNOHANG));
});scope guard 覆盖后续所有 return,包括 untracked process 和 temporary service 分支。即使日志写着 untracked process “will not be reaped”,从系统调用语义看,函数返回时 guard 仍调用 waitpid;这里的日志应理解为“不会走 Service::Reap() 管理路径”,不能据此断言 zombie 被保留。
4.2 全量循环
ReapAnyOutstandingChildren() 持续调用 ReapOneProcess(),直到返回零,并收集本轮处理过的 PID。
源码文件:system/core/init/sigchld_handler.cpp
std::set<pid_t> ReapAnyOutstandingChildren() {
std::set<pid_t> reaped_pids;
for (;;) {
const pid_t pid = ReapOneProcess();
if (pid <= 0) {
return reaped_pids;
}
reaped_pids.emplace(pid);
}
}返回集合由 shutdown 的 WaitToBeReaped() 用来从目标列表移除已经退出的 PID。主循环普通路径可以忽略集合,只依赖 Service 状态已经更新。
5. 服务定位
5.1 Subcontext
拿到 PID 后先调用 SubcontextChildReap(pid)。若 PID 属于 vendor subcontext,它由 subcontext 专用逻辑消费,不再查 ServiceList。
5.2 ServiceList
普通路径以 Service::pid 为投影在 ServiceList 中查找 owner。找到后,根据 flags 生成 exec 或 oneshot 持续时间日志;找不到时读取 /proc/<pid>/status 尽量记录进程信息。
源码文件:system/core/init/sigchld_handler.cpp
service = ServiceList::GetInstance().FindService(pid, &Service::pid);
if (service) {
name = StringPrintf("Service '%s' (pid %d)",
service->name().c_str(), pid);
if (service->flags() & SVC_EXEC) {
auto duration = boot_clock::now() - service->time_started();
wait_string = StringPrintf(" waiting took %f seconds", ...);
} else if (service->flags() & SVC_ONESHOT) {
auto duration = boot_clock::now() - service->time_started();
wait_string = StringPrintf(
" oneshot service took %f seconds in background", ...);
}
}这一步建立了内核 PID 到 init 配置对象的映射。只有找到 Service,退出结果才会进入 socket 清理、restart、critical 和 init.svc.* 属性等管理逻辑。
5.3 退出分类
siginfo.si_code == CLD_EXITED 表示正常调用 _exit/返回,si_status 是退出码;其他 code 下 si_status 表示导致状态变化的信号。init 分别输出 exited with status 或 received <signal>。
“正常退出”在这里仅指 CLD_EXITED,而 Service::Reap() 的 was_last_exit_ok_ 还要求状态码为零。退出码非零和被信号杀死都会进入失败语义。
6. 清理顺序
6.1 进程组
Service::Reap() 首先决定是否杀整个进程组。非 oneshot 或显式 restart 的服务直接 SIGKILL;oneshot 在 vendor Android 版本 R 及以上也杀进程组,以清理主进程退出后留下的后代。
源码文件:system/core/init/service.cpp
if (!(flags_ & SVC_ONESHOT) || (flags_ & SVC_RESTART)) {
KillProcessGroup(SIGKILL);
} else if (SelinuxGetVendorAndroidVersion() >= __ANDROID_API_R__) {
KillProcessGroup(SIGKILL);
}这正是 zombie PID 必须暂时保留的原因:KillProcessGroup() 使用 service PID 标识进程组。提前 waitpid 会缩短 PID 有效期,并可能破坏后续依赖。
6.2 Socket
随后删除服务启动时创建的非持久 Unix socket;标记 persist 的 socket 保留。
源码文件:system/core/init/service.cpp
for (const auto& socket : sockets_) {
if (socket.persist) continue;
auto path = ANDROID_SOCKET_DIR "/" + socket.name;
unlink(path.c_str());
}这里清理的是文件系统路径。fd 是否仍被其他进程持有是另一层生命周期;unlink 阻止新的客户端按路径连接,不会撤销已经建立的连接。
6.3 Callback
reap_callbacks_ 在服务 flags 大范围更新之前执行,并获得原始 siginfo_t。ExecWithFunctionOnFailure() 就用这个机制在临时命令异常退出时调用失败处理函数。
源码文件:system/core/init/service.cpp
for (const auto& f : reap_callbacks_) {
f(siginfo);
}
if ((siginfo.si_code != CLD_EXITED || siginfo.si_status != 0) &&
on_failure_reboot_target_) {
LOG(ERROR) << "Service " << name_
<< " has 'reboot_on_failure' option and failed";
trigger_shutdown(*on_failure_reboot_target_);
}reboot_on_failure 只在异常退出或非零退出码时触发,并设置 shutdown 状态;它不会在 Service::Reap() 内同步完成整个重启流程。
6.4 Exec解除
带 SVC_EXEC 的服务调用 UnSetExec(),同时清除全局 is_exec_service_running_ 和对象 flag。下一轮 init 主循环不再因 exec 等待而抑制普通 action。
源码文件:system/core/init/service.h
void UnSetExec() {
is_exec_service_running_ = false;
flags_ &= ~SVC_EXEC;
}这是 exec 的完成屏障:命令退出、reap callback 执行完、失败关机请求已记录之后,才允许后续 action 继续。
7. 状态分流
7.1 Temporary
temporary service 在解除 exec 等清理后立即从 Service::Reap() 返回,不走普通服务的 PID 清零、属性和 restart 分支。回到 ReapOneProcess() 后,带 SVC_TEMPORARY 的对象从 ServiceList 删除。
源码文件:system/core/init/sigchld_handler.cpp
service->Reap(siginfo);
if (service->flags() & SVC_TEMPORARY) {
ServiceList::GetInstance().RemoveService(*service);
}因此 exec 创建的临时对象没有 init.svc.<name> 状态属性;NotifyStateChange() 也明确跳过 temporary service。
7.2 Oneshot
普通非 temporary service 先清 PID、SVC_RUNNING 和 start order,并记录退出是否成功。oneshot 若不是手动 restart/reset,则增加 SVC_DISABLED,随后发布 stopped 状态并返回。
源码文件:system/core/init/service.cpp
相关函数/类型:Service::Reap
pid_ = 0;
flags_ &= ~SVC_RUNNING;
start_order_ = 0;
was_last_exit_ok_ = siginfo.si_code == CLD_EXITED && siginfo.si_status == 0;
if ((flags_ & SVC_ONESHOT) &&
!(flags_ & SVC_RESTART) && !(flags_ & SVC_RESET)) {
flags_ |= SVC_DISABLED;
}
if (flags_ & (SVC_DISABLED | SVC_RESET)) {
NotifyStateChange("stopped");
return;
}oneshot 不是“完全不清理”,而是完成公共清理后不进入自动重启。若通过控制消息临时关闭 oneshot flag,同一个服务退出后就可能进入 restarting。
7.3 Restarting
可自动重启的服务最终清除 SVC_RESTART、设置 SVC_RESTARTING,同步执行所有 onrestart 命令,并发布 restarting 属性。
源码文件:system/core/init/service.cpp
flags_ &= ~SVC_RESTART;
flags_ |= SVC_RESTARTING;
onrestart_.ExecuteAllCommands();
NotifyStateChange("restarting");onrestart_ 在当前 init 主线程调用栈中执行,不经过 ActionManager 排队。慢 onrestart 命令会延长本次 reap;对应的自动 Service::Start() 则由主循环后续 HandleProcessActions() 按 restart period 执行。
当前 Service::Reap() 只负责把对象变成“等待重启”状态。截止时间到达后,Service::Start() 会先通过 ResetFlagsForStart() 清除 SVC_RESTARTING;若后续路径展开、文件检查或 fork 失败,主循环记录错误,但对象不会自动保留在 restarting 队列中。部分失败还会设置 SVC_DISABLED。因此恢复依赖新的显式 start、class 操作或相应上层逻辑,不能假设每个启动错误都会周期性重试。
8. Critical窗口
8.1 生效条件
critical 计数只处理异常退出,且排除显式 SVC_RESTART。默认 crash window 是 4 分钟;boot 未完成时,不受窗口过期限制。
源码文件:system/core/init/service.cpp
if (((flags_ & SVC_CRITICAL) || is_process_updatable) &&
!(flags_ & SVC_RESTART) && !was_last_exit_ok_) {
bool boot_completed = GetBoolProperty("sys.boot_completed", false);
if (now < time_crashed_ + fatal_crash_window_ || !boot_completed) {
if (++crash_count_ > 4) {
// critical或updatable处理
}
} else {
time_crashed_ = now;
crash_count_ = 1;
}
}条件是 ++crash_count_ > 4,所以第五次符合条件的异常退出进入保护分支,不是第四次。若 boot 已完成且上一次窗口已过期,本次成为新窗口的第一次崩溃。
8.2 Critical服务
critical 服务可通过 init.svc_debug.no_fatal.<name>=true 跳过 fatal,供测试使用。否则还要检查 24 小时节流属性;近期已经因 critical crash 发起过重启时,只记录日志,不重复触发。
源码文件:system/core/init/service.cpp
if (flags_ & SVC_CRITICAL) {
if (!GetBoolProperty("init.svc_debug.no_fatal." + name_, false)) {
uint64_t epoch_time =
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
if (epoch_time - GetIntProperty(
"persist.init.svc.last_fatal_reboot_epoch", 0) > throttle_window) {
SetProperty("persist.init.svc.last_fatal_reboot_epoch",
std::to_string(epoch_time));
SetFatalRebootTarget(fatal_reboot_target_);
LOG(FATAL) << "critical process '" << name_ << "' exited 4 times";
}
}
}日志文本中的 “exited 4 times” 与条件表达式并不一致;判断实际门槛必须以 ++crash_count_ > 4 为准。
8.3 Updatable服务
位于默认 mount namespace 且构建支持 APEX 更新的服务也进入崩溃计数,但超过门槛时不直接 fatal,而是设置:
sys.init.updatable_crashing_process_name=<service>
sys.init.updatable_crashing=1消费者是 update_verifier 和 apexd。critical 与 updatable 共用计数框架,但结果不同,不能把所有重复崩溃都描述为“init 重启设备”。
9. SIGTERM分支
不具备 reboot 能力的容器环境会额外阻塞并注册 SIGTERM signalfd。handler 只接受 ssi_pid == 0 的内核来源;用户态发送者被忽略。
源码文件:system/core/init/init.cpp
static void HandleSigtermSignal(const signalfd_siginfo& siginfo) {
if (siginfo.ssi_pid != 0) {
LOG(DEBUG) << "Ignoring SIGTERM from pid " << siginfo.ssi_pid;
return;
}
HandlePowerctlMessage("shutdown,container");
}这条路径属于容器 shutdown,不参与 Service::Reap()。普通设备具备 reboot 能力时不会创建该 SIGTERM fd。
10. 关闭等待
shutdown 期间 WaitToBeReaped() 接受目标 PID 列表和 timeout。它先乐观 reap 一次,再等待 SIGCHLD fd;若 epoll 创建、注册或等待失败,就每 50 ms 轮询。
源码文件:system/core/init/sigchld_handler.cpp
std::vector<pid_t> alive_pids(pids);
ReapAndRemove(alive_pids);
while (!alive_pids.empty() && t.duration() < timeout) {
if (sigchld_fd >= 0) {
auto result = epoll.Wait(std::max(timeout - t.duration(), 0ms));
if (result.ok()) {
ReapAndRemove(alive_pids);
continue;
}
}
std::this_thread::sleep_for(50ms);
ReapAndRemove(alive_pids);
}timeout 到期后函数不会伪装成功,而是读取仍存活 PID 的 /proc/<pid>/status 写日志。调用者 WaitAndLogViolations() 再根据 ServiceList 和进程组判断违规数量。
11. 回收测试
11.1 Parser测试
init_test.cpp 的 RejectsCriticalAndOneshotService 构造同时带 critical 和 oneshot 的 rc service。在 vendor Android 版本 R 及以上,解析后断言 parse_error_count() == 1。
它证明新版配置入口禁止自相矛盾的“退出即停”和“重复崩溃需保护”组合;它不直接执行 Service::Reap(),也不证明旧 vendor 版本采用相同校验。
11.2 Oneshot测试
oneshot_on_test.cpp 是设备测试。它先确认 bootanim 已 stopped,通过 ctl.oneshot_off 暂时关闭 oneshot 后启动,断言快速退出的 bootanim 进入 restarting;再恢复 ctl.oneshot_on 并启动,断言退出后回到 stopped。
输入是两个控制属性和同一个真实 service,关键断言是 init.svc.bootanim 的状态差异。它反向验证 Service::Reap() 的 flags 分流;测试需要 root 且依赖已完成启动的设备,不证明 temporary exec 的删除路径。
11.3 Exec构造测试
service_test.cpp 的 make_temporary_oneshot_service_* 系列向 MakeTemporaryOneshotService() 提供不同的 seclabel、uid、gid 和 supplementary gids,断言生成对象的字段与命令参数正确;无命令或 gid 过多则必须失败。
它证明进入 temporary reap 分支前对象确实携带 SVC_ONESHOT | SVC_TEMPORARY 的构造语义,但没有 fork 子进程,也没有断言 RemoveService()。
12. 退出定位
先用只读信息区分内核状态、init 管理状态和重启策略:
adb shell 'cat /proc/1/status | grep -E "SigBlk|SigPnd|ShdPnd"'
adb shell 'getprop | grep "^\[init.svc\."'
adb shell 'ps -A -o PID,PPID,STAT,NAME | grep " Z"'
adb shell 'logcat -b all -d -s init:I | grep -E "exited with status|received|restarting|waiting took"'SigBlk 只能证明 PID 1 的信号掩码,不能证明 signalfd handler 已注册;STAT=Z 是观察瞬间的 zombie,短暂出现不等于泄漏;init.svc.<name>=restarting 表示 Service::Reap() 已完成重启分流,不表示下一次 Start() 已成功。
源码阅读可以按退出状态逐层推进:
rg -n "CreateSigchldFd|InstallSignalFdHandler|HandleSignalFd" system/core/init
rg -n "ReapOneProcess|ReapAnyOutstandingChildren|WaitToBeReaped" system/core/init
rg -n "void Service::Reap|UnSetExec|fatal_crash_window" system/core/init面对“服务退出后没有拉起”的现象,先找 exited with status 或 received,再检查它是否为 temporary、oneshot、disabled/reset,随后看是否进入 init.svc.*=restarting,最后检查主循环的 restart deadline 与 Start() 错误。这个顺序对应真实 owner 和状态迁移,比直接把所有 SIGCHLD 问题归因于 waitpid 更准确。
本文没有展开 KillProcessGroup() 的 cgroup 实现、Service::Start() 的 clone/exec、subcontext 重建或 shutdown kill 阶段。后续服务专题会继续追踪 restarting 到新 PID 的路径;本文完成的是旧 PID 从退出通知到资源清理、状态决策和最终内核回收的闭环。
