Android-重启流程源码解析

Android 重启流程源码解析:从 sys.powerctl 到内核 reboot

本文基于本地 init.cppreboot.cpp(Android R/11 前后的 system/core/init,含平台厂商定制),把 sys.powerctl 属性到达 init 之后 的完整流程逐一展开。这篇可以配合恢复出厂设置流程一起食用

阅读本文你将了解:

  • PropertyChanged / ShutdownState 如何安全地把关机命令从属性线程转交 init 主线程
  • HandlePowerctlMessage() 如何解析 reboot,xxx 命令、写 BCB、重排 action 队列
  • DoReboot() 从停服务到 reboot() 系统调用的完整关机序列(分阶段逐段分析)
  • 看门狗线程 RebootMonitorThread 如何保证"系统一定关得掉"
  • umount /data 为什么这么难:FUSE、zram、APEX、NFS 一个个拆
  • 一份从 logcat / lastkmsg 排查重启问题的关键日志时间线

目录

  1. 前言:回顾 framework 侧的第 1~7 步
  2. 命令如何安全地交给 init 主线程(init.cpp)
  3. HandlePowerctlMessage:解析与排队(reboot.cpp)
  4. DoReboot:真正的关机序列(reboot.cpp)
  5. 看门狗:RebootMonitorThread
  6. umount /data 的那些拦路虎
  7. 岔路:userspace reboot(可选阅读)
  8. 全流程时序总结
  9. 关键日志与调试技巧
  10. 总结

前言:回顾 framework 侧的第 1~7 步

先把整体链路串一遍(前 5 步在 framework 侧,只做简要回顾):

scss 复制代码
① PowerManager.reboot() → PMS.reboot()                    Binder 调用,检查 REBOOT 权限
② ShutdownThread.reboot()                                 确认对话框 / WakeLock / 进度条
③ ShutdownThread.run()                                    发 ACTION_SHUTDOWN 广播、
                                                           AMS/PM shutdown、关 radio
④ rebootOrShutdown() → PMS.lowLevelReboot(reason)
⑤ SystemProperties.set("sys.powerctl", "reboot," + reason) ← framework 的最后一步!
⑥ init 属性服务 HandlePropertySet()                        SELinux 权限检查 + 记录
                                                           "Received sys.powerctl=..." 日志
⑦ PropertyChanged() → ShutdownState → init 主循环          本篇从第 6 步末尾开始展开
        │
        ▼
   HandlePowerctlMessage()  →  DoReboot()  →  RebootSystem()  →  内核 reboot 系统调用

几个关键背景:

  • sys.powerctl 是一个 "虚"属性 :init 的属性服务(property_service.cpp)收到对它的 set 请求后,并不会把它写进属性存储区,只会走 NotifyPropertyChange() 通知出去------它本质上是一条发给 init 的命令,而不是一个状态;
  • 能写这个属性的进程受 SELinux 约束:framework 关机走的是 system_server,adb reboot 走的是 adbd,shell 域是没有权限的;
  • 属性服务在 init 的属性线程 里处理请求,而关机动作必须由 init 的主线程执行------这就引出了第 6~7 步里最核心的线程交接问题。

一、命令如何安全地交给 init 主线程(init.cpp)

1.1 PropertyChanged:sys.powerctl 特判(init.cpp:397)

属性服务的 NotifyPropertyChange() 最终走到 PropertyChanged()

scss 复制代码
void PropertyChanged(const std::string& name, const std::string& value) {   // init.cpp:397
    // If the property is sys.powerctl, we bypass the event queue and immediately handle it.
    // This is to ensure that init will always and immediately shutdown/reboot, regardless of
    // if there are other pending events to process or if init is waiting on an exec service or
    // waiting on a property.
    if (name == "sys.powerctl") {                                           // :404
        trigger_shutdown(value);                                            // :405
    }
    // (厂商定制:cdm./csd./vendor.cdm./vendor.csd. 属性 fork 出 sdprop 处理,
    //   见第十节,不影响本流程)
    ...
    if (property_triggers_enabled) {
        ActionManager::GetInstance().QueuePropertyChange(name, value);     // 普通属性:走事件队列
        WakeMainInitThread();
    }
    prop_waiter_state.CheckAndResetWait(name, value);
}

注意注释里的关键词:bypass the event queue(绕过事件队列) 。普通属性变化会被排进 action 队列慢慢处理,但 sys.powerctl 必须立即生效------哪怕 init 正在等待某个属性(wait_for_prop)、正在跑 exec 服务、队列里还堆着一大串 action,也必须优先关机

trigger_shutdown 是一个函数指针,在 SecondStageMain() 启动时被赋值(init.cpp:964):

c 复制代码
trigger_shutdown = [](const std::string& command) {
    shutdown_state.TriggerShutdown(command);          // init.cpp:964
};

1.2 为什么不能直接调 HandlePowerctlMessage?------ShutdownState(init.cpp:237)

c 复制代码
static class ShutdownState {
  public:
    void TriggerShutdown(const std::string& command) {                 // init.cpp:239
        // We can't call HandlePowerctlMessage() directly in this function,
        // because it modifies the contents of the action queue, which can cause the action queue
        // to get into a bad state if this function is called from a command being executed by the
        // action queue.  Instead we set this flag and ensure that shutdown happens before the next
        // command is run in the main init loop.
        auto lock = std::lock_guard{shutdown_command_lock_};
        shutdown_command_ = command;
        do_shutdown_ = true;
        WakeMainInitThread();
    }
​
    std::optional<std::string> CheckShutdown() {                       // init.cpp:251
        auto lock = std::lock_guard{shutdown_command_lock_};
        if (do_shutdown_ && !IsShuttingDown()) {
            do_shutdown_ = false;
            return shutdown_command_;
        }
        return {};
    }
    ...
} shutdown_state;

这段注释是整个交接机制的核心,翻译一下:

  • TriggerShutdown() 是在属性线程里被调的;
  • HandlePowerctlMessage()清空并重排 action 队列ClearQueue / QueueEventTrigger / QueueBuiltinAction);
  • 如果此刻主线程正在通过 action 队列执行命令(遍历队列的过程中队列被人改了),队列状态会被破坏;
  • 所以属性线程只做三件事 :存下命令、置 do_shutdown_ 标志、WakeMainInitThread() 唤醒主线程。真正的关机由主循环在下一条命令执行之前处理。

另外注意 CheckShutdown() 里的 !IsShuttingDown() 防护:一旦进入关机状态(EnterShutdown() 置位 shutting_down,见 2.5 节),再来一条 sys.powerctl 也不会重入

1.3 WakeMainInitThread:eventfd 唤醒(init.cpp:138)

scss 复制代码
static int wake_main_thread_fd = -1;                          // init.cpp:138
​
static void InstallInitNotifier(Epoll* epoll) {
    wake_main_thread_fd = eventfd(0, EFD_CLOEXEC);            // :140
    auto clear_eventfd = [] {
        uint64_t counter;
        TEMP_FAILURE_RETRY(read(wake_main_thread_fd, &counter, sizeof(counter)));
    };
    epoll->RegisterHandler(wake_main_thread_fd, clear_eventfd);
}
​
static void WakeMainInitThread() {                            // init.cpp:154
    uint64_t counter = 1;
    TEMP_FAILURE_RETRY(write(wake_main_thread_fd, &counter, sizeof(counter)));
}

