文章内容
视频素材:[01.驱动层单向通信流程回顾]
App 调用 startActivity() 时,代码里那个看似普通的 getDefault(),到底怎样找到另一个进程里的 ActivityManagerService(下文简称 AMS)?
答案不是“Binder 帮我们找到了”这么简单。沿调用栈往下走,会看到一次查询如何穿过 Java、native 和驱动,再原路带回一个 handle。
而且这里其实有两次 IPC:第一次向 Service Manager 问“AMS 在哪”,第二次才拿着查到的 handle 调用 AMS。
一、全局概览
涉及三块:
- 发起
startActivity()的客户端进程 - 保存服务目录的 Service Manager 进程
- 以及夹在中间的 Binder 驱动。
驱动为每个 binder_proc 维护若干红黑树。理解本次流程,盯住两类对象就够了:
binder_node表示本进程发布到驱动里的实体服务;binder_ref表示本进程对外部binder_node的引用。
引用既要能按 handle(源码常叫desc)查,也要能按目标 node 查。
更具体一点说:典型实现里每个 binder_proc 挂两类树——一类记录本进程发布的 binder_node,另一类记录本进程持有的 binder_ref;binder_ref 又同时按 handle(desc)和按目标 node 建两份索引,一份用来支持“拿着 handle 找对象”,一份用来支持“拿到一个 node 时判断是否已经建过引用、避免重复分配”。
每个进程还会 mmap 一块接收区。数据到来时,驱动把事务内容放进目标进程映射的页,再把工作项挂进 todo_queue,通过 wait_queue 唤醒睡眠线程。

简单小结:
- 关键位置在驱动的
binder_proc; - 进程打开 Binder 并
mmap时建立基础状态; - 输入是进程与映射信息;
- 驱动维护 node、ref、等待队列和待办队列;
- 状态从“没有事务”变成“可接收工作”;
二、Service Manager
Service Manager 启动时通过 binder_ioctl 把自己注册成 context manager。驱动据此保存全局的 context_manager_node,对应 ioctl 命令为 BINDER_SET_CONTEXT_MGR。它不是普通服务查出来的引用,而是所有进程约定好的入口:handle=0。
接着 Service Manager 进入循环,先向驱动写入进入 looper 的命令(BC_ENTER_LOOPER),再调用读写 ioctl。它没有任务时就睡在 wait_queue 上;todo_queue 一旦非空,驱动便唤醒它,main 循环继续取请求。
位置在 Service Manager 与 Binder 驱动的初始化路径;启动触发注册;输入是本地 Binder 实体;驱动设置 context_manager_node;状态从“无管理者”变为“0 号入口可用”;所有服务查询都去这里。
三、客户端从 Java 一路下潜路径
先看这一小段,是为了确认 Java 层拿到的并不是 AMS 实体,而是跨进程代理。不同 Android 版本方法名略有差异,核心链路不变:
// 代码位置 /frameworks/base/core/java/android/app/ActivityManagerNative.java 第 2604—2614 行IServiceManager sm = getIServiceManager();IBinder ams = sm.getService("activity");IActivityManager iam = IActivityManager.Stub.asInterface(ams);输入是服务名 activity;输出是 IActivityManager 代理。此时客户端状态从“只知道 Service Manager”变为“持有 AMS 的 BinderProxy”,下一步才能真正调用 AMS。
展开内部调用,就是 getDefault() → getIServiceManager() → ServiceManagerNative.getContextObject()。native 层先构造 BpBinder(0),Java 的 BinderProxy.mObject 保存着指向该 native 对象的句柄。查询请求随后进入 IPCThreadState::transact(),最终调用:
// 代码位置 /frameworks/native/libs/binder/IPCThreadState.cpp 第 803—902 行ioctl(fd, BINDER_WRITE_READ, &bwr);这里传入的 bwr 是一个 binder_write_read 结构,本质是“写区 + 读区”两段缓冲的打包,简化示意代码:
// 代码位置 /bionic/libc/kernel/uapi/linux/binder.h 第 59—67 行struct binder_write_read bwr;bwr.write_size = ...; // 本次待发送数据的长度bwr.write_buffer = ...; // 待发送的事务(服务名 + Parcel)bwr.read_size = ...; // 预留的回执缓冲长度bwr.read_buffer = ...; // 用来接收 BR_ 系列命令 ioctl(fd, BINDER_WRITE_READ, &bwr);为什么看这一行?因为它是用户态跨进驱动的门。
输入是写区中的 transaction 和预留的读区;驱动看到目标 handle 为 0,直接映射到 context_manager_node;客户端状态变为“请求已提交,等待结果”;请求去往 Service Manager。
四、驱动怎样把查询送过去
binder_ioctl 解析命令后,根据类型处理 Binder 对象;普通 handle 会在当前进程的 binder_ref 红黑树中查找,0 号 handle 则走 context manager 特例。驱动再用 binder_alloc_buf 为目标事务分配缓冲区。这里的“分配”带着按需加载意味:mmap 建立地址关系,真正传数据时才补齐需要的物理页。
事务数据被复制进 Service Manager 的映射区。随后驱动生成两份结果:给目标进程的 BR_TRANSACTION,以及给客户端的 BR_TRANSACTION_COMPLETE。前者进入 Service Manager 的 todo_queue 并唤醒其 wait_queue;后者只表示驱动收下了请求,不等于服务已经查到。
换句话说,这一步驱动依次做了两件事:
- 先给发起方(客户端)所在线程挂一条
BR_TRANSACTION_COMPLETE,纯粹是个回执; - 再给目标(Service Manager)线程挂一条
BR_TRANSACTION,才真正携带服务名和 Parcel 数据。两条命令分别挂在各自的todo_queue上。
普通服务引用的 handle 按进程分配,通常从可用的最小正整数递增,所以同一个服务在不同进程里完全可能是不同 handle。它是本进程的索引,不是全局服务编号。

