背景:云手机项目,Android 以容器方式运行在 Docker / K8S 之上,底座为 ARM64 服务器。本文从“容器里为什么不能靠 reboot 验证”出发,先给出 Android 用户空间的重启分层,再拆解 toolbox、init、ActivityManager 三套控制面,最后用一个 HAL 二进制的小包替换案例(以 sensors HAL 为例)走完整个验证流程。命令速查、代码路径和坑清单见文末附录。
说明:除非特别注明,本文的设备端命令默认在 root shell 下执行(
adb root后adb shell);am系列命令普通 shell 即可。
1. 问题:容器里的 reboot 不是重启,是回滚
在普通 Android 终端上,改一个 HAL 二进制、一个系统 so 或者一个 APK,最省事的验证流程是:
adb push <产物> <目标路径>
adb reboot
reboot 会把所有进程、服务、缓存清干净,起来之后一定是新代码。代价只是几十秒。
容器化 Android 打破了这个前提。云机实例的根文件系统来自镜像 + 可写层(Docker / K8S 底座的具体实现不同,但表现一致):实例重启后回到镜像基线,push 到 /system、/vendor 的文件不会保留。于是“重启”在容器里变成了两件事:
- 重启整机 / 重启容器:可能回到基线,等于把刚 push 的文件回滚掉,不能用来验证;
- 重启用户空间的某一层:进程重新加载磁盘上的新文件,push 的内容还在,这才是正确手段。
本文说的“小包调试”,就是:
只编译、只推送改动的那一个产物(HAL 二进制、native daemon、framework jar、APK),然后精确重启持有它的那一层,而不是刷整包、更不是重启整机。
要把这件事做对,先得建立一张映射:改了哪个文件 → 谁加载它 → 最小重启动作是什么。
1.1 五种重启粒度
| 粒度 | 影响范围 | 容器里是否可行 | 是否保留 push 的文件 |
|---|---|---|---|
| 整机 reboot | kernel + rootfs + 所有进程 | 可行,但会回滚 | 否 |
| 容器 / 实例重启 | Android rootfs + 所有进程 | 取决于可写层持久化策略 | 通常否 |
| Framework 软重启 | zygote、system_server、部分 native 服务 | 可行 | 是 |
| 单个 init service | 一个 HAL / daemon | 可行 | 是 |
| 单个 App | 一个应用进程 | 可行 | 是 |
真正需要掌握的是后三行:Framework、单个 service、单个 App。它们分别对应三套不同的控制面(见第 3 节)。
2. 方案总览:Android 用户空间的重启分层
先看一张分层图。容器里的 Android 用户空间,大致是下面这个依赖关系:
flowchart TB
K["kernel / kernel modules"] --> I["init (PID 1)"]
I --> N["native services<br/>netd / surfaceflinger / audioserver"]
I --> H["vendor HAL services<br/>sensors / camera / audio ..."]
I --> Z["zygote"]
Z --> S["system_server<br/>AMS / WMS / SensorService ..."]
S --> A["App processes"]
style K fill:#f5f5f5
style I fill:#e8f4ff
style H fill:#fff2cc
style N fill:#fff2cc
style Z fill:#d9ead3
style S fill:#d9ead3
style A fill:#fce5cd
这张图里,不同重启动作的边界非常清楚:
| 动作 | 实际影响 | 不影响 |
|---|---|---|
| 整机 reboot | kernel、init、native services、vendor HAL、zygote、system_server、App | — |
stop / start |
native services(netd / surfaceflinger / audioserver)、zygote、system_server、App(固定列表) |
vendor HAL、init、kernel |
am restart |
zygote、system_server、App | native services、vendor HAL、init、kernel |
stop/start <service> |
指定的那个 native service 或 vendor HAL | 其他所有层 |
am force-stop <pkg> |
指定的那个 App | 其他所有层 |
最关键的一条结论:HAL 不在 Framework 的重启范围内。 这是后面所有坑的根源(见 3.2 节和 3.4 节)。
把“改了什么”映射到“重启哪一层”,就是下面这张表:
| 改动产物 | 例子 | 谁加载它 | 最小重启动作 |
|---|---|---|---|
| APK | 普通应用、系统 App | zygote / App 进程 | am force-stop + am start |
| framework jar | services.jar、framework.jar |
system_server | am restart 或 stop / start |
| native daemon | netd、surfaceflinger |
init service | stop/start <service> |
| vendor HAL | sensors HAL、camera HAL | init service | stop/start <service> 或 ctl.restart;接口清单变化时先停 framework |
| kernel module | .ko |
kernel | 只能 reboot;容器里通常不可行 |
下面的章节,就是把这张表背后的三套控制面拆开。
3. 三套控制面:toolbox、init、ActivityManager
3.1 toolbox:一个二进制,六个身份
我们每天都在敲的 getprop、setprop、start、stop,在 AOSP 11 里其实是同一个 115 KB 的二进制——toolbox。
先看设备上的实拍(AOSP 11 云机实例实测):
generic:/ # which getevent
/system/bin/getevent
generic:/ # which getprop setprop
/system/bin/getprop
/system/bin/setprop
generic:/ # which start stop
/system/bin/start
/system/bin/stop
generic:/ # ls -lh /system/bin/* | grep toolbox
lrwxrwxrwx 1 root shell 7 2026-11-10 11:46 /system/bin/getevent -> toolbox
lrwxrwxrwx 1 root shell 7 2026-11-10 11:46 /system/bin/getprop -> toolbox
lrwxrwxrwx 1 root shell 7 2026-11-10 11:46 /system/bin/modprobe -> toolbox
lrwxrwxrwx 1 root shell 7 2026-11-10 11:46 /system/bin/setprop -> toolbox
lrwxrwxrwx 1 root shell 7 2026-11-10 11:46 /system/bin/start -> toolbox
lrwxrwxrwx 1 root shell 7 2026-11-10 11:46 /system/bin/stop -> toolbox
-rwxr-xr-x 1 root shell 115K 2026-10-29 14:42 /system/bin/toolbox
这是一个典型的 multicall 二进制,思路和 busybox 一样:所有工具编进同一个可执行文件,启动时看 argv[0] 决定跑哪个入口。
// system/core/toolbox/toolbox.c(节选)
int main(int argc, char** argv) {
char* cmd = strrchr(argv[0], '/');
char* name = cmd ? (cmd + 1) : argv[0];
for (size_t i = 0; tools[i].name; i++) {
if (!strcmp(tools[i].name, name)) {
return tools[i].func(argc, argv);
}
}
printf("%s: no such tool\n", argv[0]);
return 127;
}
这些软链接不是手工建的,而是构建规则生成的:
// system/core/toolbox/Android.bp
symlinks: [
"getevent",
"getprop",
"modprobe",
"setprop",
"start",
"stop",
],
在这棵 AOSP 11 里,toolbox 只包含 6 个工具:
| 工具 | 实现 | 作用 |
|---|---|---|
getprop |
getprop.cpp |
读系统属性 |
setprop |
setprop.cpp |
写系统属性,带名称、长度、UTF-8 校验 |
start / stop |
start.cpp |
通过 ctl.start / ctl.stop 控制 init 服务 |
getevent |
getevent.c |
读 /dev/input 事件,调试触摸、按键 |
modprobe |
modprobe.cpp |
加载内核模块 |
其他常见命令(ls、cat、ps、cp 等)由 toybox 提供;toolbox 只保留这几个系统控制/调试工具。不同 Android 版本 toolbox 的成员可能变化,本文以这棵 AOSP 11 为准。
两个直接推论:
- 这 6 个命令共享同一份代码和同一个二进制。真要替换
toolbox,它们会一起变。 start/stop不是独立程序,只是 toolbox 里的一层薄封装。它们的全部逻辑,就是往属性服务写ctl.start/ctl.stop。
3.2 start / stop:只重启“框架最小集合”
start.cpp 的核心只有两段:
// system/core/toolbox/start.cpp
static void ControlService(bool start, const std::string& service) {
if (!android::base::SetProperty(start ? "ctl.start" : "ctl.stop", service)) {
std::cerr << "Unable to " << (start ? "start" : "stop") << " service '" << service
<< "'\nSee dmesg for error reason." << std::endl;
exit(EXIT_FAILURE);
}
}
// system/core/toolbox/start.cpp(节选,省略了 zygote_secondary 的判断)
static void ControlDefaultServices(bool start) {
std::vector<std::string> services = {
"iorapd",
"netd",
"surfaceflinger",
"audioserver",
"zygote",
};
if (start) {
for (auto& service : services) {
ControlService(true, service);
}
} else {
for (auto it = services.crbegin(); it != services.crend(); ++it) {
ControlService(false, *it);
}
}
}
不带参数时,stop / start 只操作这个固定列表:
iorapd → netd → surfaceflinger → audioserver → zygote
顺序也是有讲究的:
start正序:先起底层依赖,netd、surfaceflinger、audioserver就绪,最后起zygote,框架才有一致的运行环境;stop逆序:先停zygote(system_server 和 App 一起下),再停audioserver、surfaceflinger、netd、iorapd。
这段代码解释了两个常见误解:
误解一:stop 会停掉所有服务。
不会。它只停这 5 个(多架构再加 zygote_secondary)。vendor HAL、hwservicemanager、servicemanager、vold、logd 都不会被停。
误解二:stop / start 能把新 push 的 HAL 加载进来。
不会。sensors HAL 是独立的 init service,不在默认列表里,stop / start 之后它还是原来那个进程,跑的仍然是旧二进制。
不过 start / stop 支持带参数,这就是“只重启一个服务”的入口:
stop vendor.sensors-hal-2-1-mock
start vendor.sensors-hal-2-1-mock
它的内部就是循环对每个参数写一次 ctl.stop / ctl.start。另外,start / stop 在写属性之前会检查 uid:
// system/core/toolbox/start.cpp
if (getuid()) {
std::cerr << "Must be root" << std::endl;
return EXIT_FAILURE;
}
所以设备上通常要先 adb root,或者直接在一个 root shell 里执行。
3.3 init:ctl.* 才是服务控制的真正入口
toolbox 只是“写属性的手”,真正干活的是 init。
init 在属性服务里注册了一组控制消息:
// system/core/init/init.cpp(节选,省略了 sigstop_on / oneshot_on 等其他控制消息)
static const std::map<std::string, ControlMessageFunction, std::less<>>& GetControlMessageMap() {
static const std::map<std::string, ControlMessageFunction, std::less<>> control_message_functions = {
{"start", DoControlStart},
{"stop", DoControlStop},
{"restart", DoControlRestart},
};
return control_message_functions;
}
因此下面三组命令是等价的:
stop vendor.sensors-hal-2-1-mock
setprop ctl.stop vendor.sensors-hal-2-1-mock
start vendor.sensors-hal-2-1-mock
setprop ctl.start vendor.sensors-hal-2-1-mock
setprop ctl.restart vendor.sensors-hal-2-1-mock
ctl.restart 没有直接对应的 toolbox 命令,但它就是 init 的 DoControlRestart:先停再起。
怎么知道一个服务的名字? 看它的 init rc:
# hardware/interfaces/sensors/2.1/default/android.hardware.sensors@2.1-service-mock.rc
service vendor.sensors-hal-2-1-mock /vendor/bin/hw/android.hardware.sensors@2.1-service.mock
interface android.hardware.sensors@2.0::ISensors default
interface android.hardware.sensors@2.1::ISensors default
class hal
user system
group system
rlimit rtprio 10 10
第一行的第一个字段 vendor.sensors-hal-2-1-mock 就是服务名,第二个字段才是二进制路径。服务名和二进制名经常不一样,stop/start 用的是服务名。
服务的运行状态可以直接读属性:
getprop init.svc.vendor.sensors-hal-2-1-mock
# running / stopped / restarting
3.4 ActivityManager:am restart 重启的是什么
am restart 是 framework 层的命令,帮助文本写得很直白:
restart
Restart the user-space system.
实现不在 init,而在 ActivityManagerService:
// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
@Override
public void restart() {
if (checkCallingPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires permission "
+ "android.Manifest.permission.SET_ACTIVITY_WATCHER");
}
Log.i(TAG, "Sending shutdown broadcast...");
BroadcastReceiver br = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
Log.i(TAG, "Shutting down activity manager...");
shutdown(10000);
Log.i(TAG, "Shutdown complete, restarting!");
killProcess(myPid());
System.exit(10);
}
};
Intent intent = new Intent(Intent.ACTION_SHUTDOWN);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(Intent.EXTRA_SHUTDOWN_USERSPACE_ONLY, true);
/* For now we are not doing a clean shutdown, because things seem to get unhappy.
mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, null, br, mHandler, 0, null);
*/
br.onReceive(mContext, intent);
}
注意 AOSP 11 里这段的细节:ACTION_SHUTDOWN 的有序广播被注释掉了,代码直接调用 br.onReceive(),然后 shutdown(10000)、杀掉 system_server、System.exit(10)。所以它是:
- 由 system_server 自己发起的 userspace-only shutdown;
- 杀掉 system_server 后,zygote / init 把 framework 重新拉起来;
- 不碰 kernel,不碰 vendor HAL,也不碰 netd / surfaceflinger / audioserver。
这也解释了为什么 am restart 比 stop / start “轻”:它只重启 zygote + system_server + App,native 服务保持原样。
3.5 三种方式对比
stop / start |
am restart |
ctl.restart <service> |
|
|---|---|---|---|
| 控制面 | init(toolbox 写 ctl.*) |
ActivityManager | init |
| 影响范围 | iorapd、netd、surfaceflinger、audioserver、zygote |
zygote、system_server、App | 指定的一个 init service |
| 是否碰 vendor HAL | 否 | 否 | 是(只碰指定的那个) |
| App 是否优雅退出 | 否 | 是(先走 AM shutdown) | 不涉及 |
| 需要 root | 是 | 否(shell 有 SET_ACTIVITY_WATCHER) |
是(写 ctl.*) |
| 依赖 system_server 存活 | 否 | 是 | 否 |
| 系统卡死时能否恢复 | 能 | 不能 | 能 |
| 典型用途 | 换 framework jar、整体重置框架状态 | 换 framework jar、干净重启 App 层 | 换单个 HAL / native daemon 二进制 |
4. 怎么选:从“改了哪个文件”到“重启哪一层”
把前面的内容串起来,可以得到一张决策图:
flowchart TD
A["改了某个产物"] --> B{"是 APK 吗?"}
B -->|是| B1["am force-stop <pkg><br/>am start ..."]
B -->|否| C{"是 framework jar 吗?"}
C -->|是| C1{"需要 App 优雅退出吗?"}
C1 -->|是| C2["am restart"]
C1 -->|否,或 system_server 已卡死| C3["stop / start"]
C -->|否| D{"是 native service / HAL 吗?"}
D -->|是| D1{"该服务的 SensorInfo<br/>或接口清单变了吗?"}
D1 -->|否| D2["ctl.restart <service>"]
D1 -->|是| D3["先停 framework<br/>再 stop/start 该服务"]
D -->|否| E["kernel / module<br/>只能 reboot,容器里通常不可行"]
其中“接口清单有没有变”是 HAL 场景最容易踩的坑。对 sensors HAL 来说,判断依据不是“代码改了多少”,而是 SensorInfo 有没有变:
// frameworks/native/services/sensorservice/SensorDevice.cpp
bool SensorDevice::sensorIsEquivalent(const sensor_t& prevSensor, const sensor_t& newSensor) {
bool equivalent = true;
if (prevSensor.handle != newSensor.handle ||
(strcmp(prevSensor.vendor, newSensor.vendor) != 0) ||
(strcmp(prevSensor.stringType, newSensor.stringType) != 0) ||
(strcmp(prevSensor.requiredPermission, newSensor.requiredPermission) != 0) ||
(prevSensor.version != newSensor.version) ||
(prevSensor.type != newSensor.type) ||
(std::abs(prevSensor.maxRange - newSensor.maxRange) > 0.001f) ||
(std::abs(prevSensor.resolution - newSensor.resolution) > 0.001f) ||
(std::abs(prevSensor.power - newSensor.power) > 0.001f) ||
(prevSensor.minDelay != newSensor.minDelay) ||
(prevSensor.fifoReservedEventCount != newSensor.fifoReservedEventCount) ||
(prevSensor.fifoMaxEventCount != newSensor.fifoMaxEventCount) ||
(prevSensor.maxDelay != newSensor.maxDelay) ||
(prevSensor.flags != newSensor.flags)) {
equivalent = false;
}
return equivalent;
}
只要 handle、type、vendor、typeAsString(stringType)、maxRange、resolution、power、minDelay、maxDelay、flags 里任何一项变了,sensorservice 就认为这是“另一套传感器”,不是“同一个服务换了个二进制”。sensors HAL 的具体 fatal 路径见 5.3 节。
5. 实战:换一个 bin(以 sensors HAL 为例)
小包调试的对象通常有三类:framework jar、native so、可执行 bin。它们的替换路径和重启边界不同。
5.1 jar / so / bin:先找加载者
| 产物 | 典型目标路径 | 谁加载它 | 替换前 | 替换后验证 |
|---|---|---|---|---|
| jar | /system/framework/*.jar |
system_server | stop / am restart |
pidof system_server、dumpsys、logcat、原功能 |
| so | /system/lib64、/vendor/lib64 |
某个进程 dlopen |
停掉加载它的进程 | /proc/<pid>/maps、原功能 |
| bin | /system/bin、/vendor/bin/hw |
init service(或 shell) | stop <service> |
init.svc、pidof、logcat、原功能 |
三点经验:
- jar:替换
/system/framework下的 jar 后,必须让 system_server 重新启动;services.jar这类核心 jar 建议stop/start,普通 system service 可以先试am restart。 - so:先确认谁加载了它。进程一旦 mmap 了旧 so,直接覆盖文件不会让它重新加载,必须先停掉加载者(App、native daemon 或 HAL)。
- bin:先看它是不是 init service。是的话用
stop/start <service>;不是的话(例如从 shell 临时启动的调试程序),直接杀进程再启动。
找某个 so 被哪些进程加载:
grep -l "libfoo.so" /proc/[0-9]*/maps
# 输出 /proc/<pid>/maps,再用 ps -A 查 pid 对应的进程
5.2 案例现场:一个 HAL 二进制
下面用 AOSP 自带的 sensors mock 服务作为 bin 的例子。jar / so 的替换思路相同,只是“谁加载它”和“重启哪一层”不同:
| 项 | 值 |
|---|---|
| 模块名 | android.hardware.sensors@2.1-service.mock |
| 设备路径 | /vendor/bin/hw/android.hardware.sensors@2.1-service.mock |
| init 服务名 | vendor.sensors-hal-2-1-mock |
| rc 文件 | /vendor/etc/init/android.hardware.sensors@2.1-service-mock.rc |
| 假设的变更 | 给 HAL 增加或删除一个对外条目,接口清单发生变化 |
先只编译这个模块:
# 主机端
m android.hardware.sensors@2.1-service.mock
产物在:
out/target/product/<product>/vendor/bin/hw/android.hardware.sensors@2.1-service.mock
5.3 为什么不能只重启 HAL
如果 Framework 还在运行,直接:
setprop ctl.restart vendor.sensors-hal-2-1-mock
会发生什么?
sensorservice 在初始化时给 HAL 注册了死亡回调:
// frameworks/native/services/sensorservice/SensorDevice.cpp
void SensorsHalDeathReceivier::serviceDied(
uint64_t /* cookie */,
const wp<::android::hidl::base::V1_0::IBase>& /* service */) {
ALOGW("Sensors HAL died, attempting to reconnect.");
SensorDevice::getInstance().prepareForReconnect();
}
HAL 重启后,sensorservice 会走 reconnect():重新连接 HIDL 服务、重新拉传感器列表,然后对比新旧列表:
// frameworks/native/services/sensorservice/SensorDevice.cpp
void SensorDevice::reconnect() {
Mutex::Autolock _l(mLock);
mSensors = nullptr;
auto previousActivations = mActivationCount;
auto previousSensorList = mSensorList;
mActivationCount.clear();
mSensorList.clear();
if (connectHidlServiceV2_0() == HalConnectionStatus::CONNECTED) {
initializeSensorList();
if (sensorHandlesChanged(previousSensorList, mSensorList)) {
LOG_ALWAYS_FATAL("Sensor handles changed, cannot re-enable sensors.");
} else {
reactivateSensors(previousActivations);
}
}
mReconnecting = false;
}
如果这次替换让新旧清单不一致,sensorHandlesChanged() 就会返回 true,于是:
LOG_ALWAYS_FATAL("Sensor handles changed, cannot re-enable sensors.");
LOG_ALWAYS_FATAL 会直接把 system_server 带崩。也就是说,在 Framework 运行时单独重启一个“接口清单变了”的 HAL,不是重启,是自杀。
sequenceDiagram
participant U as 操作者
participant I as init
participant H as sensors-HAL
participant S as sensorservice
U->>I: setprop ctl.restart vendor.sensors-hal-2-1-mock
I->>H: stop
H-->>S: serviceDied()
S->>S: prepareForReconnect()
I->>H: start (新二进制)
S->>H: reconnect / getSensorsList
H-->>S: 新的接口清单
S->>S: sensorHandlesChanged() == true
S-->>S: LOG_ALWAYS_FATAL<br/>system_server 崩溃
5.4 正确顺序:先下框架,再换 HAL,最后一起起来
安全的顺序是:
- 停 Framework(zygote / system_server / native services);
- 确认 system_server 已经退出;
- 停 sensors HAL;
- remount、push 新二进制、
chmod、sync; - 先启动 sensors HAL;
- 再启动 Framework。
这样 sensorservice 是在“全新启动”时第一次看到新的接口清单,不会走 reconnect() 的等价性检查。
主机端完整流程:
# 主机端
adb root
# 1. 停 Framework
adb shell stop
# 2. 等 system_server 真正退出
adb shell 'sleep 2; pidof system_server' # 预期输出为空
# 3. 停 HAL
adb shell stop vendor.sensors-hal-2-1-mock
adb shell getprop init.svc.vendor.sensors-hal-2-1-mock # 预期 stopped
# 4. 替换二进制
adb remount
adb push <out目录>/vendor/bin/hw/android.hardware.sensors@2.1-service.mock /vendor/bin/hw/
adb shell chmod 755 /vendor/bin/hw/android.hardware.sensors@2.1-service.mock
adb shell sync
# 5. 先起 HAL
adb shell start vendor.sensors-hal-2-1-mock
# 6. 再起 Framework
adb shell start
设备端等价命令(adb shell 内):
stop
sleep 2
pidof system_server
stop vendor.sensors-hal-2-1-mock
start vendor.sensors-hal-2-1-mock
start
两个细节:
- 先停 HAL 再 push。正在运行的二进制被覆盖时,
adb push可能报Text file busy(ETXTBSY)。先停进程,文件锁才会释放。 stop之后等 system_server 真的退出。stop返回不代表进程已经死透;如果 sensorservice 还活着时新 HAL 就起来了,仍然可能撞上reconnect()。
sequenceDiagram
participant U as 操作者
participant I as init
participant H as sensors-HAL
participant S as sensorservice
U->>I: stop (framework)
I->>S: 停 zygote → system_server 退出
U->>I: stop vendor.sensors-hal-2-1-mock
I->>H: stop
U->>H: 覆盖二进制 + chmod + sync
U->>I: start vendor.sensors-hal-2-1-mock
I->>H: start (新二进制)
U->>I: start (framework)
I->>S: 新建 sensorservice
S->>H: getSensorsList
H-->>S: 新的接口清单(首次连接,无旧列表可对比)
S->>S: 正常注册
5.5 验证
替换完成后,按“进程 → 服务 → 框架 → 业务”的顺序验证。
① HAL 进程是不是新起来的
getprop init.svc.vendor.sensors-hal-2-1-mock
# running
pidof android.hardware.sensors@2.1-service.mock
pidof 的值应该和替换前不同;如果没变,说明旧进程还在,新二进制没被加载。
② sensorservice 有没有看到新列表
dumpsys sensorservice
关注两点:
- 接口清单是否与预期一致(例如新增或删除了一个条目);
- 日志里是否出现
Sensor handles changed, cannot re-enable sensors.(期望:不出现)。
logcat -d | grep -i "Sensor handles changed"
预期不会出现上面那条 LOG_ALWAYS_FATAL。
③ 原有功能有没有回归
如果这次替换只改实现、不改对外行为,原有功能应保持不变;用原来的验证 App / 脚本跑一遍回归即可。
④ 故障态 vs 正确态
| 检查点 | 只重启 HAL(错误) | 先下框架再换 HAL(正确) |
|---|---|---|
| sensorservice | 走 reconnect() |
全新初始化 |
| 接口清单 | 与旧清单不同,判定为 changed | 首次读到新清单 |
| HAL 启动时机 | system_server 仍存活 | framework 已停止 |
| 结果 | LOG_ALWAYS_FATAL,system_server 崩溃 |
正常注册 |
| push 的文件 | 可能 ETXTBSY |
正常覆盖 |
6. 方法论:小包调试的五步判断
把上面的案例抽象一下,任何“只换一个小产物”的验证都可以按这五步走:
flowchart LR
A["1. 定位产物<br/>out/..."] --> B["2. 找加载者<br/>谁 mmap / exec 它"]
B --> C["3. 查重启边界<br/>进程 / service / framework"]
C --> D["4. 选最小动作"]
D --> E["5. 验证<br/>pid / init.svc / 日志 / 业务"]
第 1 步:定位产物。 先确认 m <module> 生成的到底是哪个文件,不要拿错 out 目录。
第 2 步:找加载者。 是 App 加载的 APK?是 system_server 加载的 jar?是 init exec 的 HAL?还是某个 native daemon dlopen 的 so?加载者决定了重启粒度。
第 3 步:查重启边界。 特别要确认:这个服务的“接口清单”有没有变。HAL 的 SensorInfo、Camera 的 device list、Audio 的 port config,都属于这一类。
第 4 步:选最小动作。 能只重启一个 service,就不要重启 framework;能 am restart,就不要 stop/start;能软重启,就不要 reboot。
第 5 步:验证。 至少看三样:
- 进程:
pidof/ps -A,确认新进程; - 服务:
getprop init.svc.<service>,确认running; - 业务:
dumpsys/logcat/ 原验证脚本,确认状态正确且没有 fatal。
6.1 坑清单
| 坑 | 现象 | 原因 | 规避 |
|---|---|---|---|
| 用 reboot 验证 | push 的文件消失 | 容器 rootfs 回到镜像基线 | 改用用户空间软重启 |
| 只重启 HAL | system_server 崩,日志 Sensor handles changed |
reconnect() 检测到接口清单变化 |
先停 framework,再换 HAL |
| 直接 push 运行中的二进制 | Text file busy |
进程还 mmap 着旧文件 | 先 stop 对应 service |
以为 stop 会停所有服务 |
HAL 还是旧进程 | stop 只停固定 5 个服务 |
单独 stop/start <service> |
以为 am restart 会重启 HAL |
新二进制没生效 | am restart 只重启 system_server / zygote |
单独重启 HAL |
stop 报 Must be root |
命令失败 | toolbox 检查 getuid() |
adb root 或用 root shell |
setprop ctl.* 失败 |
属性写不进去 | SELinux / property contexts 限制 | 看 dmesg,确认域权限 |
改完 HAL 直接 start framework |
旧 HAL 进程被复用 | HAL 没停,framework 只是重连 | 先停 HAL 再起 framework |
6.2 一条经验
“重启”要按“谁加载这个文件”来选,而不是按“哪个命令听起来更彻底”来选。
stop/start 听起来最彻底,但它只覆盖框架最小集合;am restart 听起来很重,但它不碰 native 服务和 HAL;ctl.restart 听起来最轻,但如果接口清单变了,它会直接把 sensorservice 送走。
附录
附录 A:命令速查
主机端:
# 只编译单个模块
m android.hardware.sensors@2.1-service.mock
# 推送前先停 Framework / HAL
adb root
adb shell stop
adb shell stop vendor.sensors-hal-2-1-mock
# 替换
adb remount
adb push <out目录>/vendor/bin/hw/android.hardware.sensors@2.1-service.mock /vendor/bin/hw/
adb shell chmod 755 /vendor/bin/hw/android.hardware.sensors@2.1-service.mock
adb shell sync
# 启动
adb shell start vendor.sensors-hal-2-1-mock
adb shell start
设备端:
# Framework 软重启
stop
start
# 只重启一个 init service
stop <service>
start <service>
setprop ctl.restart <service>
# 状态
getprop init.svc.<service>
pidof <binary-name>
dumpsys sensorservice
logcat -d | grep -i "Sensor handles changed"
附录 B:代码路径
| 内容 | 路径 |
|---|---|
| toolbox 多入口分发 | system/core/toolbox/toolbox.c |
| toolbox 构建与软链接 | system/core/toolbox/Android.bp |
start / stop 实现 |
system/core/toolbox/start.cpp |
setprop 实现 |
system/core/toolbox/setprop.cpp |
| init 控制消息 | system/core/init/init.cpp |
am restart 命令入口 |
frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java |
am restart 实现 |
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java |
| sensors HAL 死亡回调 | frameworks/native/services/sensorservice/SensorDevice.cpp |
| sensors 重连与列表对比 | frameworks/native/services/sensorservice/SensorDevice.cpp |
| sensors HAL 服务定义 | hardware/interfaces/sensors/2.1/default/android.hardware.sensors@2.1-service-mock.rc |
附录 C:经验清单
- 容器里 reboot 是回滚,不是重启;验证优先用用户空间软重启。
stop/start只覆盖固定列表,不等于“重启所有服务”。am restart只重启 system_server / zygote,不碰 native 服务和 HAL。- 换 HAL 前先确认接口清单有没有变;变了就必须先停 framework。
- push 运行中的二进制前先停进程,避免
ETXTBSY。 - 用
pidof的前后变化确认新二进制真的被加载,而不是只看init.svc显示running。 getprop init.svc.<service>是判断 init 服务状态最快的方式。ctl.restart是“停 + 起”,不是“平滑 reload”;有状态的服务要评估副作用。
附录 D:相关资源
- AOSP
system/core/toolbox/:toolbox、start、stop、getprop、setprop的实现 - AOSP
system/core/init/:init 控制消息与服务生命周期 - AOSP
frameworks/native/services/sensorservice/:sensorservice 与 HAL 的连接、重连 - AOSP
hardware/interfaces/sensors/:sensors HAL 接口与 mock 服务