init 主线程平时睡在 epoll 上;属性线程往 eventfd 写一下,主线程立刻被唤醒。注释里还讲了历史:早期用一个阻塞 socket 传属性变化消息,容易被填满导致死锁,后来才改成"锁 + eventfd 唤醒"的方式。

1.4 主循环取命令:CheckShutdown → HandlePowerctlMessage(init.cpp:1141)

init 主循环每轮第一件事就是检查有没有关机命令:

scss 复制代码
while (true) {                                                // init.cpp:1141
    ...
    auto shutdown_command = shutdown_state.CheckShutdown();   // :1148
    if (shutdown_command) {
        LOG(INFO) << "Got shutdown_command '" << *shutdown_command
                  << "' Calling HandlePowerctlMessage()";     // :1150 排查重启问题的重要日志!
        HandlePowerctlMessage(*shutdown_command);             // :1152 → reboot.cpp:1049
    }
​
    if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
        am.ExecuteOneCommand();                               // :1156 每轮只执行一条 action
        if (am.HasMoreCommands()) {
            next_action_time = boot_clock::now();
        }
    }
    if (!IsShuttingDown()) {                                  // :1165 关机期间不再调度服务重启
        auto next_process_action_time = HandleProcessActions();
        ...
    }
    ...
    auto epoll_result = epoll.Wait(epoll_timeout);            // :1179
    if (!IsShuttingDown()) {                                  // :1183 关机期间不再处理 ctl.* 消息
        HandleControlMessages();
        SetUsbController();
    }
}

三个细节值得注意:

  1. CheckShutdown() 放在 ExecuteOneCommand() 之前------保证关机命令"插队"成功,先于队列里任何待执行命令被处理(这正是 1.2 节注释承诺的 "ensure that shutdown happens before the next command is run");
  2. HandlePowerctlMessage() 本身并不执行关机 ,它只做解析和排队(见第二节),真正的动作在后续多轮循环的 ExecuteOneCommand() 里逐条执行,最后一条内建 action shutdown_done 才调 DoReboot()
  3. IsShuttingDown() 的两处防护:关机期间 HandleProcessActions()(服务重启调度、服务超时处理)和 HandleControlMessages()ctl.start/ctl.stop 等控制消息)都被跳过------防止"这边在杀服务、那边又把服务拉起来"的打架 。服务被杀后不重启,同样依赖 shutting_down 标志(IsShuttingDown(),reboot.cpp:1173)。

1.5 另一个入口:SIGTERM → shutdown,container(init.cpp:747)

arduino 复制代码
static void HandleSigtermSignal(const signalfd_siginfo& siginfo) {   // init.cpp:747
    if (siginfo.ssi_pid != 0) {
        // Drop any userspace SIGTERM requests.  用户态发来的 SIGTERM 直接忽略
        return;
    }
    // 只有内核发来的 SIGTERM(容器场景下 init 没有 CAP_SYS_BOOT)才触发关机
    HandlePowerctlMessage("shutdown,container");                     // :754
}

当 init 跑在容器里(IsRebootCapable() 为 false,init.cpp:802),收到内核 SIGTERM 时等价于 sys.powerctl=shutdown,container。这是 HandlePowerctlMessage 唯一不经过 ShutdownState 的调用点(因为它本来就在主线程的 epoll 回调上下文里)。


二、HandlePowerctlMessage:解析与排队(reboot.cpp:1049)

HandlePowerctlMessage() 是 reboot.cpp 的入口函数,只做四件事:解析命令、写 BCB、重排 action 队列、进入关机状态

2.1 命令格式与解析

c 复制代码
void HandlePowerctlMessage(const std::string& command) {       // reboot.cpp:1049
    unsigned int cmd = 0;
    std::vector<std::string> cmd_params = Split(command, ","); // 按','拆分
    std::string reboot_target = "";
    bool run_fsck = false;
    bool command_invalid = false;
    bool userspace_reboot = false;

命令格式与解析结果:

命令字符串 cmd reboot_target 特殊处理
reboot / reboot,userrequested ANDROID_RB_RESTART2 空 / userrequested
reboot,recovery ANDROID_RB_RESTART2 recovery 写 BCB:boot-recovery
reboot,bootloader ANDROID_RB_RESTART2 bootloader 写 BCB:bootloader
reboot,sideload ANDROID_RB_RESTART2 归一化为 recovery 写 BCB:--sideload
reboot,userspace - - 走 userspace reboot 分支(第七节)
shutdown,userrequested ANDROID_RB_POWEROFF - run_fsck = true
shutdown,thermal ANDROID_RB_THERMOFF - 立即关背光(降温优先)

shutdown 分支(reboot.cpp:1057):

ini 复制代码
if (cmd_params[0] == "shutdown") {
    cmd = ANDROID_RB_POWEROFF;
    if (cmd_params.size() >= 2) {
        if (cmd_params[1] == "userrequested") {
            // 用户主动关机:文件系统卸载成功后跑一次 fsck,保证下次开机干净
            run_fsck = true;
        } else if (cmd_params[1] == "thermal") {
            TurnOffBacklight();            // 热关机:立刻把热源(背光)关掉
            cmd = ANDROID_RB_THERMOFF;     // 且不跑 fsck,避免拖时间
        }
    }
}

注意 run_fsck 只在 shutdown,userrequested 时为 true:重启场景不跑 fsck(用户期望重启快),反正 ext4/f2fs 标脏了下次开机也能修。

2.2 特殊 reboot_target 与 BCB(misc 分区)

reboot 分支(reboot.cpp:1071)的核心是对 bootloader / recovery / sideload / quiescent 这些 target 写 BCB(Bootloader Control Block,misc 分区开头的 108 字节结构)

ini 复制代码
} else if (cmd_params[0] == "reboot") {
    cmd = ANDROID_RB_RESTART2;
    if (cmd_params.size() >= 2) {
        reboot_target = cmd_params[1];
        ...
        // 不支持动态分区的设备,adb reboot fastboot 落到 bootloader
        if (reboot_target == "bootloader") {
            std::string err;
            write_reboot_bootloader(&err);               // BCB.command = "bootloader"
        } else if (reboot_target == "recovery") {        // reboot.cpp:1094
            bootloader_message boot = {};
            read_bootloader_message(&boot, &err);
            // command 字段为空才写"boot-recovery",已存在则保留(别覆盖 OTA 写的 BCB!)
            if (!CommandIsPresent(&boot)) {
                strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
                write_bootloader_message(boot, &err);
            }
        } else if (reboot_target == "quiescent") {       // 静默重启:BCB 写 "boot-quiescent"
            ...
        } else if (reboot_target == "sideload" || ...) { // BCB 写 "--sideload" 等参数
            write_bootloader_message(options, &err);
            reboot_target = "recovery";                  // target 归一化成 recovery
        }
        // 第 3 段之后的参数原样追加,如 "reboot,recovery,quiescent"
        for (size_t i = 2; ...) reboot_target += "," + cmd_params[i];
    }
}

两个易忽略的细节:

  • CommandIsPresent()(reboot.cpp:1034) :先读 BCB,只有 command 字段为空才写入,非空(比如 OTA 升级已经写好了命令)则保留不动;顺带校验 command 里不能有不可打印字符,坏了就清零;
  • reboot_target 最终会作为 command 字符串传给内核的 reboot(RESTART2),所以像 userrequestedrecovery 这些字符串既出现在内核日志里("Restarting system with command 'xxx'"),也会在下次开机时被拼进 sys.boot.reason(配合 PersistRebootReason,见 4.2 节)。

