第二讲:Binder通信流程详解——Client/Server/ServiceManager/驱动

好,咱们进入第二讲。上一章我们大概聊了聊Binder是什么,以及Mock回放为什么重要。这一章,我得把Binder通信的骨架给你拆开,看看Client、Server、ServiceManager和Binder驱动这四个角色到底是怎么配合工作的。

我刚开始接触Binder时,说实话挺懵的。那时候想着不就是个IPC嘛,跟Socket能有多大区别?结果一追源码,发现它内部设计了一套相当巧妙的机制。尤其是ServiceManager这个角色,我早期调试一个问题时,发现它挂了会导致整个系统通讯瘫痪——嗯,从那以后我再也不敢小看它了。

1. 四元组:谁负责什么?

在一个完整的Binder调用链路里,涉及四个核心组件:

角色 进程空间 核心职责
Client 用户空间(调用方) 发起服务调用,等待返回结果
Server 用户空间(提供方) 注册服务并处理Client的请求
ServiceManager 用户空间(管家) 管理服务注册与查询,类似电话簿
Binder驱动 内核空间(信使+交警) 跨进程数据拷贝、线程管理、权限校验

为什么需要四个?你想想看,如果Client直接去找Server,那Server在哪它怎么知道?地址怎么定?谁告诉它?所以ServiceManager就是那个信息中介。而驱动呢,是所有数据搬运和同步的核心。我个人的理解是:驱动是真正干苦力的,ServiceManager是前台接待,Server和Client是办事的双方。

关键认知: ServiceManager本身也是一个Binder Server,它有一个特殊的Handle——0号。所有进程在初始化时都知道怎么找到0号。

2. 一条完整的调用链路

咱们不说虚的。假设有一个进程想远程调用“MediaPlayerService”的start()方法,完整流程分几步?

  1. Client发起请求:Client调用transact(),把方法编号、参数打包成Parcel。
  2. 进入驱动层:ioctl函数切入内核空间。驱动把Client进程的数据拷贝到内核缓冲区。
  3. 驱动查找目标:根据目标Handle,找到对应的Server端Binder节点。
  4. 唤醒Server线程:驱动把数据推给Server的内核缓冲区,然后唤醒Server的工作线程。
  5. Server处理并回复:Server反序列化请求,执行start(),把结果写回Parcel。
  6. 返回Client:驱动把回复数据拷贝回Client的用户空间。Client拿到结果。

你看,这个过程里数据在用户态和内核态之间至少发生了两次拷贝。但这相比传统的Socket方案,已经少了一次。因为Binder用了内存映射(mmap),Server端的内核缓冲区跟Server的用户缓冲区共享物理内存,不用再额外拷一遍。我在做Mock回放时,最关注的就是第二步和第六步——因为我们要在这里“截获”数据流。

3. 驱动内部发生了什么?

这是最难啃的一块,但我尽量用大白话讲。Binder驱动在内核里维护了三个关键数据结构:

  • binder_proc:每个用到Binder的进程都有一个,记录进程信息、线程池状态、打开的文件描述符等。
  • binder_node:每个Binder实体(Server端对象)对应一个节点,记录了该实体在哪个进程。
  • binder_ref:Client端持有的引用,指向某个binder_node。这其实就是Handle的底层实现。

我举个例子:假设App A要调用App B的服务。驱动会把哪个binder_ref映射给A?答案是ServiceManager告诉它的那个。而ServiceManager在注册服务时,驱动会为它创建一个binder_node和对应的binder_ref。

// 伪代码:驱动处理binder_ioctl的关键逻辑
case BINDER_WRITE_READ:
    // 1. 拷贝用户传进来的BC_TRANSACTION数据
    // 2. 找到目标binder_ref
    // 3. 通过ref找到binder_node
    // 4. 找到node所在进程的binder_proc
    // 5. 把数据放到目标进程的缓冲区
    // 6. 唤醒目标进程的等待线程
    break;

这一套逻辑看着简单,但实际源码有几千行。我当年看内核源码时,一个binder_thread_read函数就折腾了两天。嗯,说实话,现在让我手写一遍我也写不全,但理解核心数据流就够了。

小技巧: 做单元测试Mock时,你不需要模拟整个驱动。你只需要模拟“数据从哪里来、回哪里去”。但理解驱动原理能帮你理解Mock中的同步和异步问题。

4. ServiceManager的特殊地位

前面提到ServiceManager的Handle是0。这意味着所有进程在初始化时,都会通过驱动获取到ServiceManager的引用。那ServiceManager刚启动时,谁来告诉驱动它的存在?答案是——驱动本身。

在系统启动早期,ServiceManager会打开/dev/binder,然后调用BINDER_SET_CONTEXT_MGR ioctl。驱动收到这个命令后,会把这个进程标记为“管家进程”。所以其他进程后续查询服务时,驱动就知道该把这个请求发给谁。

我曾经碰到过一个坑:某个系统服务在注册时,ServiceManager还没准备好,结果那个服务注册失败了。后面所有依赖它的App都启动异常。这个排查过程让我深刻体会到ServiceManager的启动时序是多么关键。所以后来我做Mock框架时,特别强调要先初始化“虚拟的ServiceManager”,否则后面所有Mock服务都注册不上去。

5. 你看一张图就全懂了

下面这张SVG图,我把刚才讲的四个角色和它们之间的交互关系画出来了。你注意看箭头方向和数据流的方向,特别关注驱动所在的那个位置——它是整个通信的枢纽。

Binder四元通信框架图 Client (调用方 – 用户空间) Server (提供方 – 用户空间) ServiceManager (管家 – 用户空间) Binder驱动 (内核空间 – 数据搬运 + 线程管理 + 权限校验) 2. transact() 写数据 4. 唤醒线程处理 5. 回复数据 6. 返回结果 1. 查询服务 返回Handle 3. 注册服务 要点:ServiceManager本身也是Binder Server (Handle=0)。驱动负责所有跨进程数据拷贝与线程调度。

这张图里,我特意把ServiceManager放在上方,驱动放在下方,Client和Server左右排列。你顺着箭头看:Client先找ServiceManager查询服务(虚线),得到Handle后通过驱动(实线)跟Server通信。整个过程中,驱动是中心枢纽,数据都要经过它。我个人做Mock时,最核心的Mock点位就在驱动那层——不过我们不用真去改内核,而是在用户态把transact()的输入输出截住就行。

6. 我总结了一个口诀

讲完这么多,我怕你记不住。我习惯用一句话总结Binder通信:

查询找管家,收发靠驱动,服务在Server,调用在Client。

什么意思?

  • 查询找管家:所有服务必须先向ServiceManager注册,Client要服务也得先问它。
  • 收发靠驱动:数据拷贝、线程同步、权限校验,全都由驱动代劳。Client和Server不需要关心底层。
  • 服务在Server:真正的业务逻辑在Server端运行。
  • 调用在Client:Client只负责发起请求和接收结果。

避坑提示: 我曾经见过有人做Mock时,直接绕过了Binder驱动,用本地函数指针替换了transact()。这导致后来的数据格式跟真实Binder不兼容,Mock出来的结果跟线上对不上。所以我的建议是:尽量保持数据格式(Parcel结构)不变,只改变“从哪获取数据”这个环节。