简单小结:
- 位置在
binder_transaction与目标进程的分配器; - 客户端 ioctl 触发;
- 输入是服务名、目标 0 号 handle 和 Parcel;
- 驱动分配缓冲、复制数据、挂入两边队列;
- 双方由“空闲”变成“目标可读、发送方已提交”;
- 查询去往 Service Manager 的 main 循环。
五、Service Manager 查目录并回复
Service Manager 被唤醒后读到 BR_TRANSACTION,从事务结构取得数据,并借助地址与 cookie 关联到本地 Binder 实体。main 循环解包请求,getService 最终进入 do_find_service,在服务表中查找 activity。
找到 AMS 后,不能把 AMS 进程里的裸指针交给客户端。驱动会为客户端建立或复用指向目标 binder_node 的 binder_ref,并把扁平 Binder 对象从 BINDER_TYPE_BINDER 改写为 BINDER_TYPE_HANDLE。简化示意这次改写:
// 代码位置 /kernel/common/include/uapi/linux/android/binder.h 第 123—134 行// 回复对象从“本地实体”改写为“跨进程可用的 handle”obj.hdr.type = BINDER_TYPE_HANDLE;obj.handle = ref->desc; // 客户端能看到的只有这个整数// cookie(AMS 在自己进程内的对象地址)不会被带出这次回复回复的有效身份是 handle,而不是可跨进程使用的 cookie——AMS 在自身进程里的裸指针(cookie)只在 AMS 进程内部有意义,一旦跨进程就必须换成 handle 这种“由驱动翻译过”的间接引用。
Service Manager 随后写回两类动作:释放刚处理完的接收缓冲区,以及提交 reply。驱动依据 reply 链找到原始请求者,把结果送回客户端。

简单小结:
BR_TRANSACTION触发查找,输入是activity;- 动作是查表、建立引用并改写对象类型;
- 状态从“名字”变成“客户端进程内可用的 handle”;
- 结果回到原请求线程。
六、客户端为什么会等两次
客户端在 talkWithDriver() 的循环里先读到 BR_TRANSACTION_COMPLETE。它把 write 区清成 0,保留 read 区,再次进入 ioctl。此时 write_size=0,自己的 todo_queue 又为空,于是线程睡进等待队列。简化示意这段控制流:
// 代码位置 /frameworks/native/libs/binder/IPCThreadState.cpp 第 712—801 行// IPCThreadState 里等待 reply 的循环,简化示意do { if (talkWithDriver() < NO_ERROR) break; cmd = mIn.readInt32(); if (cmd == BR_TRANSACTION_COMPLETE) { // 只表示驱动已收下请求,不是真正的服务查询结果 // write 区清零、保留 read 区,继续等 reply continue; } // 其余分支(如 BR_TRANSACTION 携带 reply)在这里处理} while (reply == nullptr);reply 到来后,驱动找到原始请求线程,在客户端侧建立 binder_ref,按需准备接收页,把数据放入映射区并唤醒线程。IPCThreadState::executeCommand() 解析返回对象,用 handle 创建 BpBinder(handle),Java 层再用 BinderProxy 包住它。
这里创建 BpBinder(handle) 用的 handle,是本进程 binder_ref 表里的编号,不是 Service Manager 那边的编号——同一个 AMS,换一个客户端进程去查,很可能拿到不同的数字,这与第四节提到的“引用按进程分配”是同一件事的两面。顺带一提,Service Manager 自己手里那个 BpBinder(0),也是通过同样的 ServiceManagerNative 路径创建的,并没有走什么特殊通道。

简单小结:
- 第一次完成通知触发再次读取;
- 输入是 reply 中的 handle;
- 动作是解析并创建代理;
- 状态从“等待查询”变成“持有 AMS 远端引用”;
- 代理返回
getDefault()的调用方。
七、“零拷贝”不等于一字节不拷
说实话,把 Binder 简称为“零拷贝”很容易造成误会。按这次服务查询的完整往返拆开看,素材里统计了 6 段内存移动:
- 客户端用户空间经
copy_from_user进入内核。 - 内核写入 Service Manager 的映射接收区。
- Service Manager 从映射区组织回复并进入其内核写路径。
- 驱动通过
copy_to_user返回读命令等元数据。 - Service Manager 的回复数据再次从用户空间进入内核。
- 内核把回复放进客户端映射区,客户端直接读取。
这个数字取决于统计边界:若只数一次单向 payload 传输,会得到更少的次数。mmap 的价值,是让驱动可直接把 payload 放进接收方可见的缓冲区,省掉传统 IPC 的额外中转,不是施展“完全没有复制”的魔法。
八、拿到 handle 之后
Service Manager 的工作在服务发现完成后基本结束。客户端获得 AMS 的 Binder 代理后,后续调用不会再次经过 Service Manager。发送 transaction 时,Binder 驱动根据客户端进程中的 handle 找到对应的 binder_ref,再通过 binder_ref->node 找到 AMS 对应的 binder_node,并由 binder_node->proc 确定目标是 system_server,最终把事务投递给该进程的 Binder 线程。
因此,从首次获取 AMS 到调用 startActivity(),可以看到两段 Binder 通信:
- 第一段是向 Service Manager 查询 "activity" 服务;
- 第二段是直接通过 AMS 的 Binder 代理向 system_server 发起 transaction。实际运行中 AMS 代理通常会被缓存,因此并不是每次 startActivity() 都重新执行一次 getService("activity")。
本文由 AgentWorkFlow 生成