2.3 重排 action 队列:清空 → shutdown 触发器 → shutdown_done

scss 复制代码
// We do not want to process any messages (queue'ing triggers, shutdown messages, control
// messages, etc) from properties during reboot.
StopSendingMessages();                                         // reboot.cpp:1152 属性线程停止投递
​
LOG(INFO) << "Clear action queue and start shutdown trigger";
ActionManager::GetInstance().ClearQueue();                     // :1160 清空整个 action 队列
// Queue shutdown trigger first
ActionManager::GetInstance().QueueEventTrigger("shutdown");    // :1162 触发 on shutdown
// Queue built-in shutdown_done
auto shutdown_handler = [cmd, command, reboot_target, run_fsck](const BuiltinArguments&) {
    DoReboot(cmd, command, reboot_target, run_fsck);           // :1165 真正的关机序列
    return Result<void>{};
};
ActionManager::GetInstance().QueueBuiltinAction(shutdown_handler, "shutdown_done");  // :1168
​
EnterShutdown();                                                // :1170

执行到这里的队列状态:

csharp 复制代码
action 队列(已清空后重新填充):
  [0] event trigger "shutdown"   ← 各 rc 文件里 on shutdown 段的动作(OEM/厂商收尾)
  [1] builtin "shutdown_done"    ← 最后一条,回调 = DoReboot()
  • on shutdown/system/etc/init/hw/init.rc、vendor/odm 的 rc 里都可以有 on shutdown 段,用来做设备级收尾(写某个 sysfs 节点、同步某块芯片状态等)。厂商在这里做的动作会先于 DoReboot 执行;
  • shutdown_done :内建 action,排在最后。主循环 ExecuteOneCommand() 一条一条跑,跑到它时进入 DoReboot()------从此不再返回

2.4 EnterShutdown:进入关机状态(reboot.cpp:824)

scss 复制代码
static void EnterShutdown() {          // reboot.cpp:824
    LOG(INFO) << "Entering shutdown mode";
    shutting_down = true;              // 全局标志(reboot.cpp:86)
    // Skip wait for prop if it is in progress
    ResetWaitForProp();                // 取消正在进行的 wait_for_prop(别让关机等属性)
    // Clear EXEC flag if there is one pending
    for (const auto& s : ServiceList::GetInstance()) {
        s->UnSetExec();                // 清除 service 的 pending exec 标志
    }
}

shutting_down 置位后的连锁反应(配合 1.4 节):

  • 主循环跳过 HandleProcessActions()服务死了不再自动重启
  • 主循环跳过 HandleControlMessages()ctl.start/ctl.stop 失效;
  • ShutdownState::CheckShutdown() 拒绝重入;
  • IsShuttingDown() 供 service.cpp / sigchld_handler 使用,杀服务时不会触发重启逻辑。

三、DoReboot:真正的关机序列(reboot.cpp:634)

DoReboot(cmd, reason, reboot_target, run_fsck) 是整个 init 侧关机的主函数,跑完为止,绝不返回 (最后 RebootSystem() + abort())。先给全局图,再分阶段展开。

bash 复制代码
DoReboot (reboot.cpp:634)
 │
 ├─0 超时预算 + 起看门狗线程 RebootMonitorThread          :636-669
 ├─1 PersistRebootReason 记录重启原因                     :671-680
 ├─2 /data 未挂载?→ sync + 直接 RebootSystem(快速路径) :682-688
 ├─3 服务分类:调试服务/watchdogd/动画/critical/待停       :690-720
 ├─4 关背光(POWEROFF/thermal 且无关机动画)               :717-720
 ├─5 关机动画 bootanim/surfaceflinger 处理                 :722-751
 ├─6 两段式停服务:SIGTERM(半超时) → SIGKILL              :753-762
 ├─7 vold 收尾:vdc abort_fuse / vdc shutdown → stop      :764-774
 ├─8 杀调试服务(logd/tombstoned/adbd/console)            :775-776
 │    ★ "logcat stopped here" ------ 此后只能去内核日志找输出
 ├─9 sync()                                               :778-783
 ├─10 KillZramBackingDevice                               :785
 ├─11 apexd --unmount-all                                 :787-791
 ├─12 KillNfsBackingDevice(厂商定制)                     :792
 ├─13 TryUmountAndFsck:umount /data 全家 + 可选 fsck      :793-794
 ├─14 再 sync() + LogShutdownTime                         :795-803
 ├─15 停看门狗线程                                         :805-807
 ├─16 F2FS_IOC_SHUTDOWN 让 /data 干净落地                 :809-819
 └─17 RebootSystem(cmd, reboot_target) → 内核             :820-821

3.1 阶段 0:超时预算与看门狗(reboot.cpp:636-669)

