8. Binder 事务(Transaction)机制:事务的发起、传递与完成,事务栈与线程状态
好,我们进入Binder机制里最核心、也最绕的一个环节——事务机制。
说实话,我当年第一次啃Binder源码时,就是被“事务栈”和“线程状态”这两个概念搞晕的。后来在项目里调一个跨进程死锁的bug,折腾了三天,最后发现是事务栈没处理好。从那以后,我对这块的理解才算真正到位了。
8.1 什么是Binder事务?
说白了,一次Binder通信就是一次事务。从客户端发起请求,到服务端处理完成,再到结果返回,整个过程就是一个完整的事务。
你想想看,跨进程通信最怕什么?最怕数据不一致。所以Binder设计了一套事务机制,保证整个调用过程是原子性的——要么全部成功,要么全部回滚。
事务的核心特征:
- 一次事务对应一次跨进程方法调用
- 事务包含完整的调用数据(参数、返回值、异常信息)
- 事务在驱动层有严格的完整性校验
8.2 事务的发起:从用户态到内核态
事务是怎么开始的?嗯,我们一步步拆解。
当你在Java层调用一个Binder接口方法时,比如:
// 客户端调用
IBinder binder = ServiceManager.getService("xxx");
IMyService service = IMyService.Stub.asInterface(binder);
service.doSomething("hello");
这个调用会经过层层封装,最终走到C++层的IPCThreadState。我个人习惯把这里称为“事务的起点”。
关键流程是这样的:
- Java层代理对象:把方法调用转换成Parcel数据包
- JNI桥接:调用到C++层的
BpBinder::transact() - IPCThreadState:调用
talkWithDriver(),通过ioctl进入内核 - Binder驱动:分配事务结构体,放入目标进程的事务队列
这里有个细节我提醒一下:talkWithDriver()这个函数名起得挺形象的,说白了就是“跟驱动唠嗑”。但唠嗑的内容可不是随便说的,数据格式必须严格遵循Binder协议。
8.3 事务的传递:驱动层的调度
事务进入驱动层后,Binder驱动会做几件关键的事:
- 校验事务合法性:检查目标节点是否存在、权限是否足够
- 分配事务ID:每个事务都有一个唯一的
transaction_id - 放入目标队列:把事务挂到目标进程的
todo_list上 - 唤醒目标线程:如果目标线程在等待事务,就唤醒它
避坑指南:我曾经遇到过一个问题——客户端发了一个大事务(比如传了个10MB的Bitmap),结果驱动层直接返回FAILED_TRANSACTION。后来查文档才发现,Binder默认的事务数据大小限制是1MB。超过这个值,驱动会直接拒绝。所以传大数据时,记得用文件描述符或者共享内存。
8.4 事务的完成:从内核态回到用户态
服务端怎么知道有事务来了?靠的是线程轮询。
服务端的IPCThreadState会不断调用talkWithDriver(),检查自己的todo_list。一旦发现有新事务,就取出来处理:
// 伪代码示意
status_t IPCThreadState::executeCommand(int32_t cmd) {
switch (cmd) {
case BR_TRANSACTION:
// 收到事务,调用BBinder::transact()
// 最终回调到Java层的onTransact()
break;
case BR_REPLY:
// 收到回复,唤醒等待的客户端线程
break;
}
}
处理完成后,服务端会把结果封装成另一个事务(回复事务),再通过驱动传回客户端。客户端收到回复后,原来的transact()调用才返回。
嗯,这里要注意:一次完整的Binder调用,实际上包含了两个事务——请求事务和回复事务。驱动层会把它们关联起来,通过transaction_id匹配。
8.5 事务栈:为什么需要它?
好,现在聊重点——事务栈。
你想想看,如果一个线程在处理事务A的过程中,又发起了事务B(比如服务端在处理请求时,又去调用另一个服务),那怎么办?
这就是事务栈存在的意义。
每个IPCThreadState对象内部维护了一个mCallingStack,用来记录当前线程正在处理的事务链:
// 事务栈的核心结构
struct IPCThreadState {
// ...
Vector<CallState> mCallingStack; // 事务调用栈
// ...
};
struct CallState {
int32_t mCallingPid; // 调用者PID
int32_t mCallingUid; // 调用者UID
sp<BBinder> mCallingBinder; // 调用者Binder对象
};
事务栈的作用:
- 记录调用链:知道当前事务是谁发起的
- 权限传递:嵌套调用时,权限信息会沿着栈传递
- 死锁检测:如果发现事务栈里有循环依赖,驱动会返回错误
注意:事务栈不是无限深的。Binder驱动对嵌套深度有限制,默认是16层。超过这个深度,驱动会返回FAILED_TRANSACTION。我在做系统服务时,曾经因为服务间调用链太长踩过这个坑。
8.6 线程状态:Binder线程的生命周期
Binder线程的状态切换,是理解事务机制的关键。每个Binder线程有以下几个状态:
| 状态 | 说明 | 触发条件 |
|---|---|---|
BINDER_LOOPER_STATE_REGISTERED |
已注册的Binder线程 | 线程启动时注册到驱动 |
BINDER_LOOPER_STATE_ENTERED |
进入主循环 | 调用joinThreadPool() |
BINDER_LOOPER_STATE_WAITING |
等待事务 | 调用talkWithDriver()且无事务 |
BINDER_LOOPER_STATE_POLL |
轮询中 | 驱动层等待事务到来 |
BINDER_LOOPER_STATE_NEED_RETURN |
需要返回 | 事务处理完成或出错 |
线程状态的切换流程,我画了一张图:
8.7 事务与线程状态的联动
事务和线程状态是紧密耦合的。我总结几个关键点:
- 发起事务时:线程状态从WAITING切换到PROCESSING,事务入栈
- 处理事务时:如果发起嵌套调用,新事务入栈,线程状态不变
- 事务完成时:事务出栈,如果栈为空,线程回到WAITING状态
- 异常情况:如果事务处理失败,线程进入NEED_RETURN状态,等待驱动处理
个人经验:调试Binder问题时,我经常在/sys/kernel/debug/binder/目录下查看事务栈信息。这个目录下的文件会显示每个进程的事务队列和线程状态,对定位死锁问题特别有用。
8.8 总结
Binder事务机制,说白了就是一套跨进程调用的“快递系统”。事务是包裹,事务栈是快递单,线程状态是快递员的状态。
理解这三者的关系,你就能看懂Binder通信的全貌了。下次遇到跨进程调用卡死、返回异常的问题,先查事务栈,再看线程状态,八成能找到问题所在。
公众号:蓝海资料掘金营,微信deep3321