第二讲: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()方法,完整流程分几步?
- Client发起请求:Client调用
transact(),把方法编号、参数打包成Parcel。 - 进入驱动层:ioctl函数切入内核空间。驱动把Client进程的数据拷贝到内核缓冲区。
- 驱动查找目标:根据目标Handle,找到对应的Server端Binder节点。
- 唤醒Server线程:驱动把数据推给Server的内核缓冲区,然后唤醒Server的工作线程。
- Server处理并回复:Server反序列化请求,执行
start(),把结果写回Parcel。 - 返回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图,我把刚才讲的四个角色和它们之间的交互关系画出来了。你注意看箭头方向和数据流的方向,特别关注驱动所在的那个位置——它是整个通信的枢纽。
这张图里,我特意把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结构)不变,只改变“从哪获取数据”这个环节。