c 复制代码
static void DoReboot(unsigned int cmd, const std::string& reason,
                     const std::string& reboot_target, bool run_fsck) {   // reboot.cpp:634
    Timer t;
    LOG(INFO) << "Reboot start, reason: " << reason << ", reboot_target: " << reboot_target;
​
    bool is_thermal_shutdown = cmd == ANDROID_RB_THERMOFF;
​
    auto shutdown_timeout = 0ms;
    if (!SHUTDOWN_ZERO_TIMEOUT) {                          // util.h 常量,默认 false
        constexpr unsigned int shutdown_timeout_default = 6;        // 默认 6 秒
        constexpr unsigned int max_thermal_shutdown_timeout = 3;   // 热关机最多 3 秒
        auto shutdown_timeout_final = android::base::GetUintProperty(
                "ro.build.shutdown_timeout", shutdown_timeout_default);
        if (is_thermal_shutdown && shutdown_timeout_final > max_thermal_shutdown_timeout) {
            shutdown_timeout_final = max_thermal_shutdown_timeout;  // 热关机宁可不优雅也要快
        }
        shutdown_timeout = std::chrono::seconds(shutdown_timeout_final);
    }
    LOG(INFO) << "Shutdown timeout: " << shutdown_timeout.count() << " ms";

shutdown_timeout 是后面"优雅停服务 / umount 重试"的总预算(可用 ro.build.shutdown_timeout 调);SHUTDOWN_ZERO_TIMEOUT 为 true 时直接 0------一点不优雅,立刻硬来。

接着初始化信号量并起看门狗(详细分析见第五节):

scss 复制代码
    sem_t reboot_semaphore;
    if (sem_init(&reboot_semaphore, false, 0) == -1) {
        // 信号量创建失败(几乎不可能):放弃优雅流程,立即重启
        RebootSystem(cmd, reboot_target, reason);
    }
​
    LOG(INFO) << "Create reboot monitor thread.";
    bool reboot_monitor_run = true;
    std::thread reboot_monitor_thread(&RebootMonitorThread, cmd, reboot_target,
                                      &reboot_semaphore, shutdown_timeout, &reboot_monitor_run);
    reboot_monitor_thread.detach();
​
    sem_post(&reboot_semaphore);        // 给看门狗第一拍"心跳"

3.2 阶段 1:PersistRebootReason 记录重启原因(reboot.cpp:671-680)

c 复制代码
    // Ensure last reboot reason is reduced to canonical
    // alias reported in bootloader or system boot reason.
    size_t skip = 0;
    std::vector<std::string> reasons = Split(reason, ",");
    if (reasons.size() >= 2 && reasons[0] == "reboot" &&
        (reasons[1] == "recovery" || reasons[1] == "bootloader" || reasons[1] == "cold" ||
         reasons[1] == "hard" || reasons[1] == "warm")) {
        skip = strlen("reboot,");       // "reboot,recovery" → 只存 "recovery"
    }
    PersistRebootReason(reason.c_str() + skip, true);

PersistRebootReason()(reboot.cpp:100)把原因写两处:LAST_REBOOT_REASON_PROPERTY 属性和 LAST_REBOOT_REASON_FILE 文件(/data/misc/recovery/lastrebootreason,定义在 reboot_utils.h),并 fsync 落盘。下次开机时框架据此合成规范化的 sys.boot.reason(比如 reboot,userrequestedreboot,recovery)。

skip 的逻辑很讲究:bootloader 自己上报原因时会带上 reboot, 前缀,如果 init 这里存的是完整的 "reboot,recovery",拼出来就成 "reboot,reboot,recovery" 了------所以这些规范别名(recovery/bootloader/cold/hard/warm)要把前缀剥掉再存。

3.3 快速路径:/data 未挂载(reboot.cpp:682-688)

scss 复制代码
    // If /data isn't mounted then we can skip the extra reboot steps below,
    // since we don't need to worry about unmounting it.
    if (!IsDataMounted("*")) {          // IsDataMounted:遍历 /proc/mounts 找 /data
        sync();
        RebootSystem(cmd, reboot_target, reason);
        abort();
    }

没挂 /data(比如 charger 模式、早期 first stage、recovery 边界场景)就没有"保护 /data"的负担:sync 一下直接重启,后面所有阶段全省。

3.4 阶段 2:服务分类(reboot.cpp:690-720)

c 复制代码
    bool do_shutdown_animation = GetBoolProperty("ro.init.shutdown_animation", false);
    // watchdogd is a vendor specific component but should be alive to complete shutdown safely.
    const std::set<std::string> to_starts{"watchdogd"};     // 确保看门狗服务在跑!
    std::set<std::string> stop_first;                       // 其余服务:先停
    for (const auto& s : ServiceList::GetInstance()) {
        if (kDebuggingServices.count(s->name())) {          // {tombstoned, logd, adbd, console}
            // keep debugging tools until non critical ones are all gone.
            s->SetShutdownCritical();                       // 调试服务留到最后杀(日志保留最久)
        } else if (to_starts.count(s->name())) {
            if (auto result = s->Start(); !result.ok()) { ... }
            s->SetShutdownCritical();                       // watchdogd:没跑就拉起来,且不许停
        } else if (do_shutdown_animation) {
            continue;                                       // 有关机动画:动画类服务先不动
        } else if (s->IsShutdownCritical()) {
            if (auto result = s->Start(); !result.ok()) { ... }  // critical 服务没跑也拉起来
        } else {
            stop_first.insert(s->name());                   // 其余统统进入"先停"名单
        }
    }
​
    // POWEROFF / 热关机且没有关机动画:关背光(省电/降温)
    if (!do_shutdown_animation && (cmd == ANDROID_RB_POWEROFF || is_thermal_shutdown)) {
        TurnOffBacklight();                                 // 起 blank_screen 服务把屏幕抹黑
    }

分类逻辑一张表说清:

类别 判定 处理
调试服务 名字在 kDebuggingServices(reboot.cpp:88):tombstoned/logd/adbd/console 标 critical,最后
watchdogd to_starts 没跑就 Start,标 critical------它是"安全完成关机"的兜底
shutdown critical rc 里声明 shutdown critical 的服务 没跑也拉起来;不会被 stop_first 杀
关机动画类 ro.init.shutdown_animation=true 先保留,走动画流程
其余 默认 stop_first,第一批被停

为什么 logd 留到最后? 关机全程 init 的 LOG 都要往外打,logd 活得越久,logcat 里留下的关机日志越多;所以先杀业务服务,最后才轮到日志/调试设施(杀完它们 init 还在跑,但输出只剩 LOG_KLOG 直写的 /dev/kmsg 了)。

为什么 watchdogd 反而要确保在跑? 如果平台用了硬件 watchdog 且它由 watchdogd 喂狗,那关机序列卡住时 watchdog 超时复位是最后一道保险------不能自己把保险拆了。

3.5 阶段 3:关机动画(reboot.cpp:722-751)

scss 复制代码
    Service* boot_anim = ServiceList::GetInstance().FindService("bootanim");
    Service* surface_flinger = ServiceList::GetInstance().FindService("surfaceflinger");
    if (boot_anim != nullptr && surface_flinger != nullptr && surface_flinger->IsRunning()) {
        if (do_shutdown_animation) {
            SetProperty("service.bootanim.exit", "0");       // 控制动画不退出
            SetProperty("service.bootanim.progress", "0");   // 进度从 0 开始
            boot_anim->Stop();   // 可能正在放开机动画,停掉重启以切换到关机动画模式
        }
        for (const auto& service : ServiceList::GetInstance()) {
            if (service->classnames().count("animation") == 0) continue;
            if (do_shutdown_animation) service->Start();     // animation class 服务全部拉起
            service->SetShutdownCritical();                  // 全部标 critical,不许 stop_first 杀
        }
        if (do_shutdown_animation) {
            boot_anim->Start();
            surface_flinger->SetShutdownCritical();
            boot_anim->SetShutdownCritical();
        }
    }

思路:如果设备配了关机动画(ro.init.shutdown_animation),bootanim/surfaceflinger 和 animation class 的服务都要活到最后 ,通过 service.bootanim.exit/progress 属性向 bootanim 传递"关机模式 + 进度"信号(bootanim 轮询这两个属性绘制关机动画)。这套机制和开机动画共用一套代码,只是入口状态不同。

3.6 阶段 4:两段式停服务(reboot.cpp:753-762)

scss 复制代码
    // optional shutdown step
    // 1. terminate all services except shutdown critical ones. wait for delay to finish
    if (shutdown_timeout > 0ms) {
        StopServicesAndLogViolations(stop_first, shutdown_timeout / 2, true /* SIGTERM */);
    }
    // Send SIGKILL to ones that didn't terminate cleanly.
    StopServicesAndLogViolations(stop_first, 0ms, false /* SIGKILL */);   // 不再等
    SubcontextTerminate();      // 终止 subcontext 进程(vendor rc 命令的执行体)
    ReapAnyOutstandingChildren();   // 收尸

两段式 :先用一半的预算(默认 3s)发 SIGTERM 优雅终止------服务可以存数据、关文件、断连接;超时还没退的,第二轮直接 SIGKILL,杀完就收尸,一分钟都不多等。

StopServices()(reboot.cpp:546)按 services_in_shutdown_order()(ServiceList 的逆序 ,即后定义/后启动的先停)逐个 Terminate()/Stop(),并用 WaitToBeReaped() 在超时内等待 pid 被 reap。StopServicesAndLogViolations()(reboot.cpp:575)在此基础上多干一件事------把没退的服务点名:

scss 复制代码
LOG(ERROR) << "[service-misbehaving] : service '" << s->name() << "' is still running "
           << timeout.count() << "ms after receiving " << (terminate ? "SIGTERM" : "SIGKILL");

[service-misbehaving] 是关机慢问题最直接的线索:哪个服务上榜,哪个服务就要查为什么忽略 SIGTERM/赖着不死。

3.7 阶段 5:vold 收尾(reboot.cpp:764-774)

scss 复制代码
    // 3. send volume abort_fuse and volume shutdown to vold
    Service* vold_service = ServiceList::GetInstance().FindService("vold");
    if (vold_service != nullptr && vold_service->IsRunning()) {
        // Manually abort FUSE connections, since the FUSE daemon is already dead
        // at this point, and unmounting it might hang.
        CallVdc("volume", "abort_fuse");     // fork /system/bin/vdc volume abort_fuse
        CallVdc("volume", "shutdown");       // vold:卸载自己管理的所有卷
        vold_service->Stop();
    } else {
        LOG(INFO) << "vold not running, skipping vold shutdown";
    }

CallVdc()(reboot.cpp:204)fork 出 /system/bin/vdc(vold 的命令行客户端)执行命令。为什么 FUSE 要单独 abort_fuse?------vold 是 FUSE daemon,FUSE 挂载点(/storage/emulated 等)的请求全由它服务;此刻它马上要被停掉,如果直接 umount FUSE 挂载点,umount 可能在等一个永远没人应答的 FUSE 请求而挂死。所以先 abort 所有 FUSE 连接,再让 vold 正常 shutdown(它会把 emulated/adopted/private 卷都卸干净),最后 stop vold 本体。

这一步和 framework 侧 ShutdownThread "vold 由 init 负责" 的分工相呼应:ShutdownThread 只把用户态清干净,存储栈的收尾全部交给 init

3.8 阶段 6:杀调试服务(reboot.cpp:775-776)

arduino 复制代码
    // logcat stopped here
    StopServices(kDebuggingServices, 0ms, false /* SIGKILL */);

源码注释就一行 // logcat stopped here。logd/tombstoned/adbd/console 被一把 SIGKILL------从这行往后,logcat 里再也看不到新日志 。init 后续的 LOG 走 LOG_KLOG 直写 /dev/kmsg(内核日志缓冲区),要看重启前最后阶段的日志得去 lastkmsg / pstore 翻。这是调试重启问题时必须知道的分水岭。

3.9 阶段 7:sync + zram + apex + nfs(reboot.cpp:778-792)

scss 复制代码
    // 4. sync, try umount, and optionally run fsck for user shutdown
    {
        Timer sync_timer;
        LOG(INFO) << "sync() before umount...";       // 全文件系统刷盘
        sync();
    }
    // 5. drop caches and disable zram backing device, if exist
    KillZramBackingDevice();                          // 见第六节详解
​
    LOG(INFO) << "Ready to unmount apexes. So far shutdown sequence took " << t;
    // 6. unmount active apexes, otherwise they might prevent clean unmount of /data.
    if (auto ret = UnmountAllApexes(); !ret.ok()) {   // fork: apexd --unmount-all
        LOG(ERROR) << ret.error();
    }
    KillNfsBackingDevice();                           // 厂商定制:杀 nfsd/rpcbind/mountd
    UmountStat stat =
            TryUmountAndFsck(cmd, run_fsck, shutdown_timeout - t.duration(), &reboot_semaphore);

到这里 /data 的"层层包装"被逐层剥掉:

  1. sync():先把手脏页全落盘;
  2. KillZramBackingDevice():拆掉压在 /data 上的 swap(zram backing 是 /data 上的文件经 loop 设备映射的);
  3. UnmountAllApexes():APEX 都是从 /data 或 super 动态挂载的,不卸掉会卡住 /data 的 umount;
  4. (厂商定制)KillNfsBackingDevice():NFS 相关进程与挂载点清掉;
  5. TryUmountAndFsck():终于轮到 umount /data 本体(第六节详解)。

3.10 阶段 8:收尾(reboot.cpp:795-819)

scss 复制代码
    // Follow what linux shutdown is doing: one more sync with little bit delay
    {
        Timer sync_timer;
        LOG(INFO) << "sync() after umount...";
        sync();                       // umount 后再 sync 一次(学 Linux 原生 shutdown)
    }
    if (!is_thermal_shutdown) std::this_thread::sleep_for(100ms);   // 给底层一点缓冲
    LogShutdownTime(stat, &t);        // "powerctl_shutdown_time_ms:<耗时>:<umount状态>"
​
    // Send signal to terminate reboot monitor thread.
    reboot_monitor_run = false;       // 通知看门狗线程退出
    sem_post(&reboot_semaphore);
​
    // Reboot regardless of umount status. If umount fails, fsck after reboot will fix it.
    if (IsDataMounted("f2fs")) {      // /data 是 f2fs:让文件系统"优雅落地"
        uint32_t flag = F2FS_GOING_DOWN_FULLSYNC;
        unique_fd fd(TEMP_FAILURE_RETRY(open("/data", O_RDONLY)));
        int ret = ioctl(fd.get(), F2FS_IOC_SHUTDOWN, &flag);
        ...
    }
    RebootSystem(cmd, reboot_target, reason);   // ★ 最后一步,见下
    abort();                                    // 正常情况下执行不到这里
  • LogShutdownTime()(reboot.cpp:218)输出 powerctl_shutdown_time_ms:xxx:0,冒号后的数字是 UmountStat(见第六节表格)------统计关机耗时的标准出口
  • F2FS_IOC_SHUTDOWN(FULLSYNC):通知 f2fs 把 journal/脏数据全部落盘并把文件系统置为干净状态,避免下次开机被标 "unclean shutdown" 触发修复;ext4 设备在这个版本里没有对应处理(后续 AOSP 版本补了 EXT4_IOC_SHUTDOWN);
  • 注释再次强调兜底哲学:umount 失败也照样重启,脏盘交给下次开机的 fsck。

3.11 最后一步:RebootSystem() → 内核

RebootSystem() 定义在同目录的 reboot_utils.cpp

c 复制代码
void __attribute__((noreturn))
RebootSystem(unsigned int cmd, const std::string& rebootTarget) {
    LOG(INFO) << "Reboot ending, jumping to kernel";
    switch (cmd) {
        case ANDROID_RB_POWEROFF:
            reboot(RB_POWER_OFF);                       // 关机:内核走 power off 流程
            break;
        case ANDROID_RB_RESTART2:
            syscall(__NR_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2,
                    LINUX_REBOOT_CMD_RESTART2, rebootTarget.c_str());   // ★ 带命令串的重启
            break;
        case ANDROID_RB_THERMOFF:
            reboot(RB_POWER_OFF);
            break;
    }
    PLOG(ERROR) << "reboot call returned";   // 能走到这里说明 syscall 失败
    abort();                                  // 主动 crash,让 watchdog/pstore 留现场
}

内核侧接着发生的事(简述):sys_reboot 校验 magic 后打印 Restarting system with command 'userrequested',依次走过 reboot notifier 链、syscore 关停,最后 machine_restart() ------ 在 arm64 上通常是 PSCI 的 SYSTEM_RESET 调用(SMC 进 EL3/固件),由芯片级复位逻辑把 SoC 复位;之后 bootloader 起来,读 BCB/复位原因寄存器决定去哪(主系统 / recovery / fastboot),加载 kernel,init 重新开始。


四、看门狗:RebootMonitorThread(reboot.cpp:323)

关机流程里藏着一个独立线程,它的唯一使命是:DoReboot 卡死时把机器强制重启/关掉

c 复制代码
void RebootMonitorThread(unsigned int cmd, const std::string& reboot_target,
                         sem_t* reboot_semaphore, std::chrono::milliseconds shutdown_timeout,
                         bool* reboot_monitor_run) {                  // reboot.cpp:323
    unsigned int remaining_shutdown_time = 0;
​
    // 300 seconds more than the timeout passed to the thread as there is a final Umount pass
    // after the timeout is reached.
    constexpr unsigned int shutdown_watchdog_timeout_default = 300;  // 看门狗预算:300 秒
    auto shutdown_watchdog_timeout = android::base::GetUintProperty(
            "ro.build.shutdown.watchdog.timeout", shutdown_watchdog_timeout_default);
    remaining_shutdown_time = shutdown_watchdog_timeout + shutdown_timeout.count() / 1000;
​
    while (*reboot_monitor_run == true) {
        if (TEMP_FAILURE_RETRY(sem_wait(reboot_semaphore)) == -1) return;  // 等一次"心跳"
        ...
        shutdown_timeout_timespec.tv_sec += remaining_shutdown_time;       // 装填倒计时
        int sem_return = 0;
        while ((sem_return = sem_timedwait_monotonic_np(reboot_semaphore,
                                                        &shutdown_timeout_timespec)) == -1 &&
               errno == EINTR) { }                                         // 带超时地等下一次"心跳"
        if (sem_return == -1) {                    // 超时:迟迟没有心跳 → 判定 init 卡死
            LOG(ERROR) << "Reboot thread timed out";
            if (android::base::GetBoolProperty("ro.debuggable", false) == true) {
                LOG(INFO) << "Show stack for all active CPU:";
                WriteStringToFile("l", PROC_SYSRQ);        // sysrq l:全 CPU 栈回溯
                LOG(INFO) << "Show tasks that are in disk sleep...";
                WriteStringToFile("w", PROC_SYSRQ);        // sysrq w:D 状态任务
            }
            // 关机场景:紧急 sync + 全部 remount 只读,然后直接掉电
            if (cmd == ANDROID_RB_POWEROFF || cmd == ANDROID_RB_THERMOFF) {
                WriteStringToFile("s", PROC_SYSRQ);        // sysrq s:emergency sync
                WriteStringToFile("u", PROC_SYSRQ);        // sysrq u:remount read-only
                RebootSystem(cmd, reboot_target);
            }
            LOG(ERROR) << "Trigger crash at last!";
            WriteStringToFile("c", PROC_SYSRQ);            // sysrq c:主动 panic
        } else {
            // 收到心跳:计算剩余时间,回到循环开头重新等
            ...
        }
    }
}

工作机制(信号量 = 心跳):

  • DoReboot启动时 (:669)、fsck 前后 (:459/:468,因为 fsck 可能很久)、结束时 (:806-807)各 sem_post 一次;
  • 看门狗线程每次收到心跳就重新装填倒计时(默认 300s + shutdown_timeout ,可用 ro.build.shutdown.watchdog.timeout 调整);
  • 若倒计时耗尽仍无心跳 → 说明 init 主流程卡死在某个阶段,走超时路径。

超时路径的层层递进非常有节奏感:

顺序 动作 sysrq 键 目的
1 dump 全 CPU 栈 l debuggable 设备才做,留现场
2 dump D 状态任务 w 找卡在不可中断 IO 的进程
3 紧急 sync s 仅关机场景:尽最后努力刷盘
4 全部 remount 只读 u 尽最后努力保护文件系统
5 直接 RebootSystem - 关机场景到这为止
6 主动 panic c 重启场景:kernel panic → ramoops 留现场 + 硬件 watchdog 兜底复位

PROC_SYSRQ/proc/sysrq-trigger,往里写一个字符等价于触发对应的 Magic SysRq 功能。)

