Android-Framework(九)-从Java层跟踪服务的查找过程

文章内容

视频素材:[01.驱动层单向通信流程回顾]

App 调用 startActivity() 时,代码里那个看似普通的 getDefault(),到底怎样找到另一个进程里的 ActivityManagerService(下文简称 AMS)?
答案不是“Binder 帮我们找到了”这么简单。沿调用栈往下走,会看到一次查询如何穿过 Java、native 和驱动,再原路带回一个 handle。

而且这里其实有两次 IPC:第一次向 Service Manager 问“AMS 在哪”,第二次才拿着查到的 handle 调用 AMS。

一、全局概览

涉及三块:

  1. 发起 startActivity() 的客户端进程
  2. 保存服务目录的 Service Manager 进程
  3. 以及夹在中间的 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 通信三方关系

简单小结:

  • 关键位置在驱动的 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 版本方法名略有差异,核心链路不变:

java
// 代码位置 /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(),最终调用:

c
// 代码位置 /frameworks/native/libs/binder/IPCThreadState.cpp 第 803—902 行ioctl(fd, BINDER_WRITE_READ, &bwr);

这里传入的 bwr 是一个 binder_write_read 结构,本质是“写区 + 读区”两段缓冲的打包,简化示意代码:

c
// 代码位置 /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;后者只表示驱动收下了请求,不等于服务已经查到。

换句话说,这一步驱动依次做了两件事:

  1. 先给发起方(客户端)所在线程挂一条 BR_TRANSACTION_COMPLETE,纯粹是个回执;
  2. 再给目标(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。简化示意这次改写:

c
// 代码位置 /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 链找到原始请求者,把结果送回客户端。

Service Manager 响应流程

简单小结:

  • BR_TRANSACTION 触发查找,输入是 activity;
  • 动作是查表、建立引用并改写对象类型;
  • 状态从“名字”变成“客户端进程内可用的 handle”;
  • 结果回到原请求线程。

六、客户端为什么会等两次

客户端在 talkWithDriver() 的循环里先读到 BR_TRANSACTION_COMPLETE。它把 write 区清成 0,保留 read 区,再次进入 ioctl。此时 write_size=0,自己的 todo_queue 又为空,于是线程睡进等待队列。简化示意这段控制流:

cpp
// 代码位置 /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 段内存移动:

  1. 客户端用户空间经 copy_from_user 进入内核。
  2. 内核写入 Service Manager 的映射接收区。
  3. Service Manager 从映射区组织回复并进入其内核写路径。
  4. 驱动通过 copy_to_user 返回读命令等元数据。
  5. Service Manager 的回复数据再次从用户空间进入内核。
  6. 内核把回复放进客户端映射区,客户端直接读取。

这个数字取决于统计边界:若只数一次单向 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 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Android-Framework(九)-从Java层跟踪服务的查找过程 · Yinchen 的博客