Skip to content

SIGCHLD处理

追踪 SIGCHLD 从 signalfd 到 waitid、Service::Reap、waitpid 和服务状态恢复的完整路径。

基于android-17.0.0_r1
AndroidinitSIGCHLDsignalfdService

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

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

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

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

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

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

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

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

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

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

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

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

cpp
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

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

cpp
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

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

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

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,而是设置:

text
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

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

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 管理状态和重启策略:

sh
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() 已成功。

源码阅读可以按退出状态逐层推进:

sh
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 从退出通知到资源清理、状态决策和最终内核回收的闭环。