结论:Android 关机是有"三重保险"的------优雅流程(DoReboot)→ 软件看门狗(RebootMonitorThread 300s)→ 硬件看门狗(watchdogd/SoC WD)。用户按重启键后机器理论上不可能永远卡着不关(除非 EL3/固件级别的 bug)。


五、umount /data 的那些拦路虎

TryUmountAndFsck()(reboot.cpp:417)与 UmountPartitions()(reboot.cpp:281)是整个关机序列里最容易出问题、也最能体现工程功力的部分。

5.1 谁压在 /data 上?

重启前的挂载关系(典型设备):

bash 复制代码
/dev/block/by-name/userdata (块设备)
 └── /data (f2fs/ext4)
      ├── /mnt/user/0/emulated/0  ← FUSE(由 vold 代理,fsname 是 /data/media)
      ├── zram backing 文件 → /dev/block/loopX(swap 用)
      └── 各种 APEX 的 backing 数据 / libc 等

所以顺序必须是:杀进程 → abort FUSE → 拆 zram/loop → 卸 APEX → 才能卸 /data。少任何一步,umount /data 都会 EBUSY。

5.2 FindPartitionsToUmount:找出要卸的分区(reboot.cpp:240)

less 复制代码
// 扫 /proc/mounts:
if (MountEntry::IsBlockDevice(*mentry) && hasmntopt(mentry, "rw")) {
    // 块设备且 rw 的才需要处理;排除 "/" "/system" "/vendor" "/oem"
    // ------ 这些是 adb remount 后变 rw 的只读分区,关机 critical 服务还依赖它们,不动
    block_dev_partitions->emplace(block_dev_partitions->begin(), *mentry);
} else if (MountEntry::IsEmulatedDevice(*mentry)) {   // fsname 以 /data/ 开头 = FUSE 挂载
    emulated_partitions->emplace(emulated_partitions->begin(), *mentry);
}

