C++调用链
本文面向已经能够用 rg 查找符号、知道 C++ 虚函数和智能指针基本语义,但还容易在 AOSP 的代理类、跨进程调用和异步输出之间迷路的读者。建议先读 ADB架构 以区分主机、adbd 和设备进程,再读 dumpsys诊断 了解命令输出的使用侧;你不需要先掌握 Binder 驱动,本文会把用户态可以由源码直接证明的边界补齐。
我们只回答一个可验证的问题:执行 dumpsys <service> [args...] 后,服务名、参数和输出文件描述符如何从命令行进入 Binder 服务,最终成为标准输出?沿途由谁持有服务对象,什么时候等待,发生服务缺失、事务失败或超时时又如何结束?
这不是 RefBase API 百科,也不展开 Binder 驱动协议、任意服务的内部状态或 SurfaceFlinger 架构。读完后,你应能从一个 C++ 可执行文件的 main 出发,区分接口、代理、服务端对象与资源所有者,并用测试反向检查自己画出的调用链。需要把同样方法迁移到 shell 控制命令时,可继续阅读 系统控制命令,观察另一组 wrapper 如何进入 Binder 服务。
1. 阅读坐标
大型 C++ 工程最先要解决的不是“读哪个类”,而是给每个对象标注进程、角色和所有权。IBinder 在源码里是统一接口,但运行时可能指向本地 BBinder,也可能指向远端代理 BpBinder;同一句 service->dump() 因对象类型不同,会走完全不同的实现。
| 对象 | 所在侧 | 角色 | 生命周期或资源 |
|---|---|---|---|
Dumpsys | 客户端进程 | 参数解析、调度、超时和输出复制 | 持有读端 fd 与工作线程 |
IServiceManager | 客户端接口 | 服务名查找 | 返回 sp<IBinder> |
BpBinder | 客户端进程 | 远端 binder handle 的代理 | sp<> 强引用保持代理可用 |
IPCThreadState | 客户端线程 | Parcel 到 Binder 事务的桥接 | 等待同步 reply |
BBinder | 服务进程 | 内建事务分发 | 从 Parcel 还原 fd 和参数 |
| 具体服务 | 服务进程 | 生产 dump 内容 | 向收到的 fd 写字节 |
writeDump | 客户端主线程 | 输出消费者 | poll、read、写 stdout |
这里先记住两个同名消歧:IBinder::dump() 是跨边界接口,具体对象决定它是代理封包还是服务实现;Dumpsys::writeDump() 不调用服务,它只消费管道中的结果。
2. 命令入口
2.1 参数状态
入口位于 frameworks/native/cmds/dumpsys/dumpsys.cpp 的 Dumpsys::main。参数解析产生的不是零散局部变量,而是后续控制流的状态:服务列表、传给服务的参数、超时以及是否等待服务。
源码文件:frameworks/native/cmds/dumpsys/dumpsys.cpp
int Dumpsys::main(int argc, char* const argv[]) {
Vector<String16> services;
Vector<String16> args;
bool waitForService = false;
bool asProto = false;
int dumpTypeFlags = 0;
int timeoutArgMs = 10000;
// ... getopt_long(..., "+t:T:lw", ...)
case 't':
timeoutArgMs = strtol(optarg, &endptr, 10) * 1000;
// ... validate
break;
case 'T':
timeoutArgMs = strtol(optarg, &endptr, 10);
// ... validate
break;
case 'w':
waitForService = true;
break;
}-t 的单位是秒,内部转为毫秒;-T 直接接收毫秒。默认超时为 10 秒。-w 只改变服务查找策略,不改变单次 dump 的输出超时。把这两种等待混为一谈,会错误地认为 dumpsys 超时等于 ServiceManager 等待超时。
第一个非选项参数是服务名,其余参数才进入 Vector<String16> args:
源码文件:frameworks/native/cmds/dumpsys/dumpsys.cpp
for (int i = optind; i < argc; i++) {
if (skipServices) {
skippedServices.add(String16(argv[i]));
} else if (i == optind) {
services.add(String16(argv[i]));
} else {
const String16 arg(argv[i]);
args.add(arg);
if (!asProto && !arg.compare(String16(PriorityDumper::PROTO_ARG))) {
asProto = true;
}
}
}因此 dumpsys activity -a 中 activity 由客户端解释,-a 由服务消费。阅读命令行工具时,应在解析循环里找出“工具自己的参数”和“透传给下游的参数”的分界,不能只看 usage 文本猜测。
2.2 服务选择
显式给出一个服务时,main 不需要先枚举全部服务。没有服务名或要求列表时,它才调用 listServices。真正启动 dump 前,waitForService 被折叠为一个明确策略:
源码文件:frameworks/native/cmds/dumpsys/dumpsys.cpp
auto serviceBehavior = waitForService
? ServiceBehavior::WAIT_UNTIL_STARTED
: ServiceBehavior::DUMP_IF_STARTED;
for (size_t i = 0; i < services.size(); i++) {
const String16& serviceName = services[i];
if (IsSkipped(skippedServices, serviceName)) continue;
if (startDumpThread(dumpTypeFlags, serviceName, args, serviceBehavior) == OK) {
// ... writeDump(...)
bool dumpComplete = (status == OK);
stopDumpThread(dumpComplete);
}
}这是调用链的第一处分流:默认行为是“存在才 dump”,不是“等待它启动”。只有用户传入 -w,startDumpThread 才使用 waitForService。
3. 服务查找
3.1 默认查找
startDumpThread 先取得 sp<IBinder>,再建立输出管道。服务缺失时,线程和管道都不会创建:
源码文件:frameworks/native/cmds/dumpsys/dumpsys.cpp
sp<IBinder> service;
if (serviceBehavior == ServiceBehavior::WAIT_UNTIL_STARTED) {
service = sm_->waitForService(serviceName);
} else {
service = sm_->checkService(serviceName);
}
if (service == nullptr) {
std::cerr << "Can't find service: " << serviceName << std::endl;
return NAME_NOT_FOUND;
}Android 17 的 C++ ServiceManager shim 中,checkService 调用统一 ServiceManager 的 checkService2,再从返回联合类型中取出 binder:
源码文件:frameworks/native/libs/binder/IServiceManager.cpp
sp<IBinder> CppBackendShim::checkService(const String16& name) const {
Service ret;
if (!mUnifiedServiceManager
->checkService2(String8(name).c_str(), &ret)
.isOk()) {
return nullptr;
}
return ret.get<Service::Tag::serviceWithMetadata>().service;
}这里的消费者是 Dumpsys::startDumpThread,它只关心空指针或可调用的 IBinder,不会解释服务注册元数据。阅读大型工程时,这种“适配层缩窄信息”的位置很重要:继续深入 checkService2 只有在研究注册、权限或元数据时才有必要,对当前输出主线已经足够。
3.2 等待边界
getService 和 waitForService 不是同一个实现。前者保留兼容性的轮询逻辑,最多约 5 秒;dumpsys -w 调用的则是 waitForService。因此不能拿 getService 的 5 秒常量解释 -w。
源码文件:frameworks/native/libs/binder/IServiceManager.cpp
sp<IBinder> CppBackendShim::getService(const String16& name) const {
if (sp<IBinder> svc = checkService(name); svc != nullptr) return svc;
constexpr auto timeout = 5s;
const auto startTime = std::chrono::steady_clock::now();
// ... choose retry interval
while (std::chrono::steady_clock::now() - startTime < timeout) {
usleep(1000 * sleepTime);
sp<IBinder> svc = checkService(name);
if (svc != nullptr) return svc;
}
return nullptr;
}这段代码在本文中的价值不是证明 dumpsys -w 的时长,而是提醒读者:接口名相近不代表调用方使用了它。必须从调用点反向确认动态路径,再研究候选实现。
4. 输出管道
4.1 资源所有者
服务对象取得后,客户端创建匿名管道。读端保存在 Dumpsys::redirectFd_,写端移动到工作线程的 lambda 捕获中:
源码文件:frameworks/native/cmds/dumpsys/dumpsys.cpp
int sfd[2];
if (pipe(sfd) != 0) {
std::cerr << "Failed to create pipe to dump service info for "
<< serviceName << ": " << strerror(errno) << std::endl;
return -errno;
}
redirectFd_ = unique_fd(sfd[0]);
unique_fd remote_end(sfd[1]);
activeThread_ = std::thread(
[=, remote_end{std::move(remote_end)}]() mutable {
if (dumpTypeFlags & TYPE_DUMP) {
status_t err = service->dump(remote_end.get(), args);
reportDumpError(serviceName, err, "dumping");
}
});lambda 按值捕获 service,因此工作线程持有自己的 sp<IBinder> 强引用;remote_end 是 move-only 的 unique_fd,线程结束时自动关闭。主线程拥有读端。这个所有权划分解释了为什么客户端可以一边等待输出,一边让阻塞的 Binder 调用停留在工作线程。
4.2 输出消费
主线程的 writeDump 用 deadline 计算剩余时间,poll 等待读端,再把每批字节写到目标 fd。对命令行调用而言,目标 fd 是 STDOUT_FILENO。
源码文件:frameworks/native/cmds/dumpsys/dumpsys.cpp
while (true) {
auto time_left_ms = [end]() {
auto now = std::chrono::steady_clock::now();
auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(end - now);
return std::max(diff.count(), 0LL);
};
int rc = TEMP_FAILURE_RETRY(poll(&pfd, 1, time_left_ms()));
if (rc < 0) {
status = -errno;
break;
} else if (rc == 0 || time_left_ms() == 0) {
status = TIMED_OUT;
break;
}
char buf[4096];
rc = TEMP_FAILURE_RETRY(read(redirectFd_.get(), buf, sizeof(buf)));
if (rc < 0) {
status = -errno;
break;
} else if (rc == 0) {
break; // EOF
}
if (!WriteFully(fd, buf, rc)) {
status = -errno;
break;
}
totalBytes += rc;
}这里存在三个不同结束条件:写端关闭产生 EOF,deadline 到达产生 TIMED_OUT,读写系统调用失败产生负 errno。只有 EOF 且此前没有错误,才表示客户端完整消费了服务输出。
5. Binder封包
5.1 代理实现
service 若是远端服务代理,虚调用进入 BpBinder::dump。它把 fd、参数数量和每个 String16 依次写入 Parcel,然后发送内建事务码 DUMP_TRANSACTION:
源码文件:frameworks/native/libs/binder/BpBinder.cpp
status_t BpBinder::dump(int fd, const Vector<String16>& args) {
Parcel send;
Parcel reply;
send.writeFileDescriptor(fd);
const size_t numArgs = args.size();
send.writeInt32(numArgs);
for (size_t i = 0; i < numArgs; i++) {
send.writeString16(args[i]);
}
return transact(DUMP_TRANSACTION, send, &reply);
}事务码定义在 frameworks/native/libs/binder/include/binder/IBinder.h:
源码文件:frameworks/native/libs/binder/include/binder/IBinder.h
enum {
FIRST_CALL_TRANSACTION = 0x00000001,
LAST_CALL_TRANSACTION = 0x00ffffff,
PING_TRANSACTION = B_PACK_CHARS('_', 'P', 'N', 'G'),
DUMP_TRANSACTION = B_PACK_CHARS('_', 'D', 'M', 'P'),
// ...
};
virtual status_t dump(int fd, const Vector<String16>& args) = 0;DUMP_TRANSACTION 属于 Binder 基础协议,不是某个 AIDL 服务自己的业务方法编号。这就是为什么不同 Binder 服务都能通过同一 dumpsys 入口接受 dump 请求。
5.2 fd语义
Parcel::writeFileDescriptor 不会把“一个整数”按普通字段跨进程复制。内核 Binder 模式下,它写入带 BINDER_TYPE_FD 的 flat binder object;RPC Binder 路径还会检查双方是否允许 fd transport。
源码文件:frameworks/native/libs/binder/Parcel.cpp
status_t Parcel::writeFileDescriptor(int fd, bool takeOwnership) {
if (auto* rpcFields = maybeRpcFields()) {
if (!mAllowFds) return FDS_NOT_ALLOWED;
switch (rpcFields->mSession->getFileDescriptorTransportMode()) {
case RpcSession::FileDescriptorTransportMode::NONE:
return FDS_NOT_ALLOWED;
case RpcSession::FileDescriptorTransportMode::UNIX:
case RpcSession::FileDescriptorTransportMode::TRUSTY:
// ... record transported fd
return OK;
}
}
// ... kernel Binder flat_binder_object path
}因此服务端拿到的是可写入同一管道对象的 fd,而不是客户端进程中的原始 fd 数值。本文只证明用户态如何声明和消费 fd 传递;具体驱动怎样安装目标进程 fd,不在这条源码主线中外推。
5.3 事务发送
BpBinder::transact 最终把 handle、事务码和 Parcel 交给 IPCThreadState::transact。这里强制加上 TF_ACCEPT_FDS,写入 BC_TRANSACTION,同步事务随后等待 reply:
源码文件:frameworks/native/libs/binder/IPCThreadState.cpp
status_t IPCThreadState::transact(int32_t handle, uint32_t code,
const Parcel& data, Parcel* reply,
uint32_t flags) {
flags |= TF_ACCEPT_FDS;
status_t err = writeTransactionData(
BC_TRANSACTION, flags, handle, code, data, nullptr);
if (err != NO_ERROR) {
if (reply) reply->setError(err);
return (mLastError = err);
}
if ((flags & TF_ONE_WAY) == 0) {
if (reply) {
err = waitForResponse(reply);
} else {
Parcel fakeReply;
err = waitForResponse(&fakeReply);
}
}
// ...
return err;
}dump 没有设置 one-way,所以工作线程不仅发送请求,还要等服务端事务返回。服务可以在返回前持续向 fd 写数据;主线程同时从另一端读取,避免管道缓冲区填满后双方互相等待。
6. 服务分发
6.1 内建事务
服务进程收到事务后,本地 binder 对象的 BBinder::transact 处理少量内建事务,其余进入 onTransact。DUMP_TRANSACTION 在基础类中直接解包:
源码文件:frameworks/native/libs/binder/Binder.cpp
status_t BBinder::onTransact(uint32_t code, const Parcel& data,
Parcel* reply, uint32_t /*flags*/) {
switch (code) {
case DUMP_TRANSACTION: {
int fd = data.readFileDescriptor();
int argc = data.readInt32();
Vector<String16> args;
for (int i = 0; i < argc && data.dataAvail() > 0; i++) {
args.add(data.readString16());
}
return dump(fd, args);
}
// ...
}
}封包与解包顺序必须完全对应:fd、argc、参数序列。阅读跨边界 C++ 代码时,应该把写端和读端并排核对;只读一侧容易漏掉类型、顺序和数量约束。
6.2 默认行为
BBinder 的默认实现不输出内容,只返回成功:
源码文件:frameworks/native/libs/binder/Binder.cpp
status_t BBinder::dump(int /*fd*/, const Vector<String16>& /*args*/) {
return NO_ERROR;
}因此不能声称“所有 Binder 服务都有可读 dump”。真正的内容由覆写 dump() 的具体服务生产;未覆写时,事务仍可能成功,但管道只会在调用结束、写端关闭后出现 EOF。判断一个服务输出什么,下一步应从它的继承层次查找最终 override,而不是继续钻研 dumpsys。
7. 引用生命期
7.1 强引用边界
sp<IBinder> 是这条链中的对象存活保证,但不能把它泛化成“所有 AOSP C++ 对象都由 RefBase 管理”。这里只讨论继承 IBinder/RefBase 的 binder 对象。
服务查找返回的 sp<IBinder> 先由 startDumpThread 局部变量持有,随后按值捕获进工作线程。即使 startDumpThread 返回,只要线程 lambda 还存在,代理对象仍有强引用。Parcel 在持有 binder object 时也会增加引用,释放对象时对称减少:
源码文件:frameworks/native/libs/binder/Parcel.cpp
static void acquire_object(const sp<ProcessState>& proc,
const flat_binder_object& obj,
const void* who, bool tagFds) {
switch (obj.hdr.type) {
case BINDER_TYPE_BINDER:
if (obj.binder) {
reinterpret_cast<IBinder*>(obj.cookie)->incStrong(who);
}
return;
case BINDER_TYPE_HANDLE: {
const sp<IBinder> b = proc->getStrongProxyForHandle(obj.handle);
if (b != nullptr) b->incStrong(who);
return;
}
// ...
}
}对应的 release_object 对本地 binder 和远端 handle 调用 decStrong。关键不是背诵计数函数,而是确认每次跨容器、跨线程或进入 Parcel 时,谁新增了一份所有权,退出时谁释放。
7.2 延迟释放
Binder 命令处理还会延迟部分 dereference。IPCThreadState::processPendingDerefs 明确先处理弱引用,再处理一个强引用,并循环到队列为空:
源码文件:frameworks/native/libs/binder/IPCThreadState.cpp
while (mPendingWeakDerefs.size() > 0 ||
mPendingStrongDerefs.size() > 0) {
while (mPendingWeakDerefs.size() > 0) {
RefBase::weakref_type* refs = mPendingWeakDerefs[0];
mPendingWeakDerefs.removeAt(0);
refs->decWeak(mProcess.get());
}
if (mPendingStrongDerefs.size() > 0) {
BBinder* obj = mPendingStrongDerefs[0];
mPendingStrongDerefs.removeAt(0);
obj->decStrong(mProcess.get());
}
}注释说明 decStrong() 可能触发析构,析构又可能发起事务并追加待处理引用。这是典型的大型 C++ 阅读点:释放不是一句“离开作用域就结束”,还要检查框架是否有延迟队列、析构回调或重入可能。
8. 失败与清理
8.1 失败分层
把所有失败都归为“dumpsys 失败”无法定位责任。应按第一个没有完成职责的 owner 分层:
服务缺失由 ServiceManager 查找暴露;pipe 创建失败属于客户端资源问题;Binder 事务失败由工作线程报告;输出读取、超时和 stdout 写入由主线程判断。服务自己的 dump() 返回错误时,错误信息写到 stderr,但 Dumpsys::main 最终仍按其循环结构继续处理其他服务并返回 0,这一点也不能仅凭内部 status_t 推断命令退出码。
8.2 超时语义
正常完成时主线程 join 工作线程;非正常完成时它 detach,随后关闭管道读端:
源码文件:frameworks/native/cmds/dumpsys/dumpsys.cpp
void Dumpsys::stopDumpThread(bool dumpComplete) {
if (dumpComplete) {
activeThread_.join();
} else {
activeThread_.detach();
}
redirectFd_.reset();
}这不是服务取消协议。客户端没有向服务发送“停止 dump”的事务,也没有终止服务进程。超时只表示主线程不再等待和复制输出;原工作线程可能仍阻塞在同步 Binder 调用,服务稍后写管道时还可能遇到读端已关闭。具体服务如何响应写失败,必须检查该服务实现,不能由 dumpsys.cpp 统一推断。
恢复动作也因此分层:服务未注册要检查注册和启动;Binder 事务错误要检查服务死亡、权限和进程状态;单个服务 dump 超时要定位该服务的锁、阻塞 I/O 或内部遍历;stdout 写失败则应检查调用方管道或重定向消费者。
9. Dumpsys测试
9.1 正常输出
frameworks/native/cmds/dumpsys/tests/dumpsys_test.cpp 用 mock ServiceManager 和 BinderMock 隔离客户端逻辑。fixture 先规定服务查找,再规定 dump 把字符串写入收到的 fd:
源码文件:frameworks/native/cmds/dumpsys/tests/dumpsys_test.cpp
void ExpectDump(const char* name, const std::string& output) {
sp<BinderMock> binder_mock = ExpectCheckService(name);
EXPECT_CALL(*binder_mock, dump(_, _))
.WillRepeatedly(DoAll(WithArg<0>(WriteOnFd(output)), Return(0)));
}
TEST_F(DumpsysTest, DumpRunningService) {
ExpectDump("Valet", "Here's your car");
CallMain({"Valet"});
AssertOutput("Here's your car");
}输入是服务名 Valet;动作是调用真实 Dumpsys::main;断言是 stdout 与 mock 写入完全一致。它证明客户端执行了 checkService → dump(fd,args) → pipe → stdout,但 mock dump 没有经过真实 BpBinder、Binder 驱动和 BBinder::onTransact,所以不能单独证明跨进程封包。
参数测试进一步断言 Vector<String16> 的内容,而不是只断言调用发生:
源码文件:frameworks/native/cmds/dumpsys/tests/dumpsys_test.cpp
TEST_F(DumpsysTest, DumpWithArgsRunningService) {
ExpectDumpWithArgs(
"SERVICE", {"Y", "U", "NO", "HANDLE", "ARGS"}, "I DO!");
CallMain({"SERVICE", "Y", "U", "NO", "HANDLE", "ARGS"});
AssertOutput("I DO!");
}它证明首个非选项参数和后续参数的分界以及参数顺序;不证明具体服务如何解释这些字符串。
9.2 超时输出
超时测试让 mock dump 睡眠 2 秒,而客户端 deadline 只有 1 秒:
源码文件:frameworks/native/cmds/dumpsys/tests/dumpsys_test.cpp
TEST_F(DumpsysTest, DumpRunningServiceTimeoutInSec) {
sp<BinderMock> binder_mock =
ExpectDumpAndHang("Valet", 2, "Here's your car");
CallMain({"-t", "1", "Valet"});
AssertOutputContains(
"SERVICE 'Valet' DUMP TIMEOUT (1000ms) EXPIRED");
AssertNotDumped("Here's your car");
Mock::AllowLeak(binder_mock.get());
}测试的断言是 deadline 先到时不会把迟到内容当作成功输出,并且源码确实采用 detached thread。AllowLeak 旁的 TODO 表明当前清理路径没有等待 mock 析构。这个测试不包含真实服务取消,因此不能据此推断服务会被终止。
10. 复现路径
先在 Android 17 源码树中建立一个只读调用图,不需要运行设备:
rg -n 'Dumpsys::main|startDumpThread|writeDump|stopDumpThread' \
frameworks/native/cmds/dumpsys
rg -n 'BpBinder::dump|DUMP_TRANSACTION|BBinder::onTransact' \
frameworks/native/libs/binder
rg -n 'writeFileDescriptor|readFileDescriptor|TF_ACCEPT_FDS' \
frameworks/native/libs/binder
rg -n 'DumpRunningService|DumpWithArgsRunningService|TimeoutInSec' \
frameworks/native/cmds/dumpsys/tests然后选一个真实服务做最后一跳,不要猜类名:
rg -n 'status_t .*::dump\(|status_t dump\(' frameworks/native services system对搜索结果完成四项核对:该类是否是 ServiceManager 返回对象的最终实现;它是否覆写 dump;是否持锁或切换线程;所有输出是否都写向传入 fd。若设备可用,再用一个存在服务和一个不存在服务验证分流:
adb shell service list
adb shell dumpsys activity -h
adb shell dumpsys __definitely_missing_service__
adb shell dumpsys -T 1000 activity最后尝试不看图复述完整链:命令参数进入 Dumpsys::main,ServiceManager 返回 sp<IBinder>,工作线程把 pipe 写端交给 BpBinder::dump,Parcel 携带 fd 和参数发送 DUMP_TRANSACTION,服务端 BBinder::onTransact 解包并调用具体 dump,主线程从 pipe 读出并写到 stdout。若你还能指出服务缺失发生在 pipe 之前、超时不会取消服务,以及测试没有覆盖真实 Binder 驱动,这条 C++ 调用链才算真正闭环。
11. 阅读边界
本文证明的是 Android 17 用户态源码中的 dumpsys 主链、资源所有权和失败边界。它没有证明每个服务都覆写 dump(),没有证明所有服务的输出都不会阻塞,也没有展开 kernel Binder 对 fd 的安装细节。
真正可迁移的阅读方法有三步:先从真实入口确定动态对象,再把代理封包与服务端解包配对,最后用资源所有权和测试断言检查正常与非正常结束。遇到下一条大型 C++ 链时,优先重复这三步,而不是先通读继承树或把所有智能指针实现都读一遍。
