常用类与结构体
进程将数据发往驱动时,并不是直接发送,而是进行各种封装后再发送过去。而且发送过程中涉及到不同的类:
- ProcessState:进程单例,负责 open、mmap 驱动,进而拿到驱动对应的文件描述符
- IPCThreadState:线程单例,负责与驱动通信,即 ioctl 过程
- Parcel:数据包装工具类。该类在 java 与 native 层都有实现,java 层只是对 native 的简单调用
服务进程与驱动交互过程中使用到两个主要结构体:binder_write_read 与 binder_transaction_data
- binder_write_read:数据的最外层封装,binder 驱动拿到的第一层数据,携带有 binder 驱动要读取数据、写入数据的地址。通过它里面的属性驱动可以知道进程要进行的哪种操作(是想读,还是想写,还是即读又写)
- binder_transaction_data:携带有一次通信的数据,它由 binder_write_read 携带,它里面还有一些额外信息,如发送者的 pid 等
binder_write_read 结构体(简称 bwr),read_size>0 则代表要读取数据,write_size>0 代表要写入数据,若都大于 0 则先写入,后读取。
struct binder_write_read {
binder_size_t write_size; /* bytes to write,要写到驱动中的数据量 */
binder_size_t write_consumed; /* bytes consumed by driver,驱动读了多少 */
binder_uintptr_t write_buffer; /* 写到驱动中的数据在本进程空间中的地址 */
binder_size_t read_size; /* bytes to read,要从驱动中读的数据量 */
binder_size_t read_consumed; /* bytes consumed by driver,read_buffer 中驱动使用了的字节数 */
binder_uintptr_t read_buffer; /* 从驱动中读的数据在本进程空间中的地址 */
};
ProcessState
进程单例。负责 binder 驱动 open、mmap
一个进程想与 binder 驱动交互,必然要经过 open, mmap, ioctl 三步,前两步就由 ProcessState 负责
内部提供 self 静态方法,用于获取进程单例的 ProcessState 实例。类似于 java 中常用的 getInstance() 写法
self() 调用构造函数,构造函数调用 open_driver(),后者调用 open() 打开 binder 驱动,然后调用 mmap 将本进程与内核空间建立内存映射关系
ProcessState::ProcessState(const char *driver)
: mDriverFD(open_driver(driver)){
// 通过 mmap 与 binder 驱动建立联系
// BINDER_VM_SIZE = (1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2 即 1M-2*页大小
// 也就是常说的 1M - 8k
mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
}
static int open_driver(const char *driver)
{
// 先通过 open 打开 binder 驱动,最终到 binder_open 方法
int fd = open(driver, O_RDWR | O_CLOEXEC);
if (fd >= 0) {
int vers = 0;
// 比对版本号
status_t result = ioctl(fd, BINDER_VERSION, &vers);
size_t maxThreads = DEFAULT_MAX_BINDER_THREADS; // 值为 15
// ioctl 调用 binder_ioctl
result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
} else {
}
return fd;
}
构造函数中,使用 mDriverFD 记录驱动的文件描述符,使用 mVMStart 记录与驱动建立映射的虚拟地址起始值
从 AIDL 看数据传输
假设有一个 aidl 文件,定义的方法如下;
oneway void test(int a,in IBinder ibinder);
转成 java 后如下:
@Override public void test(int a) throws android.os.RemoteException
{
// 先获取一个 Java 层的 Parcel
android.os.Parcel _data = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(a);// 写入参数
// 参数为 IBinder 时,Parcel 传输逻辑
_data.writeStrongBinder(ibinder);
// mRemote 为 BinderProxy 对象
// 因为是 oneway 模式的,所以最后一个参数为 FLAG_ONEWAY,如果不是 oneway 最后一个就是 0
boolean _status = mRemote.transact(Stub.TRANSACTION_test, _data, null, android.os.IBinder.FLAG_ONEWAY);
if (!_status && getDefaultImpl() != null) {
getDefaultImpl().test(a);
return;
}
}
finally {
_data.recycle();
}
}
上面的 mRemote.transact() 会调用到 jni
// obj 指调用该 jni 的 java 对象
// dataObj/replyObj 指的是 java 层的 Parcel 对象
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
// 从 java 层的 Parcel 转换成 native 层 Parcel
// java 层的 Parcel 会在构造函数中创建一个 native Parcel,同时记录下该对象的地址
// 这里就是取出该地址
Parcel* data = parcelForJavaObject(env, dataObj);
Parcel* reply = parcelForJavaObject(env, replyObj);
// target 是 BpBinder 类型
IBinder* target = getBPNativeData(env, obj)->mObject.get();
status_t err = target->transact(code, *data, reply, flags);
}
BpBinder#transact 如下
status_t BpBinder::transact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
if (mAlive) {
status_t status;
status = IPCThreadState::self()->transact(binderHandle(), code, data, reply, flags);
return status;
}
return DEAD_OBJECT;
}
最后一步到 IPCThreadState 了,见下面分析。
IPCThreadState
线程单例。负责与 binder 通信,即 ioctl 的调用
内部有 mIn、mOut 两个 Parcel 对象,前者用于给驱动写入数据(这些数据来自于目标进程),后者存放写给目标进程的数据。简单理解,驱动将数据写入到前者,驱动从后者中读取数据(当然,不是直接从 mOut 中读)。
- 提供有 self 静态方法获取实例。
- talkWithDriver:通过 ioctl 与 binder 驱动进行通信
transact
供 BpBinder 调用:用于
发送数据到驱动,并从驱动读取数据
说一下 Proxy,它就是服务端在客户端的代理。服务端会提供功能,客户端在使用功能时必然涉及到数据的传输,所以提供一个 Proxy 封装着数据的传输、读取过程。
在分析 binder 时一般都会涉及到 bp 与 bn,前者中的 p 就是指 proxy,用在客户端;后者的 n 指 native,用于服务端,可以理解为客户端对功能的实现。
它的功能就很简单:
- 调用 writeTransactionData 写数据
- 调用 waitForResponse 等待数据
status_t IPCThreadState::transact(int32_t handle,
uint32_t code, const Parcel& data,
Parcel* reply, uint32_t flags)
{
status_t err;
// 调用 writeTransactionData 写数据
writeTransactionData(BC_TRANSACTION, flags, handle, code, data, nullptr);
// 然后调用 waitForResponse 等待返回结果
if ((flags & TF_ONE_WAY) == 0) { //【代码一】
// 非单程。如果有 reply 就用,没有就伪造一个用
if (reply) {
err = waitForResponse(reply);
} else {
Parcel fakeReply;
err = waitForResponse(&fakeReply);
}
} else {// 单程
err = waitForResponse(nullptr, nullptr);
}
return err;
}
在【代码一】处有一个判断,在 aidl 转成的 java 代码中如果定义成 oneway 参数 flags 就会有值,否则该值为 0。所以【代码一】处就一个作用:如果不是 oneway 方法走 if 代码块,否则走 else,两者最大区别:oneway 方法 waitForResponse() 第一个参数为 null,非 oneway 不是 null
writeTransactionData
它的唯一作用:为 IPCThreadState#mOut 填充数据
这个方法有一个参数 Parcel,这里装的是 java 层添加的各种数据。
status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
{
// 在第一节,binder_ioctl 时,第二次 copy_from_user 拿到的就是该结构体
binder_transaction_data tr;
tr.target.ptr = 0;
// handle 是需要的 IBinder 的句柄
tr.target.handle = handle;
// 省略一些判断,主要看不出错时的情况
// 这里面的参数赋值可以看下面关于 IBinder 对象传输部分
if (err == NO_ERROR) {
// 返回 Parcel 携带的数据量
tr.data_size = data.ipcDataSize();
// 返回 mData,即 Parcel 用于存储数据的起始位置
tr.data.ptr.buffer = data.ipcData();
// ipcObjectsCount() 返回的是 mObjectsSize,即 IBinder 对象的个数
tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);
// 返回 mObjects,即 Parcel 中用于记录 IBinder 对象与 mData 偏移量的数组
tr.data.ptr.offsets = data.ipcObjects();
}
// 写入 int。cmd 是以 BC_XX 的一个 int 值,常用的是 BC_TRANSACTION 与 BC_REPLY
mOut.writeInt32(cmd);
// 写入 tr 地址
mOut.write(&tr, sizeof(tr));
return NO_ERROR;
}
方法结束了,这个方法有两个作用:
- 先向 mOut 中写入 cmd,cmd 的值是 BC_TRANSACTION。这在上一节中说过,它是 binder 整体中最常用的一个请求码
- 再写入 binder_transaction_data 所有数据。binder_transaction_data 的对象中又包括了 java 层数据在本进程中的虚拟地址。这个地址传到内核后,内核使用 copy_from_user 就可以复制出真正的数据了
waitForResponse
这个方法就是死循环等待内核的返回结果。只不过区分了 one way 与 非 one way:oneway 在内核返回 BR_TRANSACTION_COMPLETE 时会立即结束,非 oneway 会一直死循环,直到收到内核发送的 BR_REPLY
下面代码中有几个注意点:
- goto finish;:会直接跳出整个 while 循环,也就是方法结束
- switch 中的 break 只会退出 switch
// 第二个参数有默认值,为 null
// 从 transact() 中可知单程的时候 reply 为 null,否则 reply 不为 null
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
uint32_t cmd;
int32_t err;
// 死循环
while (1) {
// 使用 talkWithDriver() 与驱动交互
if ((err=talkWithDriver()) < NO_ERROR) break;
if (mIn.dataAvail() == 0) continue;
cmd = (uint32_t)mIn.readInt32();
// cmd 是由内核发给源进程的
switch (cmd) {
case BR_TRANSACTION_COMPLETE://【判断一】
// 当 binder 驱动将数据复制到目标进程后,会返回给调用进程的一个请求码,此时就走到这个 case
// 后一个判断为 true,前一个判断在 one way 时为 true
// 因此,one way 模式下,源进程收到内核发的 BR_TRANSACTION_COMPLETE 就会结束
// 而非 one way 模式下判断不成立,就会 break 掉 switch,进入下一轮 while 循环
if (!reply && !acquireResult) goto finish;
// 注意 break 掉的是 switch,并不是整个 while 循环
break;
// 省略错误以及一些不知道是啥的 case
case BR_REPLY:// 【判断二】
// 到这里一定是非 oneway 模式,因为 oneway 模式在 BR_TRANSACTION_COMPLETE 时已经结束了
// 此时相当于服务端已处理完数据,驱动将处理结果返回给客户端了
{
binder_transaction_data tr;
// 将内核返回的数据读到 tr
err = mIn.read(&tr, sizeof(tr));
if (reply) {// 非 one way 模式
if ((tr.flags & TF_STATUS_CODE) == 0) {
// 根据驱动返回的数据填充 reply,即:填充它的 mData,mObjects 等属性
// 这一点与上面的 writeTransactionData() 中格式一致
// 两者结合到一起看,效果更好
reply->ipcSetDataReference(
reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
tr.data_size,
reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
tr.offsets_size/sizeof(binder_size_t),
freeBuffer, this);
} else {
// 调用 freeBuffer,感觉是释放一些内存
}
} else {
// 也是调用 freeBuffer
// 这里的 continue 是继续 while 循环
continue;
}
}
goto finish;
default: // 【判断三】,很重要 BR_TRANSACTION 的处理就在该方法中
err = executeCommand(cmd);
break;
}
}
finish:
// 处理错误,然后返回
}
【判断一】,它是 binder 驱动将数据复制到目标进程后,返回给调用进程的一个请求码,收到该请求码后 oneway 模式下会退出 while 循环,从而 java 层可继续执行
【判断二】,是客户进程收到驱动返回的结果。驱动会把结果使用 copy_to_user 复制到 mIn 缓存区中
【判断三】,正常的一个请求时,驱动将数据从客户进程复制到服务进程后,会给服务进程发一个 BR_TRANSACTION,此时的逻辑就在【判断三】中
talkWithDriver
主要功能有两个:
- 通过 ioctl 与 binder 驱动交互
- 待驱动返回结果后,将设置 mOut 与 mIn 中的数据
说一下跟 waitForResponse() 的交互,第一次时
// 有默认参数,doReceive 为 true。doReceive 给调用者一个控制是否要读的选择
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
binder_write_read bwr;
// needRead = true 表示当前 mIn 缓存区中的数据已经处理完成,需要再次从 binder 中读取
// 从后面逻辑看,needRead 为 false 的话,即不会写数据也不会读数据。本方法就不与驱动交互,直接返回了
const bool needRead = mIn.dataPosition() >= mIn.dataSize();
// 前面一个判断为 false,参数默认为 true
// needRead 表示当前读的已经处理完了,所以可以再写一部分数据
// 如果 needRead 为 false,表示还没有读完,所以不能写,bwr.write_size = 0
// 到 binder 驱动后,只会执行读逻辑
const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0;
bwr.write_size = outAvail;
bwr.write_buffer = (uintptr_t)mOut.data();
if (doReceive && needRead) {
// 指定自己想读多大,以及读的数据存储的位置
bwr.read_size = mIn.dataCapacity();
bwr.read_buffer = (uintptr_t)mIn.data();
} else {
bwr.read_size = 0;
bwr.read_buffer = 0;
}
// 即不读,也不写,不需要执行 ioctl
if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR;
// 从这个地方可以将方法分两部分:前一部分是告诉驱动,我要读写多少,数据存储位置在哪
// 下面是将驱动要修改的设置为 0
bwr.write_consumed = 0;
bwr.read_consumed = 0;
// 这一部分是 ioctl
status_t err;
do {
ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)
} while (err == -EINTR);
if (err >= NO_ERROR) {
if (bwr.write_consumed > 0) {
if (bwr.write_consumed < mOut.dataSize())
// 驱动读了,但没读完,这里只是输出日志
// 理论上不考虑这个
else {
// 将 out 的 dataSize 设置为 0
mOut.setDataSize(0);
}
}
// 【代码一】
if (bwr.read_consumed > 0) { // 驱动写回了数据,存储到 mIn 中
// 同时将 mIn 的一些属性重置
mIn.setDataSize(bwr.read_consumed);
mIn.setDataPosition(0);
}
return NO_ERROR;
}
return err;
}
注意 write_size 与 read_size 赋值的区别:前者是自己要写入的数据,后者是自己要读的数据量。下面描述一下具体情形:
客户进程向目标进程传输数据:第一次调用 talkWithDriver() 时两者都有值,所以到驱动后执行 binder_thread_write,也执行 binder_write_read,前者会读取所有数据、后者会向客户进程回一条 BR_TRANSCTION_COMPLETE 命令 —— 这些都是在 ioctl() 方法中的逻辑。
驱动中 binder_write_read() 执行完后,客户进程回到 ioctl() 处继续往下执行,然后会将 mOut 的 dataSize 设置为 0。
客户进程在 talkWithDriver() 结束后,会回到 waitForResponse() 去处理 BR_TRANSCTION_COMPLETE 等命令。因为该方法是一个死循环,所以又会调用到 talkWithDriver(),此时 needRead 还是为 true,只不过 write_size 这次会变成 0(在上一次 ioctl 回来后被设置成 0),read_size 会大于 0。再次通过 ioctl 进入驱动,驱动这次只会执行 binder_thread_read 了。这一次 binder_thread 已经没有要执行的任务,所以会进入休眠。这就是同步方法调用线程会陷入休眠的逻辑:通过两次 talkWithDriver()
通过这两步可知,发送给 binder 驱动的结构为:binder_write_read 指向 Parcel 中的 data,Parcel 中存储有 binder_transaction_data,而它的 data.ptr.buffer 指向了 java 层设置的数据。
在【代码一】中,驱动会将数据存储到 mIn 对应的缓存区中,这里会记录驱动写了多少数据以游标(用于记录当前进程读到哪了)。
talkWithDriver() 到这里就结束了,然后回到 waitForResponse(),它根据驱动传过来的请求码执行不同的逻辑。
Parcel
完成对数据的拆解与组成。在发送方,将数据拆解成可传输的零件,在接收方根据这些零件还原成一个对象
Parcel 只存储要发送的数据,并不负责数据发送。因此它里面就需要记录数据量的大小、数据存储的位置等信息。
我们知道在同一个进程中进行数据传输时,传递的就是引用。因为是同一个进程,所以接收方拿到引用后可以直接操作数据。但在不同进程中,由于存在进程隔离,一个进程的引用传递到另一个进程中时,是没办法获取到相应的数据的。这里将数据分为两种形式:
- 如果是基本数据类型,可直接复制到目标进程即可
如果是对象,就需要将对象进行拆解,拆成可传递的的零件,接收方接收到所有的零件后,在自己的内存中还原出完整的对象。当然,接收方与发送方的对象已不是同一个了。这种情况下,需要对对象的拆解与组装。android 系统中完成该任务的是 Parcel 对象。
可以将 Parcel 理解为一个集装箱。需要运输一辆车时,它首先按照一定顺序将车给拆成零件,漂洋过海到达目的地后再按顺序将零件组装成车。
由于拆解后需要还原,所以拆解与组装必须按约定好的协议进行。比如传输数组时,它首先写入 4 个字节的整数,用于表示数组的大小,然后再依次写入数组中的元素。这就是协议
相关属性
- dataSize: 当前已存储的数量大小
- dataAvail: 当前 Parcel 中可读数据的大小
- dataCapacity:当前 Parcel 的总容量。这个值不可能小于 dataSize
- mDataPos:数据当前位置。它的值不可能超过 dataSize
- mData:数据指针,也是数据在本进程空间内的内存地址
- mObjects:flat_binder_object 数据的位置,它指向一个数组
- mObjectsSize:flat_binder_object 的数量,即 mObjects 的元素个数
Active Object
一般的存取两端是两个不同的对象,但 Active Object 却是同一个对象。常用的有 FileDescriptor 与 Binder。下面的 IBinder 时再分析。
writeString
以 writeString 分析 Parcel 写数据流程。该方法最终调用到 Parcel.cpp 中的 writeString16() 方法
// str 是 writeString() 的参数, len 为字符串长度
status_t Parcel::writeString16(const char16_t* str, size_t len)
{
if (str == nullptr) return writeInt32(-1);
// 先写长度
status_t err = writeInt32(len);
if (err == NO_ERROR) {
// 计算字符串占的字符数
len *= sizeof(char16_t);
// 字符串长度额外加一个 char16 长度,后者用于填充字符串结束标志符
uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
if (data) {
// 将字符串复制到 data 指定的起始位置
memcpy(data, str, len);
// 填充字符串结束符
*reinterpret_cast<char16_t*>(data+len) = 0;
return NO_ERROR;
}
err = mError;
}
return err;
}
主要分几步:
- 先写字符串长度
- 写字符串本身(包括对齐数据)
- 写字符串结束符
void* Parcel::writeInplace(size_t len)
{
// 按 len 长度进行补齐计算
const size_t padded = pad_size(len);
// 省略一些校验
// 不需要扩容
if ((mDataPos+padded) <= mDataCapacity) {
restart_write:
// 计算出字符串要插入的位置
// 可以将 mData 理解为一个数组,这个数组有一部分已经被占用,mDataPos 指向还空着的下标
// 所以 data 就是字符串需要被复制到的起始下标
uint8_t* const data = mData+mDataPos;
// 省略补齐逻辑
finishWrite(padded);// 更新 mDataPos
return data;
}
// 扩容,扩容后 goto restart_write
return nullptr;
}
这就是 Parcel 写入数据的整体流程。读取的时候也必须按该流程读。
IBinder 传输
后面为描述方便,将 IBinder 对象称为 binder 实体或实体,其所属的进程称为宿主进程,实体对应的 binder_ref 称为 binder 引用。
先说结论:
-
并不是直接将实体复制到目标进程。
驱动会为每一个实体创建一个 binder_node 对象,其中 binder_node::ptr 指向了 binder 实体在宿主进程中的首地址;使用进程只是拿到一个 handle,驱动可以根据该 handle 拿到对应的 binder_node,进而拿到实际地址 -
binder_node 只属于其宿主进程对应的 binder_proc,其余进程只能拿到 binder_ref。实体第一次进入驱动时,驱动创建 binder_node,并挂载到宿主进程对应的 binder_proc 中。同一个 binder 实体在不同的进程中,有不同的 binder_ref -
IBinder 对象在非宿主进程中,只能拿到一个整数 handle:
handle 是驱动按 binder 实体进入到目标进程的顺序依次分配的一个整数值,但 service_manager 永远是 0,后面依次进入的就是 1,2 等 -
在整个过程中,IBinder 对象并不会被复制:驱动拿的是它的首地址,客户进程拿的是驱动提供的句柄
在数据传输中,基础数据类型基础数据类型是 writeString(),writeInt() 形式直接传的,而普通的引用类型是调用它的 writeToParcel() 转成基础数据类型传输的。但 IBinder 不一样,它需要方法调用,所以不能像普通对象一个拆解成基础数据类型。
下面的分析是基于当前进程向 Binder 驱动发送 IBinder 对象场景。在分析过程中会用到一个结构体以及几个变量,先说明一下:
// Parcel.cpp
// Parcel 中用于存储数据的起始位置
uint8_t* mData;
// 从 mData 开始,顺延到目前的位置。即 mData+mDataPos 就是下一个数据要写入的起始位置
// 这里面的数据不仅指 IBinder 对象,还包括基本数据类型等
mutable size_t mDataPos;
// 存储有多少个 IBinder 对象
size_t mObjectsSize;
// 也是数组的起始位置。它的每一个元素记录了 flat_binder_object 相对于 mData 的地址偏移量
// 实际中 mObjects[mObjectsSize] = mDataPos;
binder_size_t* mObjects;
// 用于封装 IBinder 对象
struct flat_binder_object {
// hdr 只有一个 type 属性,它的值指 binder类型:BINDER_TYPE_BINDER 或 BINDER_TYPE_HANDLE
// 传 IBinder 对象时,值为 BINDER_TYPE_BINDER,否则值为 BINDER_TYPE_HANDLE
// 再简单说:进程传自己进程内的 IBinder 时值为 BINDER_TYPE_BINDER; 进程传其它进程的 IBinder 对象时值是 BINDER_TYPE_HANDLE
struct binder_object_header hdr;
__u32 flags;
/* 8 bytes of data. */
// 联合体。binder 与 handle 同时只能有一个有值
// 当 hdr = BINDER_TYPE_BINDER 时,binder 指向 IBinder 在本进程内的 BBinder 引用
// 当 hdr = BINDER_TYPE_HANDLE 时,handle 指向 Binder 对象在驱动中对应的引用
union {
binder_uintptr_t binder; /* local object */
__u32 handle; /* remote object */
};
/* extra data associated with local object */
binder_uintptr_t cookie;
};
struct binder_object_header {
__u32 type;
};
// 以下结构体都省略一部分属性
// 这个结构体主要用在驱动中,一般称之为 binder 实体
// 每一个 IBinder 对象在驱动中都有一个 binder_node 与之对应
// binder_proc 会以红黑树管理所有的 binder_node,比较节点时用的是其 ptr 属性
struct binder_node {
struct binder_proc *proc;
// 指向 IBinder 对象在宿主进程的首地址
binder_uintptr_t ptr;
binder_uintptr_t cookie;
};
// 一般称之为 binder 引用。它是连接 binder 实现与句柄的桥梁
// node 属性指向 binder 实现,data.desc 表示 binder 引用
// 所以,驱动中通过 binder 引用换 binder 实体依赖于该类
struct binder_ref {
struct binder_ref_data data;
struct binder_proc *proc;
struct binder_node *node;
};
struct binder_ref_data {
uint32_t desc;
};
我们以一个 IBinder 对象为例说明一下上面最后四个结构体的关系:
- 在宿主进程中 flat_binder_object::binder 指向该对象的实际首地址,在使用进程中 flat_binder_object::handle 指向驱动为该 IBinder 对象分配的句柄
- 每一个 IBinder 对象在驱动中都有一个 binder_node 与之对应,其 ptr 指向对象在宿主进程的首地址
- binder_ref 引用有 binder_node,binder_ref::data::desc 就是驱动为 IBinder 对象分配的句柄。
为了说明这一点,拿驱动中 binder_transaction() 一部分代码来看看,该方法由 binder_ioctl 调用。想要调用 IBinder 对象,必须首先找到它在驱动中对应的 binder_node,下面的代码就是查找过程:
// binder_transaction 方法节选
if (tr->target.handle) {
// 先通过句柄查找到 binder_ref
ref = binder_get_ref_olocked(proc, tr->target.handle,
true);
if (ref) {
// 这个方法可以理解为直接返回 ref->node
// 同埋通过 binder_node::proc 为 target_proc 赋值
// 该方法执行完后,就找到目标进程对应的 binder_proc
target_node = binder_get_node_refs_for_txn(
ref->node, &target_proc,
&return_error);
} else {
}
} else {
// 请求的是 service_manager,这里就可以直接赋值
target_node = context->binder_context_mgr_node;
if (target_node)
// 跟 if 块中一样,通过 binder_node::proc 为 target_proc 赋值
target_node = binder_get_node_refs_for_txn(
target_node, &target_proc,
&return_error);
}
// 参数 desc 就是传递的句柄
static struct binder_ref *binder_get_ref_olocked(struct binder_proc *proc,
u32 desc, bool need_strong_ref)
{
struct rb_node *n = proc->refs_by_desc.rb_node;
struct binder_ref *ref;
// 红黑树的查找,最终拿到对应的 binder_ref
while (n) {
// 里面也调用了 container_of,简单说就是拿到含有 n 的 binder_ref 对象
ref = rb_entry(n, struct binder_ref, rb_node_desc);
// 使用句柄和 ref->data.desc 比较
// 这就验证了上面说的第三点
if (desc < ref->data.desc) {
n = n->rb_left;
} else if (desc > ref->data.desc) {
n = n->rb_right;
} else if (need_strong_ref && !ref->data.strong) {
binder_user_error("tried to use weak ref as strong ref\n");
return NULL;
} else {
return ref;
}
}
return NULL;
}
从 AIDL 那一节可以看出,IBinder 传输使用的是 Parcel#writeStrongBinder() ,它调用了 Parcel::flattenBinder()
// 参数就是 IBinder 对象
status_t Parcel::flattenBinder(const sp<IBinder>& binder)
{
// 创建 flat_binder_object 结构体对象
flat_binder_object obj;
if (binder != nullptr) {
BBinder *local = binder->localBinder();
if (!local) {
// local 为 null 时走该分支,此时是客户进程向 binder 驱动中发送 IBinder 对象
// 这里我们假设是主动向 binder 驱动中发 IBinder,所以判断不成立
BpBinder *proxy = binder->remoteBinder();
// 拿到原来由驱动返回的 handle,即句柄
const int32_t handle = proxy ? proxy->handle() : 0;
obj.hdr.type = BINDER_TYPE_HANDLE;
obj.binder = 0;
obj.handle = handle; // 设置成句柄
obj.cookie = 0;
} else {
obj.hdr.type = BINDER_TYPE_BINDER;
// 指向本地 binder 的弱引用
obj.binder = reinterpret_cast<uintptr_t>(local->getWeakRefs());
// cookie 直接指向本地的地址
obj.cookie = reinterpret_cast<uintptr_t>(local);
}
} else {
}
return finishFlattenBinder(binder, obj);
}
status_t Parcel::finishFlattenBinder(
const sp<IBinder>& binder, const flat_binder_object& flat)
{
status_t status = writeObject(flat, false);
}
status_t Parcel::writeObject(const flat_binder_object& val, bool nullMetaData)
{
const bool enoughData = (mDataPos+sizeof(val)) <= mDataCapacity;
const bool enoughObjects = mObjectsSize < mObjectsCapacity;
// 还有扩容逻辑,不考虑,就当 if 判断成立
if (enoughData && enoughObjects) {
// 这一步:将 val 记录到 mData+mDataPos 处
// mData 是一个指针,可以理解为 Parcel 中用于存储数据的内存的起始地址
restart_write:
*reinterpret_cast<flat_binder_object*>(mData+mDataPos) = val;
// Need to write meta-data?
if (nullMetaData || val.binder != 0) {
// 记录 IBinder 对象所处的地址,即相对于 mData 的偏移量
mObjects[mObjectsSize] = mDataPos;
mObjectsSize++;
}
// finishWrite() 最核心的逻辑就是 mDataPos += len;
return finishWrite(sizeof(flat_binder_object));
}
// 省略扩容逻辑
}
到现在为止,IBinder 对象已经写入到 Parcel 中了。可以看一下传输时数据是如何写入的,这里截取的是 IPCThreadState#writeTransactionData() 一部分代码:
binder_transaction_data tr;
// 返回 Parcel 携带的数据量
tr.data_size = data.ipcDataSize();
// 返回 mData,即 Parcel 用于存储数据的起始位置
tr.data.ptr.buffer = data.ipcData();
// ipcObjectsCount() 返回的是 mObjectsSize,即 IBinder 对象的个数
tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);
// 返回 mObjects,即 Parcel 中用于记录 IBinder 对象与 mData 偏移量的数组
tr.data.ptr.offsets = data.ipcObjects();
现在 IPCThreadState 会将存储有 IBinder 对象信息的 flat_binder_object 的对象统一打包发送到驱动了。下面看一下驱动的处理:
// binder_transaction 节选,它最终由 binder_ioctl() 调用
case BINDER_TYPE_BINDER:
case BINDER_TYPE_WEAK_BINDER: {
struct flat_binder_object *fp;
// to_flat_binder_object 会调用 linux 中 container_of 宏
// 它的作用是:通过 hdr 地址获取到它所属的 flat_binder_object 对象的地址
fp = to_flat_binder_object(hdr);
// 这里面最终会创建一个 binder_node 对象 node
// 同时会将 node 加到 binder_proc 中
// 注意:node->ptr = fp->binder; 即指向了 IBinder 对象在其宿主进程中的地址
ret = binder_translate_binder(fp, t, thread);
// 将 fp 复制到目标进程,要注意此时的 fp 经过 binder_translate_binder() 的操作后
// fp::binder 的值是 0,但 handle 已经有值了
// 所以目标进程只能拿到 handle
binder_alloc_copy_to_buffer(&target_proc->alloc,
t->buffer, object_offset,
fp, sizeof(*fp));
} break;
case BINDER_TYPE_HANDLE:
case BINDER_TYPE_WEAK_HANDLE: {
// 这一部分 case 的逻辑与上面一样,只不过它是通过 handle 拿对应的 binder 实体
struct flat_binder_object *fp;
// 由 hdr 通过 contaner_of 拿到对应的 flat_binder_object
fp = to_flat_binder_object(hdr);
binder_alloc_copy_to_buffer(&target_proc->alloc,
t->buffer, object_offset,
fp, sizeof(*fp));
} break;
//....
// 后面一部分代码在驱动时已经说过,放这里只是说明:对 IBinder 的处理先于唤醒目标线程
if (reply) {
// ...
} else if (!(t->flags & TF_ONE_WAY)) {
binder_enqueue_deferred_thread_work_ilocked(thread, tcomplete);
t->need_reply = 1;
t->from_parent = thread->transaction_stack;
thread->transaction_stack = t;
return_error = binder_proc_transaction(t,
target_proc, target_thread);
} else {
}
IBinder 对象的传输有三个可能:1. binder 实体转句柄,2. 句柄转实体 3. 句柄转句柄。第一种情况是向 serviceManager#addService,第二种、第三中都是从 serviceManager#getService(),只不过拿到的实体可能属性当前进程。第一种情况对应的是上面的 case 块,第二种是下面的 case 块
实体转句柄(handle)
在 case BINDER_TYPE_BINDER 时有一点很重要:为 IBinder 对象创建与之对应的 binder_node 对象,并加到 binder_proc 中,同时 binder_node.ptr 指向 IBinder 在其宿主进程中的地址。这里并没有将 IBinder 复制到内核中,只是使用 binder_node 记录了它的地址。这一部分内容都是 binder_translate_binder() 方法的工作:
// fp->binder 指的是 IBinder 在其宿主进程的内存首地址
// thread 指源线程,即宿主进程中的某一个线程
static int binder_translate_binder(struct flat_binder_object *fp,
struct binder_transaction *t,
struct binder_thread *thread)
{
struct binder_node *node;
struct binder_proc *proc = thread->proc; // 宿主进程对应的 binder_proc
struct binder_proc *target_proc = t->to_proc; // 目标进程
// 创建一个 binder_ref_data 结构体对象
struct binder_ref_data rdata;
int ret = 0;
// binder_node 是由 binder_proc 按红黑树形式进行管理
// 每一个节点是按 fp->binder 进行比较的
// 即 binder_node 会以 IBinder 对象在宿主进程的首地址插入到 binder_proc 的红黑树中
// 从宿主进程中查找 binder 实体对应的 binder_node
node = binder_get_node(proc, fp->binder);
if (!node) {
// 没有,表示 binde 实体第一次进入驱动,要新建一个 binder_node 与之对应
// 同时将 node->ptr = fp->binder,即 node->ptr 值为实体在宿主进程中的首地址
// node->cookie = cookie,这也是实体在宿主进程中的首地址
// 这里会将新生成的 binder_node 挂载到 binder_proc 中
// 这一部分可参考上面的 Parcel::flattenBinder() 方法
node = binder_new_node(proc, fp);
}
// 经过上面的操作,binder 实体在驱动中已有对应的 binder_node
// 处理 binder_ref:将 node 与 binder_ref 关联,同时将 ref 与 binder_proc 关联
binder_inc_ref_for_node(target_proc, node,
fp->hdr.type == BINDER_TYPE_BINDER,
&thread->todo, &rdata);
// 修改 type
// 其实好理解:A 进程往驱动中写入 IBinder 实体,B 进程肯定不可能拿到实体,所以需要将 type 修改成 BINDER_TYPE_HANDLE
// 从引用转换成实体时,也一样
if (fp->hdr.type == BINDER_TYPE_BINDER)
fp->hdr.type = BINDER_TYPE_HANDLE;
else
fp->hdr.type = BINDER_TYPE_WEAK_HANDLE;
// 因为 fp 是要发送别的进程的,所以不能带上 binder,只需要加上 handle 即可
fp->binder = 0;
// 这个地方可以看出,fp->handle 的值就是 binder_ref_data.desc
// 下面主要看这个 desc 是如何变化的
fp->handle = rdata.desc;
fp->cookie = 0;
return ret;
}
// proc 是目标进程对应的 binder_proc
static int binder_inc_ref_for_node(struct binder_proc *proc,
struct binder_node *node,
bool strong,
struct list_head *target_list,
struct binder_ref_data *rdata)
{
struct binder_ref *ref;
struct binder_ref *new_ref = NULL;
int ret = 0;
// 从目标进程中中查找与 node 对应的 binder_ref
ref = binder_get_ref_for_node_olocked(proc, node, NULL);
if (!ref) {
// 找不到就新建
new_ref = kzalloc(sizeof(*ref), GFP_KERNEL);
// 该方法的分析见下面
ref = binder_get_ref_for_node_olocked(proc, node, new_ref);
}
// 赋值给参数
*rdata = ref->data;
return ret;
}
// proc 指目标进程
static struct binder_ref *binder_get_ref_for_node_olocked(
struct binder_proc *proc,
struct binder_node *node,
struct binder_ref *new_ref)
{
// 将 binder_node 存储到 binder_ref 中
new_ref->node = node;
// 这里只关注 data.desc 的处理,省略一部分代码
// 默认初始 handle。比如 service_manager 时下面的红黑树中没有节点,所以它的 desc 就是默认值 0
// 这就是常说的 service_manager 的 handle 为 0 的来源
new_ref->data.desc = (node == context->binder_context_mgr_node) ? 0 : 1;
// rb_first 与 rb_next 都是红黑树的操作逻辑
// rb_first 返回的是一棵红黑树的最左节点,即红黑树中的最小值
// 如果将节点按从小到大排序,rb_next 返回的是当前节点的下一个节点
// 因此这个 for 循环就是:将红黑树中的节点按从小到大排列,找到第一个没有用的数字赋值给 new_ref->data.desc
// 这里的 proc 指的是 target proc。也就是说:【binder_ref 拿到的句柄只跟自己的进程有关】
// 每一个进程拿到的 binder_ref 不相同,但它引用的 binder_node 相同
for (n = rb_first(&proc->refs_by_desc); n != NULL; n = rb_next(n)) {
ref = rb_entry(n, struct binder_ref, rb_node_desc);
if (ref->data.desc > new_ref->data.desc)
break;
new_ref->data.desc = ref->data.desc + 1;
}
// 将 new_ref 插入到红黑树中
p = &proc->refs_by_desc.rb_node;
while (*p) {
parent = *p;
ref = rb_entry(parent, struct binder_ref, rb_node_desc);
if (new_ref->data.desc < ref->data.desc)
p = &(*p)->rb_left;
else if (new_ref->data.desc > ref->data.desc)
p = &(*p)->rb_right;
else
BUG();
}
// 将 new_ref 添加到 binder_proc 中
rb_link_node(&new_ref->rb_node_desc, parent, p);
rb_insert_color(&new_ref->rb_node_desc, &proc->refs_by_desc);
return new_ref;
}
经上面执行后:目标进程拿到了 binder_ref,binder_ref::node 指向了 binder_node,binder_node::ptr 指向 binder 实体的首地址,而且 binder_node 对象也挂载到宿主进程对应的 binder_proc 中
从这里可知 IBinder 的所谓 handle 是什么了:它就是驱动为实体分配的一个整数值:。
目前为止,IBinder 对象在驱动中已经建立了 binder_node,也准备好了要发送目标进程的 flat_binder_object 对象,并复制到了目标进程,但目标进程只能拿到一个句柄,并以句柄为核心建立一套 BinderProxy。
句柄转实体或句柄
这种情况是第二种 case,方便分析这里也贴一下
case BINDER_TYPE_HANDLE:
case BINDER_TYPE_WEAK_HANDLE: {
struct flat_binder_object *fp;
// 通过 hdr 拿到 fp
fp = to_flat_binder_object(hdr);
// 见下面
// thread 指调用线程
ret = binder_translate_handle(fp, t, thread);
// 复制 fp
binder_alloc_copy_to_buffer(&target_proc->alloc,
t->buffer, object_offset,
fp, sizeof(*fp));
} break;
最主要的是 binder_translate_handle(),如下:
// thread 指把句柄发送到驱动中的线程
// 比如进程 A 需要调用 B 中某个 binder 实体的功能,这里的 thread 就指 A 中的某个线程
// 下面为了方便,也称为源线程、源进程
static int binder_translate_handle(struct flat_binder_object *fp,
struct binder_transaction *t,
struct binder_thread *thread)
{
struct binder_proc *proc = thread->proc;
struct binder_proc *target_proc = t->to_proc;
struct binder_node *node;
struct binder_ref_data src_rdata;
int ret = 0;
// 从源进程中拿 binder_node,这里一定可以拿到
// 源进程中已经有句柄了,那肯定有 binder_ref,就肯定会有关联到 binder_ref 中的 binder_node
node = binder_get_node_from_ref(proc, fp->handle,
fp->hdr.type == BINDER_TYPE_HANDLE, &src_rdata);
binder_node_lock(node);
if (node->proc == target_proc) {
// 如果目标进程就是 binder 实体的宿主进程,那就完美了
// 这意味着:我们找到实体的最终实现,可以调用方法了
// type 由 BINDER_TYPE_HANDLE 转成 BINDER_TYPE_BINDER
// target 拿到后才知道要将地址转成 binder 实体的指针
if (fp->hdr.type == BINDER_TYPE_HANDLE)
fp->hdr.type = BINDER_TYPE_BINDER;
else
fp->hdr.type = BINDER_TYPE_WEAK_BINDER;
// 通过 binder_node 还原成地址
fp->binder = node->ptr;
fp->cookie = node->cookie;
binder_inc_node_nilocked(node,
fp->hdr.type == BINDER_TYPE_BINDER,
0, NULL);
} else {
// 否则,target_proc 并不是 binder 实体的宿主
// 也就意味着 target_proc 只能拿到句柄
struct binder_ref_data dest_rdata;
// 这个方法在上面分析过,简单说下:
// 1. 为 target_proc 创建一个关联 binder_node 的 binder_ref,如果没有的话;如果有,就直接用
// 2. 根据 target_proc 自己的情况,为 binder_ref 生成一个句柄
ret = binder_inc_ref_for_node(target_proc, node,
fp->hdr.type == BINDER_TYPE_HANDLE,
fp->binder = 0;
// 记录
fp->handle = dest_rdata.desc;
fp->cookie = 0;
}
}
整个流程还是非常简单的:如果 target_proc 是 binder 实体的宿主进程就将句柄转成真实地址,否则再创建一个 binder_ref 并挂载到 target_proc 中
总结
上图只是画了大致流程,有一些细节:
-
binder_node 在驱动中只有一份, 存在于宿主进程对应的 binder_proc 中 -
binder_ref 可以有多份,每一个使用到 binder 实体的进程中都会有一份 -
binder 实体的句柄在不同进程中不同,除了 serviceManager,每一个使用进程会根据自己已使用的实体个数为新加的实体配置句柄——简单讲,句柄是递增的
匿名实体
通过某个方法返回给调用者一个 binder 实体,这个返回的实体就是匿名实体。源码中常见的就是 WindowSession。匿名实体与普通实现在传输中没有什么区别:
IWindowSession openSession(in IInputMethodClient client,
in IInputContext inputContext);
Parcelable
标识可以被 Parcel 存取的对象
在实现了 Parcelable 接口的类中,需要实现 writeToParcel() 以及参数类型为 Parcel 的构造函数。
- Parcel 通过调用 writeToParcel() 将一个对象拆成零件
- 然后通过调用橇函数,将零件还原成对象
这就是为什么Parcelable 子类的读写顺序必须一致的原因。
Bundle
实现 Parcelable 接口的一个类
其内部维护了一个 Map 对象,所有的 putXXX 方法都是先将数据存储到 Map 中。在 Bundle#writeToParcel() 时统一将 Map 中的数据写到 Parcel 中