5.3 UmountPartitions:带超时的重试循环(reboot.cpp:281)

c 复制代码
static UmountStat UmountPartitions(std::chrono::milliseconds timeout) {   // reboot.cpp:281
    Timer t;
    while (true) {
        std::vector<MountEntry> block_devices, emulated_devices;
        if (!FindPartitionsToUmount(&block_devices, &emulated_devices, false)) {
            return UMOUNT_STAT_ERROR;                       // /proc/mounts 都打不开
        }
        if (block_devices.size() == 0) {
            return UMOUNT_STAT_SUCCESS;                     // 没有可卸的了 → 成功!
        }
        bool unmount_done = true;
        if (emulated_devices.size() > 0) {                  // 先卸 FUSE
            for (auto& entry : emulated_devices) {
                if (!entry.Umount(false)) unmount_done = false;
            }
            if (unmount_done) sync();                       // FUSE 卸干净了就 sync 一次
        }
        for (auto& entry : block_devices) {                 // 再卸块设备(/data 等)
            if (!entry.Umount(timeout == 0ms)) unmount_done = false;  // 最后一轮用 MNT_FORCE
        }
        if (unmount_done) return UMOUNT_STAT_SUCCESS;
        if ((timeout < t.duration())) {
            return UMOUNT_STAT_TIMEOUT;                     // 超时了:先至少试过一轮
        }
        std::this_thread::sleep_for(100ms);                 // 100ms 后再试一轮
    }
}

算法概括:循环"扫 /proc/mounts → 卸 FUSE → sync → 卸块设备",全部卸光才算成功;失败就 100ms 一轮地重试,直到预算耗尽Umount(force)(reboot.cpp:139)内部是 umount2(dir, force ? MNT_FORCE : 0),并打印 Unmounting <fsname>:<dir> opts <opts> / Umounted ... / Cannot umount ... 三种日志。

