本文基于Androidplatform分支android-13.0.0_r1和内核分支common-android13-5.15解析。
一些关键代码的链接,可能会因为源码的变动,发生位置偏移、丢失等现象。可以搜索函数名,重新进行定位。
Binder事务是AndroidIPC(进程间通信)机制的基本单元。它是基于C/S架构的,主要涉及到两个进程:客户端进程和服务端进程。客户端进程发送一个包含指令和数据的Binder事务请求到服务端进程,服务端进程在收到请求后,进行处理并返回一个Binder事务回复。
你可以认为Binder事务,就是一个进程(客户端)在调用另一个进程(服务端)中的一个函数。这个"函数调用"的过程就是通过发送一个Binder事务来发起,"返回值"就是Binder事务的回复。
Binder驱动在Binder事务中的作用,就像一个中转站,负责转发工作:
本文主要围绕Binder事务,逐个解释它涉及的各种小概念。
Binder消息,即Binder命令,笔者更喜欢称其为消息。二者并无差别。
就像网络请求中的Http报文一样,在进程间传递的Binder消息,也遵循特定的Binder协议。Binder协议是AndroidBinder的底层通信协议,主要在Android用户空间和内核空间之间进行通信。Binder协议定义了一组消息码,每个消息码都对应Binder系统中的一个操作或事件。
Binder的消息通信基于这组消息码。消息的具体格式由Binder协议定义,通常包括消息码和数据两部分:
注意:不是所有的消息码后面,都会有对应的数据,如BR_TRANSACTION_COMPLETE。
binder_write_read的定义如下:
structbinder_write_read{binder_size_twrite_size;binder_size_twrite_consumed;binder_uintptr_twrite_buffer;binder_size_tread_size;binder_size_tread_consumed;binder_uintptr_tread_buffer;};结构体中的字段有以下含义:
在调用BINDER_WRITE_READ这个ioctl命令时,用户空间会设置write_size和write_buffer来指定要写入Binder驱动的数据,设置read_size和read_buffer来指定读取数据的缓冲区。Binder驱动在处理完ioctl命令后,会更新write_consumed和read_consumed字段来反馈实际消耗和返回的字节数。
这个ioctl命令的用途非常广泛,包括但不限于向驱动发送Binder事务请求、接收来自驱动的回复和通知等。
Binder消息都是通过BINDER_WRITE_READ这个ioctl命令发送、读取的。一个BINDER_WRITE_READ里可以发送、读取多个Binder消息。如下:
下面的代码揭示了如何通过一个ioctl()调用,发送BC_TRANSACTION消息。
同步事务是最常见的事务。当一个进程给另一个进程发送同步事务时,它会等待直到事务处理完毕。这意味着在事务处理期间,调用进程会被阻塞。当事务处理完毕后,会有一个回复数据,返回到调用进程,也就是客户端。
大致流程是:
注意:
失败的情况有很多种,比如服务端不存在、客户端已死亡、缓冲区溢出等等。这里举两个可能的例子:
Binder驱动在处理BC_TRANSACTION消息时,发现消息中指向服务端的引用是一个无效引用,就会回复BR_FAILED_REPLY消息给客户端。
Binder驱动在处理服务端的BC_REPLY消息时,发现这时候客户端已经死亡,不需再回复客户端。不过,仍然需要回复BR_TRANSACTION_COMPLETE消息给服务端。
异步事务是一种不需要回复的事务,它会在事务数据的flags中增加一个TF_ONE_WAY标志位。当一个进程发送一个异步事务到另一个进程时,它会立即返回,不需要等待事务的完成。这意味着异步事务是非阻塞的,所以它们对于需要快速响应的场景很有用。然而由于没有回复,客户端无法知道事务何时完成以及是否成功。
事务的数据,通常会包含多种数据类型,主要分为两种:普通的数据类型和对象。
flat_binder_object的定义如下:
structflat_binder_object{structbinder_object_headerhdr;__u32flags;union{binder_uintptr_tbinder;__u32handle;};binder_uintptr_tcookie;};普通的数据类型传递给Binder驱动的时候,会直接拷贝到接收缓冲区。而对象不一样,需要做特殊处理。比如Binder实体传递给Binder驱动的时候,会被逐个处理,转换成Binder引用。
binder_transaction_data的定义如下:
这是一段我们想要发送给其他进程的数据。buffer就是指向这段数据的起始地址。而offsets则是指向数据里offsets数组。offsets[0]、offsets[1]则分别代表数据里两个flat_binder_object的地址相比较于buffer的偏移量,即offset[i]-buffer。
可以发现,int、float、flat_binder_object这些数据并不遵循一定的顺序,写入到缓冲区中。所以,这就要求在服务端里,我们必须按照写入的顺序,读取接收缓冲区里的数据。
Binder实体、Binder代理是Framework层的抽象概念。Binder节点和Binder引用则是内核层的抽象概念。它们的关系如下:
把Binder实体跨进程传递时,其实就是把指向该Binder实体(即BBinder对象)的指针存储在flat_binder_object中,驱动会在为当前进程维护的红黑树里为它创建对应的Binder节点,然后在为目标进程的维护的红黑树里里,创建对应的Binder引用,最后把引用传递给目标进程,并被记录在对应的BpBinder里。
我们想要调用另一个进程的函数,必须先持有另一个进程的能调用这个函数的Binder实体相对应的Binder代理。这就要求另一个进程必须先把该Binder实体传递到我们当前进程。
下图展示了将Binder实体通过一个Binder事务第一次传递给其他进程时所发生的事:
注意,客户端、服务端只是相对而言的。图中是指发起事务的是客户端。当服务端拿到Binder实体的引用时,就可以构建一个对应的Binder代理,即BpBinder,向Binder实体发起事务。这时候,它就是客户端,而Binder实体所在进程就是服务端。
细心的读者,可能会有一个疑问:Binder实体需要由一个事务发送出去。但是事务是要由一个Binder代理发起的,那么最开始的一个Binder代理,它的Binder引用又是从哪里来的呢?
这其实就是个鸡生蛋,蛋生鸡,到底是先有鸡还是先有蛋的问题。ServiceManager就是那只最开始的鸡。后续在介绍ServiceManager时,会解释这个问题。
客户端发起事务:
BpBinder.cpp
└──IPCThreadState.cpp
└───────ioctl.cpp
服务端接收事务:
IPCThreadState.cpp
└────ioctl.cpp
└───IPCThreadState.cpp(这里重新回到用户空间,并完成talkWithdDriver()调用)
└─────Binder.cpp
└────IPCThreadState.cpp(这里完成了BBinder::transact()调用,回到了executeCommand())
└────────后续调用栈与客户端发送BC_TRANSACTION消息类似...