5.4 TryUmountAndFsck:最后的疯狂与可选 fsck(reboot.cpp:417)

scss 复制代码
static UmountStat TryUmountAndFsck(unsigned int cmd, bool run_fsck,
                                   std::chrono::milliseconds timeout, sem_t* reboot_semaphore) {
    ...
    auto sm = snapshot::SnapshotManager::New();
    bool ota_update_in_progress = false;
    if (sm->IsUserspaceSnapshotUpdateInProgress()) {        // OTA 虚拟 A/B 合并进行中?
        LOG(INFO) << "OTA update in progress";
        ota_update_in_progress = true;
    }
    UmountStat stat = UmountPartitions(timeout - t.duration());
    if (stat != UMOUNT_STAT_SUCCESS) {
        LOG(INFO) << "umount timeout, last resort, kill all and try";
        if (DUMP_ON_UMOUNT_FAILURE) DumpUmountDebuggingInfo();   // lsof + sysrq l/w
        if (ota_update_in_progress) {
            return stat;    // ★ OTA 中:snapuserd 正在服务 IO,不能杀,直接认输返回
        }
        KillAllProcesses();                 // sysrq i:SIGKILL 所有进程(init 除外)
        UmountStat st = UmountPartitions(0ms);   // 全杀光后再试最后一轮(MNT_FORCE)
        ...
    }
​
    if (stat == UMOUNT_STAT_SUCCESS && run_fsck) {   // 仅用户主动关机跑 fsck
        LOG(INFO) << "Pause reboot monitor thread before fsck";
        sem_post(reboot_semaphore);        // fsck 可能很久:先给看门狗一拍心跳
        for (auto& entry : block_devices) {
            entry.DoFsck();                // f2fs → fsck.f2fs -a;ext4 → e2fsck -y
        }
        LOG(INFO) << "Resume reboot monitor thread after fsck";
        sem_post(reboot_semaphore);        // fsck 完:再来一拍
    }
    return stat;
}

三个要点:

  1. OTA 合并进行中不许杀进程:虚拟 A/B 的 snapuserd 在服务合并 IO,杀光进程那套"最后的疯狂"会造成 IO 错误,宁可带着 umount 失败的状态直接重启(fsck 下次开机修);
  2. umount 超时的最后手段KillAllProcesses()(reboot.cpp:318,sysrq i)------所有进程杀光后再用 MNT_FORCE 硬卸一轮,死马当活马医;
  3. fsck 前后给看门狗发心跳------fsck 一个大分区可能超过 300s,不发心跳的话看门狗会误判 init 卡死、直接把系统 panic 掉。这个细节和第四节呼应,读代码时非常容易被忽略。

DumpUmountDebuggingInfo()(reboot.cpp:267)在 umount 失败时(DUMP_ON_UMOUNT_FAILURE 控制,默认关)跑 lsof + 打印全部挂载点 + sysrq l/w------排查"谁占着 /data"的全套工具。

UmountStat 各值含义(reboot.cpp:116):

含义 对应 powerctl_shutdown_time_ms:...:N 尾号
UMOUNT_STAT_SUCCESS = 0 全部卸载成功 0
UMOUNT_STAT_SKIPPED = 1 未执行 umount 1
UMOUNT_STAT_TIMEOUT = 2 重试到超时仍失败 2
UMOUNT_STAT_ERROR = 3 无法执行(如 /proc/mounts 打不开) 3

5.5 KillZramBackingDevice:拆掉压在 /data 上的 swap(reboot.cpp:479)

zram(内存压缩 swap)可以配一个 backing device:把不活跃的页换出到 /data 上的一个文件(经 loop 设备映射)。不清掉它,/data 永远 umount 不掉:

scss 复制代码
static Result<void> KillZramBackingDevice() {      // reboot.cpp:479
    ...                                            // 读 /sys/block/zram0/initstate
    swapoff(ZRAM_DEVICE);                          // ① 先把 swap 停了
    WriteStringToFile("1", ZRAM_RESET);            // ② reset zram 设备
    if (!StartsWith(backing_dev, "/dev/block/loop")) return {};  // 非 loop 设备到此为止
    // ③ 清 loop 设备映射(backing 文件从此与 loop 脱钩)
    unique_fd loop(open(backing_dev.c_str(), O_RDWR | O_CLOEXEC));
    ioctl(loop.get(), LOOP_CLR_FD, 0);             // LOOP_CLR_FD
}

swapoff 可能耗时(要把换出的页读回来),所以有单独的 Timer 日志 swapoff() took ...


六、岔路:userspace reboot(可选阅读)

HandlePowerctlMessage()reboot,userspace 走的是完全不同的路(reboot.cpp:1075→1154):

ini 复制代码
if (reboot_target == "userspace") {
    userspace_reboot = true;                     // reboot.cpp:1075
}
...
if (userspace_reboot) {
    HandleUserspaceReboot();                     // reboot.cpp:1154
    return;
}

HandleUserspaceReboot()(reboot.cpp:998)不重启内核,只重造用户态:fork 一个看门狗进程(UserspaceRebootWatchdogThread,reboot.cpp:974:10s 内 sys.init.userspace_reboot.in_progress 没置 1、或 5 分钟内没 sys.boot_completed=1,就带着 userspace_failed,watchdog_triggered,... 的 reason 做脏硬重启 ),然后排队 userspace-reboot-requested 触发器和 userspace-reboot 内建 action。

DoUserspaceReboot()(reboot.cpp:847)的主干:只停 is_post_data() 的服务(SIGTERM 5s → SIGKILL 10s,违规服务记录到 /metadata/userspacereboot/services.txt)→ 拆 zram → vdc volume reset → 停调试服务 → apexd --unmount-all → 切回 bootstrap mount namespace → 删掉 APEX 定义的 action/service → 重新 enable 服务 → 触发 userspace-reboot-resume,从挂载 /data 之后的阶段重新把用户态拉起来。全程用 make_scope_guard 兜底:任何一步失败自动降级为硬重启trigger_shutdown("reboot,userspace_failed,shutdown_aborted,..."))。

对比项 正常硬重启(本文主线) userspace reboot
经过 bootloader / kernel
耗时 长(完整开机流程) 短(从 late-init 后段续跑)
主要用途 日常重启 Mainline/框架级更新后激活
失败兜底 硬件 watchdog scope guard → 自动转硬重启

七、全流程时序总结

从用户按下重启到 SoC 复位的完整时序:

scss 复制代码
用户/system_server                    init 属性线程                init 主线程
────────────────                     ──────────────              ──────────────
PMS.reboot()
 │
 ShutdownThread.run()
 │  ACTION_SHUTDOWN 广播
 │  AMS/PM/radio 收尾
 │
 PMS.lowLevelReboot()
 │
 SystemProperties.set(
   "sys.powerctl","reboot,xxx") ──► HandlePropertySet()
                                     │ SELinux 检查
                                     │ 日志 "Received sys.powerctl=..."
                                     │ (虚属性不落盘)
                                     │ NotifyPropertyChange()
                                     ▼
                                  PropertyChanged() ──────► TriggerShutdown()
                                   (init.cpp:397)  WakeMainInitThread() ──► 主循环被唤醒
                                                                                │
                                                              CheckShutdown() 取出命令
                                                                │
                                                       HandlePowerctlMessage("reboot,xxx")
                                                                │  解析 / 写 BCB
                                                                │  StopSendingMessages
                                                                │  ClearQueue
                                                                │  QueueEventTrigger("shutdown")
                                                                │  QueueBuiltinAction("shutdown_done")
                                                                │  EnterShutdown()
                                                                ▼
                                                       ExecuteOneCommand()×N
                                                                │  逐条跑 on shutdown 动作
                                                                ▼
                                                          DoReboot()  ★ 不再返回
                                                                │ 0 看门狗线程
                                                                │ 1 PersistRebootReason
                                                                │ 2 /data 检查
                                                                │ 3 服务分类
                                                                │ 4 SIGTERM → SIGKILL
                                                                │ 5 vdc abort_fuse/shutdown
                                                                │ 6 杀 logd/adbd ← logcat 到此为止
                                                                │ 7 sync → zram → apex → nfs
                                                                │ 8 umount /data(+可选 fsck)
                                                                │ 9 sync → F2FS_IOC_SHUTDOWN
                                                                ▼
                                                            RebootSystem()
                                                                │ syscall(__NR_reboot,
                                                                │   CMD_RESTART2, target)
                                                                ▼
内核 sys_reboot → kernel_restart("xxx")
 → "Restarting system with command 'xxx'"
 → reboot notifiers → machine_restart()
 → PSCI SYSTEM_RESET → SoC 复位
 → bootloader(读 BCB/复位原因)→ kernel → init 开机

八、关键日志与调试技巧

8.1 标志性日志时间线

一次正常重启,在 logcat(-b all)里应能看到:

日志 来源 说明
Received sys.powerctl='reboot,userrequested' from pid: xxxx (system_server) property_service 谁发起的重启,pid+cmdline 全在这
Got shutdown_command 'reboot,userrequested' Calling HandlePowerctlMessage() init.cpp:1150 命令到达主线程
Clear action queue and start shutdown trigger reboot.cpp:1159 开始重排队列
Entering shutdown mode reboot.cpp:825 shutting_down=true
Reboot start, reason: reboot,userrequested, reboot_target: reboot.cpp:637 DoReboot 入口
Shutdown timeout: 6000 ms reboot.cpp:652 本次的优雅预算
Create reboot monitor thread. reboot.cpp:662 看门狗已就位
Stopping N services by sending SIGTERM / ... SIGKILL reboot.cpp:548 两段式停服务
Calling /system/bin/vdc volume abort_fuse / volume shutdown reboot.cpp:205 vold 收尾
sync() before umount... reboot.cpp:780 logcat 大约到此为止
Unmounting /data/media:/mnt/user/... / Umounted ... reboot.cpp:140/143 FUSE/块设备卸载
sync() after umount... reboot.cpp:798
powerctl_shutdown_time_ms:<ms>:<stat> reboot.cpp:219 关机总耗时+umount 结果
Reboot ending, jumping to kernel reboot_utils.cpp init 的遗言

杀掉 logd 之后的日志只存在于内核日志缓冲 ,重启后看 /sys/fs/pstore/console-ramoops*(或老设备的 /proc/last_kmsg)。

8.2 排查套路

  1. 意外重启 :先查 sys.boot.reason(下次开机后),再去 pstore/lastkmsg 搜 Received sys.powerctl=------能直接定位发起进程;搜不到则怀疑内核 panic / 硬件 watchdog(找 ramoops 的 panic 记录);
  2. 重启慢/关机卡 :logcat 搜 [service-misbehaving](谁赖着不死)、UnmountingCannot umount(哪个分区卸不掉)、powerctl_shutdown_time_ms 的尾号(2=umount 超时);怀疑进程占着 /data 就开 DUMP_ON_UMOUNT_FAILURE 看 lsof 输出;
  3. 关机疑似卡死 :等 300s 看有没有 Reboot thread timed out------有,说明看门狗介入了,后面跟着的 sysrq dump(栈、D 状态任务)就是卡死现场;没有,就得怀疑 init 本身死了(查 pstore 里的 abort/panic 记录);
  4. 调参ro.build.shutdown_timeout(优雅预算,默认 6s)、ro.build.shutdown.watchdog.timeout(看门狗,默认 300s)、ro.init.shutdown_animation(关机动画开关)。

九、总结

  • 交接sys.powerctl 是 framework 递给 init 的接力棒。属性线程不直接执行关机(会破坏 action 队列),而是经 ShutdownState 存命令 + eventfd 唤醒,主循环在下一条命令前优先处理------HandlePowerctlMessage() 只解析和排队,真正的执行分散在后续每轮 ExecuteOneCommand() 里,最终汇合于 shutdown_doneDoReboot()
  • 队列 :关机时清空整个 action 队列,只留 on shutdown(OEM 收尾)和 shutdown_done(关机本体)两件事,EnterShutdown() 置位 shutting_down 后,服务不再重启、ctl 消息不再处理、命令不会重入;
  • 分层优雅:DoReboot 的顺序是一场"剥洋葱"------SIGTERM 半程 → SIGKILL → vold(先 abort FUSE 防挂死)→ 杀日志服务(logcat 的终点)→ sync → 拆 zram/loop → 卸 APEX → umount /data(100ms 一轮重试)→ 超时杀光所有进程再硬卸 → fsck(仅用户关机)→ F2FS 干净落地 → reboot 系统调用;
  • 兜底哲学:umount 失败照样重启(下次开机 fsck 修);优雅流程卡死有 300s 软件看门狗(sysrq dump → emergency sync → 主动 panic);再往下还有硬件 watchdog。"一定能关掉"比"关得完美"优先级更高;
  • 调试Received sys.powerctl=(谁发起)、[service-misbehaving](谁拖慢)、Cannot umount(谁占着)、powerctl_shutdown_time_ms(多久)、Reboot thread timed out(卡没卡)------五条日志基本覆盖重启关机问题的定位路径。

一句话总结:framework 负责"体面地送走用户态",init 负责"把家收拾干净再喊内核复位",看门狗负责"init 罢工时强行拉闸" ------三层协作,共同保证按下重启键后机器一定会回来。

相关推荐
清水白石00827 分钟前
Python 类型设计深度解析:TypedDict 能否替代 dataclass?从 JSON 数据边界到 API 设计的最佳实践
java·python·json
晊晌_h41 分钟前
嵌入式从0到精通——线程
java·开发语言·jvm
一木 之林1 小时前
五、C++ 新特性、关键字与编译原理(进阶)(二)
java·开发语言·c++
洋不写bug1 小时前
绕过权限检查,访问修改私有属性,反射,枚举,lambda
java·枚举·lambda·反射
evans在进步1 小时前
Java 常用设计模式入门:建造者、工厂、单例、外观与代理
java·python·设计模式
协议的旁观者1 小时前
Android 高级逆向实战(一):对抗 360 企业加固,Native 抽取还原与 Activity 生命周期重建
android
lisin-lee-cooper1 小时前
【leetcode658】有序数组找出k个最接近x的数
java·数据结构·算法
sunburn-1 小时前
Java堆(Heap)详解与实战教学
java·开发语言·数据结构·ide·算法
Kyrie_kk1 小时前
Java--TimeUnit时间单位枚举类(时间单位规范)
java·后端