每位 Android 行业的开发人员,Binder 通信机制是我们绕不开的技术,打开一个页面、点击一下屏幕、发送一个通知,看似平常的操作,背后都离不开 Binder 的默默支持,如果我们不能深刻的理解它,岂不是辜负了它的辛勤付出。
本文从以下4个角度来分析 Binder 通信机制是如何运作的。
- 从通信特点的角度分析,为什么 Android 选择 Binder 作为跨进程通信的机制;
- 从架构框架的角度分析,C/S 框架对应的各接口层(Java 层、Native 层、Driver 层);
- 从数据传递的角度分析,Binder 数据的一次拷贝;
- 从驱动模型的角度分析,Binder 驱动层对应的数据模型;
1、Binder 通信机制
- 什么是跨进程通信(IPC)?
- 什么是 Binder 通信?
- Android 为什么选择 Binder 通信机制?
我们通过上述三个问题,来回顾下 Binder 通信机制。
1.1、什么是跨进程通信(IPC)?
IPC(Inter-Process Communication),译为进程间通信,是指两个进程之间进行数据交换的过程。 IPC 不是 Android 系统所独有的,任何操作系统都有 IPC 方式; Android 系统基于 Linux 系统而来,Linux 系统提供了多种跨进程通信的方式,eg:管道(Pipe)、信号(Signal)、消息(Message)、共享内存(ShareMemory)和插口(Socket);上述跨进程的通信方式,因为通信特点和使用方式的要求,在 Android 系统中并不能满足需求,因此 Android 系统提供了 Binder 跨进程通信方式;
通信方式 | 特点 |
---|---|
管道(Pipe) | 面向字节流、先进先出、基于文件读写、只能单向传输、阻塞式读写、有固定的大小和缓冲区等;本质是利用内核提供的一块缓存区来实现进程间的数据通信;阻塞式通信、先进先出; |
信号(Signal) | 信号主要用于进程间事件通知,由自身或其他进程触发,通过内核转发到指定进程,每个信号有预定的处理操作;使用场景受限; |
消息(Message) | 通过消息队列实现进程间数据通信,消息队列由内核负责维护;独立于发送和接收进程、消息具有格式和优先级、可以按照类型读取消息;消息类型限制; |
共享内存(ShareMemory) | 每个进程的虚拟地址页面映射到同一块物理内存页,两个进程即可同时访问该内存数据,实现进程间通信,需要考虑同步问题;存在数据同步问题; |
插口(Socket) | 协议较多,通信成本较重,依赖通信端口;适用于不同机器间/网络通信; |
Binder | 基于 OpenBinder 实现,采用C/S架构,高效(一次数据拷贝)、轻量(无复杂通信协议)等特点;适用于数据量适中(1M内)的复杂业务通信; |
1.2、什么是 Binder 通信?
Binder 进程间通信机制是在 OpenBinder 的基础上实现的,采用 C/S 通信框架,其中,提供服务的进程称为 Server 进程,而访问服务的进程称为 Client 进程。Server 进程可以同时运行多个组件向 Client 进程提供服务,这些组件称为 service 组件(BBinder)。同时,Client 进程也可以同时向多个 service 组件请求通信,每一个请求对应有一个 service 代理组件(BpBinder)。Binder 进程间通信机制的 Server 进程和 Client 进程都维护一个 Binder 线程池来处理进程间的通信请求,因此 Server 进程和 Client 进程可以并发地进行通信。
Server 进程和 Client 进程的通信要依靠运行在内核空间的 Binder 驱动程序来进行。Binder 驱动程序向用户空间提供了一个设备文件/dev/binder
,使得应用程序进程可以间接地通过它来建立通信通道。
应用程序进程在启动时,会打开设备文件,创建一个 mDriverFD 文件描述符给到进程,进程通过 fd 与驱动建立了通信的桥梁,这样 Server 和 Client 的进程对象(binder_proc)就存在于内核 Binder 驱动中,进程对象维护着进程内的 service 组件(binder_node)或 service 代理组件(binder_ref),service 组件和 service
代理组件之间互相有引用关系,这样两个进程通过 Binder 驱动找到目标组件,并进行通信。
1.3、Android 为什么采用 Binder 通信?
结合 Linux 系统提供的 IPC 方式,各有优缺点,而 Android 系统期望拥有一套简单高效且符合业务场景的 IPC 方式,Binder 通信应运而生。
Binder 通信主要特点:
- 高效性:Binder 机制通过减少数据拷贝次数来提高 IPC 的效率。在 Binder 机制中,发送方只需要将数据从用户空间拷贝到内核空间一次,接收方可以直接访问内核空间中的数据,减少从内核空间再向用户空间的数据拷贝;
- 简洁易用:调用本地方法即可完成跨进程间通信,无感调用服务端方法;支持对象数据类型传输,数据类型支持 Parcel 序列化即可;
- 异步通信:通信请求通过 Binder 线程处理,不会阻塞主线程交互;
- 安全性:Binder 通过使用 UID 和 PID 验证请求的来源,提供了进程间通信的安全性保障。这意味着每个 Binder 事务都可以精确到发起者,系统可以据此实施安全策略,例如权限检查,防止未授权的通信操作。每个 Binder 通信都有明确的权限控制,可以限制哪些进程可以访问 Binder 服务,增强了系统的安全性。
基于以上 Binder 通信的特点,其更符合 Android 系统的业务场景,因此选择 Binder 作为 IPC 通信的方式。
2、Binder 通信架构
Binder 采用 C/S 架构,Client 端与 Server 端是运行在用户空间的进程,两个进程通过内核空间的 Binder 驱动进行通信。Client 端的 Binder 代理对象(BpBinder) 的 handle 变量,持有着 Binder 驱动的引用对象地址,引用对象的 node 成员则持有着实体对象地址,通过实体对象找到目标进程及本地对象(BBinder),Binder 驱动将事务提交到 Server 端的 Binder 线程进行处理。
Binder 通信架构图
Binder 对象介绍
- 实体对象:位于内核空间,通过 binder_node 结构体描述,结构体内的 cookie 成员是用户空间 BBinder 本地对象的地址;
- 引用对象:位于内核空间,通过 binder_ref 结构体描述,是 Binder 驱动中实体对象在 Client 端的代理对象,结构体内的 node 成员对应着实体对象的地址;
- 本地对象:位于用户空间,通过 BBinder 类描述,其子类 JavaBBinder 在类内部持有 Java 端 Binder 对象地址,通过 JNI 调用将通信传递到 Java 层;
- 代理对象:位于用户空间,通过 BpBinder 类描述,引用对象在用户空间的代理,其通过成员 mHandle 持有 Binder 驱动的引用对象地址;
Binder 通信的流程分析
- 运行在 Client 进程的代理对象(BpBinder)通过 Binder 驱动程序向运行在 Server 进程的本地对象(BBinder)发出通信请求,Binder 驱动根据 Client 进程传递的代理对象的句柄值(handle)找到引用对象(binder_ref) ,binder_ref 的 desc 成员与 handle 相同;
- Binder 驱动程序根据找到的引用对象(binder_ref),进而找到实体对象(binder_node),引用对象(binder_ref)的 node 成员是实体对象的地址;
- 创建一个事务(binde_transaction),用于描述通信请求,包括发起请求的源进程和源线程、目标进程和目标线程、从用户空间拷贝的数据、处理请求的 Binder 本地对象地址、工作项类型等;
- 通过 Binder 实体对象(binder_node)找到 Server 进程,并将事务提交到 Server 进程的 Binder 线程的事务队列;
- Binder 本地对象处理完请求后,将结果返回给 Binder 驱动;
- Binder 驱动根据事务内记录的 Client 端信息,将结果返回给 Binder 代理对象处理;
2.1、Binder Client 端架构
Client 端 Binde 通信时核心类介绍
- IXXX.Stub.Proxy 是 Java 端应用程序的代理对象,内部成员 mRemote 持有 JavaProxy 对象,其内部的业务方法通过 mRemote.transact 方法将请求转发到 BinderProxy 进行处理;
- BinderProxy 是 Java 端应用程序的代理对象,内部成员 mNativeData 持有 BpBinder 对象地址,BinderProxy 内有大量的 native 方法,通过 JNI 层调用实现与 Native 层的通信;Cpp 端应用程序实现 BinderProxy 职责的是 BpRefBase 类的子类;
- BpBinder 是 Client 端在用户空间的 Binder 代理对象,handler 成员持有内核空间引用对象(binder_ref)的地址,在 transact 方法内通过 IPCThreadState 对象实现与内核空间的通信;
- IPCThreadState 在进程内是单例对象,进程启动时初始化,并启动 binder 线程池,用于处理 binder 通信的任务,在 transact 方法内通过 ioctl 系统方法,实现从用户空间与内核空间的通信;
2.2、Client 端到驱动的通信流程
// IXXX.Stub.Proxy.java
public void addBook2(Book book) {
...
try {
// 写入请求 Binder 目标组件的描述符号
_data.writeInterfaceToken(DESCRIPTOR);
// 写入参数
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
// 发起请求,mRemote:BinderProxy
boolean _status = mRemote.transact(Stub.TRANSACTION_addBook2, _data, _reply, 0);
...
// 读取返回结果
if ((0!=_reply.readInt())) {
book.readFromParcel(_reply);
}
}
...
}
// BinderProxy
public boolean transact(int code, Parcel data, Parcel reply, int flags) {
...
try {
// 调用本地方法
return transactNative(code, data, reply, flags);
} finally {
...
}
}
// android_util_Binder.cpp
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
jint code, jobject dataObj, jobject replyObj, jint flags) {
// data 的 Java 对象转成 Cpp 对象
Parcel* data = parcelForJavaObject(env, dataObj);
...
// reply 的 Java 对象转成 Cpp 对象
Parcel* reply = parcelForJavaObject(env, replyObj);
...
// 获取到用户空间的 Binder 代理对象 BPBinder
IBinder* target = getBPNativeData(env, obj)->mObject.get();
...
// 调用 BPBinder.transact 方法
status_t err = target->transact(code, *data, reply, flags);
...
return JNI_FALSE;
}
// BpBinder.cpp
status_t BpBinder::transact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
...
// 通过 IPCThreadState 继续发送请求
status = IPCThreadState::self()->transact(binderHandle(), code, data, reply, flags);
...
}
// IPCThreadState.cpp
status_t IPCThreadState::transact(int32_t handle,
uint32_t code, const Parcel& data,
Parcel* reply, uint32_t flags)
{
status_t err;
...
err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, nullptr);
...
// 处理 ONE_WAY 类型请求,0:需要关注返回结果,1:不需要关注返回结果
if ((flags & TF_ONE_WAY) == 0) {
...
// 等待返回结果
waitForResponse(reply);
...
} else {
// 不需要关注返回结果
err = waitForResponse(nullptr, nullptr);
}
return err;
}
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
uint32_t cmd;
int32_t err;
while (1) {
// 与 Binder 驱动进行交互,talkWitchDriver 主要负责向驱动写入和读取数据
if ((err=talkWithDriver()) < NO_ERROR) break;
err = mIn.errorCheck();
if (err < NO_ERROR) break;
if (mIn.dataAvail() == 0) continue;
cmd = (uint32_t)mIn.readInt32();
switch (cmd) {
...
// 收到驱动返回的 BR_TRANSACTION_COMPLETE 协议,是否关注返回结果判断结束等待
case BR_TRANSACTION_COMPLETE:
if (!reply && !acquireResult) goto finish;
break;
...
case BR_REPLY:
{
binder_transaction_data tr;
err = mIn.read(&tr, sizeof(tr));
ALOG_ASSERT(err == NO_ERROR, "Not enough command data for brREPLY");
if (err != NO_ERROR) goto finish;
...
// 将返回结果写入 reply 返回 Client 进程
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);
}
goto finish;
...
}
}
finish:
if (err != NO_ERROR) {
// 返回失败信息
...
}
return err;
}
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
// 用户数据传递的结构体
binder_write_read bwr;
const bool needRead = mIn.dataPosition() >= mIn.dataSize();
const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0;
// 从Client 进程写入驱动的数据,即用户空间写入内核空间的数据
bwr.write_size = outAvail;
bwr.write_buffer = (uintptr_t)mOut.data();
// 接收 Server 进程返回的结果,即内核空间写入用户空间的数据
if (doReceive && needRead) {
bwr.read_size = mIn.dataCapacity();
bwr.read_buffer = (uintptr_t)mIn.data();
} else {
bwr.read_size = 0;
bwr.read_buffer = 0;
}
...
// 记录写入数据被消费的位置
bwr.write_consumed = 0;
// 记录返回数据被消费的位置
bwr.read_consumed = 0;
status_t err;
do {
...
#if defined(__ANDROID__)
// 调用系统函数,从用户空间转入内核空间,mDriverFD 是进程启动是打开驱动文件节点(/dev/binder) 返回的 Binder 驱动描述符
// BINDER_WRITE_READ 是通信协议类型
// bwr 是通信数据
if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
err = NO_ERROR;
else
err = -errno;
#endif
...
} while (err == -EINTR);
...
return err;
}
Client 端发起的 Binder 通信请求主要分为7步:
- Client 进程通过 Proxy 调用业务方法,内部通过 mRemote.transact 将请求转发到 BinderProxy 代理类进行处理;
- BinderProxy 负责与 JNI 层进行交互,transact 方法内会调用 transactNative 本地方法,将请求转入 JNI 层;
- 本地方法的实现是在 android_util_Binder.cpp 文件,transactNative 本地方法的实现是 android_os_BinderProxy_transact 方法,在本地方法内获取到用户空间的 Binder 代理对象(BpBinder),然后将请求通过 BpBinder.transact 转发;
- BpBinder 内部则通过调用 IPCThreadState.transact 方法进一步转发请求;
- IPCThreadState.transact 负责将 Client 进程请求的数据通过 BC_TRANSACTION 协议写入驱动;
- 如果不需要关注返回结果,则收到驱动返回的 BR_TRANSACTION_COMPLETE 协议,则本次 Binder 通信结束;
- 如果需要关注返回结果,则等待驱动返回的 BR_REPLY 协议,并解析内核空间返回到用户空间的数据,将结果返回到 Client 端,本次 Binder 通信结束;
2.3、Server 端 Binder 架构
// BpBinder 内 mHandle 成员的数据结构
Handle mHandle;
struct BinderHandle {
// 指向内核空间的 binder_ref 对象的地址
int32_t handle;
};
Server 端 Binder 通信时核心类介绍
- IPCThreadState 在进程内是单例对象,进程启动时初始化,并启动 binder 线程池,用于处理 binder 通信的任务,binder 驱动将事务提交到Server进程的 binder 线程的 todo 队列,binder 线程检测到任务后,从事务中解析出处理请求的目标 binder 对象(BBinder),并调用其 transact 方法,实现从内核空间与用户空间的通信;
- BBinder 位于 Cpp 层,是 Server 端在用户空间的 Binder 对象,其地址被内核空间的实体对象(binder_node)的 cookie 成员所持有;
- JavaBBinder 位于 JNI 层,是 BBinder 的子类,主要负责 Java 层与 Native 层的 Binder 通信,内部通过 mObject 变量持有 Java 层 Binder 子类的对象;
- JavaBinderHolder 位于 JNI 层,是将 Java 层的 Binder 与 JNI 层 JavaBBinder 建立关系的桥梁;
- Binder 位于 Java 层,其子类是 Java 应用程序 Server 端实现业务的组件,JavaBinder 的 mObject 变量是该对象地址;
- BnInteface 位于 Cpp 层,继承自 BBinder 类,用于描述一个业务接口类的模板类;
- BnXXXXX 位于 Cpp 层,是 Cpp 应用程序 Server 端实现业务的组件,其继承自 BnInterface 模板类;
2.4、Server 端和 Binder 驱动通信流程
// IPCThreadState.cpp
status_t IPCThreadState::executeCommand(int32_t cmd)
{
BBinder* obj;
RefBase::weakref_type* refs;
status_t result = NO_ERROR;
switch ((uint32_t)cmd) {
...
// 驱动发送到Server端的 BR_TRANSACTION 请求协议
case BR_TRANSACTION:
{
binder_transaction_data_secctx tr_secctx;
// 进程间通信传输数据的结构体,其中 cookie 成员是目标 binder 本地对象的地址
binder_transaction_data& tr = tr_secctx.transaction_data;
if (cmd == (int) BR_TRANSACTION_SEC_CTX) {
// 将内核空间的数据读取到 tr_secctx,因为建立了内存映射关系,在用户空间可以直接访问内核空间内存
result = mIn.read(&tr_secctx, sizeof(tr_secctx));
} else {
result = mIn.read(&tr, sizeof(tr));
tr_secctx.secctx = 0;
}
...
Parcel reply;
status_t error;
// prt 决定请求是发送给哪个目标binder,0表示 servicemanager 的 Binder 本地对象(固定值0);其他表示通过解析 cookie 获取 Binder 本地对象;
if (tr.target.ptr) {
...
// 将请求转发到 BBinder 的子类方法 transact 处理
error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,&reply, tr.flags);
...
} else {
// serviceManager 的 binder 本地对象,通过它可以访问注册的 binder 本地对象
error = the_context_object->transact(tr.code, buffer, &reply, tr.flags);
}
// 判断是否为 ONE_WAY 方法,0表示需要等待返回结果;1表示不需要等待方法返回结果;
if ((tr.flags & TF_ONE_WAY) == 0) {
...
// 给 Binder 驱动回复 BR_REPLY 命令协议,并将处理结果 reply 传会 Binder 驱动
sendReply(reply, (tr.flags & kForwardReplyFlags));
}
...
}
break;
...
return result;
}
<!---->
// android_util_Binder.cpp
status_t onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) override
{
...
// mObject 是 Java 层 Binder 的子类对象,调用它的 execTransact 方法,JNI 层通过 CallBooleanMethod 方法实现 Java 方法的调用
jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact,
code, reinterpret_cast<jlong>(&data), reinterpret_cast<jlong>(reply), flags);
...
return res != JNI_FALSE ? NO_ERROR : UNKNOWN_TRANSACTION;
}
<!---->
// Binder.java
private boolean execTransact(int code, long dataObj, long replyObj,
int flags) {
...
return execTransactInternal(code, dataObj, replyObj, flags, callingUid);
}
private boolean execTransactInternal(int code, long dataObj, long replyObj, int flags,
int callingUid) {
// 传递的参数
Parcel data = Parcel.obtain(dataObj);
// 用于结果回复
Parcel reply = Parcel.obtain(replyObj);
boolean res;
...
try {
// 将请求转发到 onTransact 方法,该方法在子类实现,通过 code 参数可以判断具体调用的是哪个业务方法
res = onTransact(code, data, reply, flags);
} catch (RemoteException|RuntimeException e) {
// 通过 reply 写入异常信息
res = true;
} finally {
...
}
...
return res;
}
Server 端处理 Binder 请求的流程主要分为6步:
- IPCThreadState 内的 binder 线程收到 Binder 驱动提交的事务之后,开始对其进行解析处理;
- 当收到 Binder 驱动发送的 BR_TRANSACTION 协议,开始读取内核空间的数据,解析出处理请求的 BBinder 本地对象和通信数据 binder_transaction_data(code、flags、data等);
- 将请求参数通过 BBinder 本地对象的 transact 方法将请求转发,transact 方法内部调用 onTransact 方法;
- 在 JavaBBinder 的 onTransact 方法内,通过 JNI 方式调用到 Java 层的 Binder 的 execTransact 方法,进而调用到 onTransact 方法;
- Binder 的子类(IXXX.Stub)是真正实现业务的组件,在 onTransact 方法内通过 code 值将请求转到对应的业务方法;
- 根据 ONE_WAY 类型决定是否回复 BC_REPLY 命令协议,如果 Binder 通信关注请求结果,则将结果写入Binder 驱动,并发送 BC_REPLY 协议;
3、Binder 内存机制
了解 Binder 的内存工作方式,可以从下面4个问题进行分析:
- 什么是用户空间和内核空间?
- 什么是 mmap 内存映射?
- Binder 如何通过 mmap 建立的内存映射关系?
- Binder 如何通过一次拷贝,实现数据跨进程的传递?
3.1、用户空间与内核空间
在 Linux 系统中的每个进程都有独立的内存空间,Linux 把内存空间划分为用户内存空间和内核内存空间;
为什么划分用户空间和内核空间,主要是从安全角度考虑,如果进程可以随意系统设备交互,则系统会变的很不稳定,比如内存的使用、CPU的占用等;通过划分内核区域,用户空间通过系统调用的方式与内核交合,而内核负责与系统设备的交互,可以确保系统稳定和资源的分配,进程崩溃只会影响单个进程,对系统不会产生危害;同样可以避免某个进程长时间占用系统资源而导致其他进程被阻塞的情况。
3.2、mmap 内存映射技术
mmap 是一种内存映射文件的方法,将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映到用户空间,从而可以实现不同进程间的文件共享。 mmap的作用,在应用中访问文件内容像访问内存一样。把文件映射到物理内存,同时将进程的虚拟空间(用户/内核)映射到物理内存。这样,进程不仅能像访问内存一样读写文件,多个进程映射同一文件,还能保证虚拟空间映射到同一块物理内存,达到内存共享的作用。
linux 内核使用 vm_area_struct 结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个 vm_area_struct 结构来分别表示不同类型的虚拟内存区域。各个 vm_area_struct 结构使用链表或者树形结构链接,方便进程快速访问。
vm_area_struct 结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个 vm_ops 指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用到的信息,都可以从 vm_area_struct 中获得。mmap()函数就是要创建一个新的 vm_area_struct 结构,并将其与文件的物理磁盘地址相连。
3.3、Binder 内存映射过程
用户空间和内核空间地址都是虚拟地址,内核空间地址与物理内存地址建立一一对应的关系。
Binder 通信内核空间与用户空间建立内存映射关系分析
// ProcessState.cpp
ProcessState::ProcessState(const char *driver)...
{
if (mDriverFD >= 0) {
// mmap the binder, providing a chunk of virtual address space to receive transactions.
// 用户空间调用 mmap 方法,初始化进程的内存映射
mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
...
}
...
}
// binder.c
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret;
// 进程在 Binder 驱动中的对象,进程启动时由 binder_open 方法打开虚拟文件 /dev/binder 时创建
struct binder_proc *proc = filp->private_data;
...
// 设置虚拟内存的操作方法
vma->vm_ops = &binder_vm_ops;
// 进程对象设置到虚拟内存的 vm_private_data 成员
vma->vm_private_data = proc;
// 建立内存映射关系,并记录在 proc->alloc 内
ret = binder_alloc_mmap_handler(&proc->alloc, vma);
...
}
// 内存映射方法
int binder_alloc_mmap_handler(struct binder_alloc *alloc,
struct vm_area_struct *vma)
{
...
// 用于描述内核缓冲区的结构体,Binder 通信的基本缓冲单元
struct binder_buffer *buffer;
...
// Binder驱动最多为进程分配4M内核缓冲区来传输进程间通信数据
alloc->buffer_size = min_t(unsigned long, vma->vm_end - vma->vm_start, SZ_4M);
...
// 记录用户空间起始地址
alloc->buffer = (void __user *)vma->vm_start;
// 计算内核空间内存页数量,PAGE_SIZE 标识每页大小4K
alloc->pages = kcalloc(alloc->buffer_size / PAGE_SIZE, sizeof(alloc->pages[0]), GFP_KERNEL);
...
// 内核缓冲区分配内存空间
buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
...
// 内核缓冲区记录用户空间地址,内核空间地址与用户空间地址建立关联
buffer->user_data = alloc->buffer;
// 将 buffer 添加到 alloc 的 buffers 队列内
list_add(&buffer->entry, &alloc->buffers);
// 标记空闲缓冲区
buffer->free = 1;
// 将空闲的内核缓冲区 buffer 添加到 alloc 的空闲红黑树
binder_insert_free_buffer(alloc, buffer);
...
}
/************************* 关键结构体 *************************/
// 描述内核缓冲区的结构体,binder通信是传递数据的基本单元
struct binder_buffer {
...
// 用于进程间传递数据的结构体
struct binder_transaction *transaction;
// service 组件在驱动中的对象
struct binder_node *target_node;
// 通信数据大小
size_t data_size;
// 通信数据中如果有binder对象,binder对象在buffer数组中的地址
size_t offsets_size;
// 指向用户缓冲区地址
void __user *user_data;
....
};
// 管理进程地址空间的结构体,在 binder_mmap 中被初始化,通过 buffers 跟踪内核和用户两个地址空间均可访问的 binder_buffer 对象;
struct binder_alloc {
struct mutex mutex;
// 用户空间地址
struct vm_area_struct *vma;
// vma->vm_mm 的副本
struct mm_struct *vma_vm_mm;
// 首个 binder_buffer 地址,binder_mmap 时分配;
void __user *buffer;
// binder_buffer 集合
struct list_head buffers;
// 空闲 binder_buffer 集合
struct rb_root free_buffers;
// 已使用的 binder_buffer 集合
struct rb_root allocated_buffers;
size_t free_async_space;
// 记录分配的内存页数量
struct binder_lru_page *pages;
// 记录进程用于 binder 通信的缓冲区大小
size_t buffer_size;
...
};
mmap 内存映射的实现过程,可以分为两个阶段:
- 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域。
- 进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
- 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址;
- 为此虚拟区分配一个 vm_area_struct 结构,接着对这个结构的各个域进行了初始化;
- 将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中;
- 通过系统调用函数 mmap, 进入 Binder 驱动的 binder_mmap 函数实现文件物理地址和进程虚拟地址的一一映射关系
- 通过 filp 找到驱动中进程对象 proc,对用户虚拟空间设置驱动内的进程对象和内存操作方法;
- 通过 binder_alloc_mmap_handler 方法对 proc 内的 binder_alloc 进行初始化操作;
- 记录进程分配的缓冲区空间大小、内存页和记录用户空间的虚拟地址;
- 申请一块内核缓冲区空间(初始化时仅申请一块,后续使用时会继续申请),并将用户缓冲区地址设置到 buffer->user_data 成员;
- 将新的 binder_buffer 加入缓冲区列表和空闲红黑树;
Binder 通过 mmap 方式,将用户空间缓冲区(vm_area_struct)与内核空间缓冲区(binder_buffer)建立了映射关系,并在 Binder 驱动各个进程对象的 alloc(binder_alloc) 成员维护其映射关系。
3.4、Binder 内存的一次拷贝
Binder 内存映射模型
3.4.1、Binder 一次内存拷贝的流程
// binder.c
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int ret;
// 进程对象
struct binder_proc *proc = filp->private_data;
// 工作线程
struct binder_thread *thread;
unsigned int size = _IOC_SIZE(cmd);
// 用户空间传递的通信数据
void __user *ubuf = (void __user *)arg;
...
// 获取工作线程
thread = binder_get_thread(proc);
...
switch (cmd) {
// 处理 Binder 通信命令
case BINDER_WRITE_READ:
ret = binder_ioctl_write_read(filp, cmd, arg, thread);
if (ret)
goto err;
break;
...
}
...
}
static int binder_ioctl_write_read(struct file *filp,
unsigned int cmd, unsigned long arg,
struct binder_thread *thread)
{
int ret = 0;
struct binder_proc *proc = filp->private_data;
unsigned int size = _IOC_SIZE(cmd);
void __user *ubuf = (void __user *)arg;
// 用户空间传输数据的结构体对象
struct binder_write_read bwr;
...
// 将用户空间传递过来的 binder_write_read 对象地址拷贝到 bwr,仅是对象地址的拷贝
if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
ret = -EFAULT;
goto out;
}
...
// Client/Server 端传递的数据放在 write_buffer 内
if (bwr.write_size > 0) {
ret = binder_thread_write(proc, thread,
bwr.write_buffer,
bwr.write_size,
&bwr.write_consumed);
...
}
// Driver 回复的命令数据放在 read_buffer 内
if (bwr.read_size > 0) {
ret = binder_thread_read(proc, thread, bwr.read_buffer,
bwr.read_size,
&bwr.read_consumed,
filp->f_flags & O_NONBLOCK);
...
}
...
}
用户空间与内核空间的数据拷贝,是通过系统函数实现 copy_fromm_user 和 copy_to_user;
- copy_to_user: 将数据从用户空间拷贝到内核空间;
- to: 目标地址,这个地址是用户空间的地址;
- from: 源地址,这个地址是内核空间的地址;
- size: 将要拷贝的数据的字节数;
- copy_from_user: 将数据从内核空间拷贝到用户空间;
- to: 目标地址,这个地址是内核空间的地址;
- from: 源地址,这个地址是用户空间的地址;
- size: 将要拷贝的数据的字节数;
进程与驱动数据处理的过程,主要分为6步:
- Client 端发起通信请求经系统函数 ioctl,将请求转入 Binder 驱动内,由 binder_ioctl 方法处理;
- 进程通信时经 ioctl 函数发送的通信协议为 BINDER_WRITE_READ,由 binder_ioctl_write_read 处理;
- 进程请求数据存放在 binder_write_read 的 write_buffer 成员内,Binder 驱动通过 binder_thread_write 方法处理通信数据;
- 将通信数据和目标组件等信息组成 binder_transaction 事务,并将其提交到目标组件的 todo 队列处理;
- Binder 驱动通过 binder_thread_read 方法给 Cient/Server 进程回复一个通信命令;
- Server 将处理结果经系统函数 ioctl,将回复转入 Binder 驱动内,如过程 2-5;
3.4.2、用户空间向内核空间数据拷贝的过程
// 用户空间
static int binder_thread_write(struct binder_proc *proc,
struct binder_thread *thread,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed)
{
...
while (ptr < end && thread->return_error.cmd == BR_OK) {
...
switch (cmd) {
...
case BC_TRANSACTION:
case BC_REPLY: {
struct binder_transaction_data tr;
// 将用户空间传递过来的 binder_transaction_data 对象地址拷贝到 tr,仅是对象地址的拷贝
if (copy_from_user(&tr, ptr, sizeof(tr)))
return -EFAULT;
ptr += sizeof(tr);
// 真正拷贝数据的方法
binder_transaction(proc, thread, &tr,
cmd == BC_REPLY, 0);
break;
}
...
}
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply,
binder_size_t extra_buffers_size)
{
...
if (reply) {
...
// 找到待回复的目标线程和目标进程
target_thread = binder_get_txn_from_and_acq_inner(in_reply_to);
target_proc = target_thread->proc;
...
} else {
// Client 端发起通信请求的逻辑,此处的 handle 即 BPBinder 内记录的 mHandle.handle 成员,对应着内核空间的 binder_ref 对象
// BpBinder.cpp
// int32_t BpBinder::binderHandle() const {
// return std::get<BinderHandle>(mHandle).handle;
// }
// IPCThreadState::self()->transact(binderHandle(), code, data, reply, flags);
if (tr->target.handle) {
struct binder_ref *ref;
binder_proc_lock(proc);
// 找到 Client 端的在 Binder 驱动的 service 代理组件,即引用对象(binder_ref)
ref = binder_get_ref_olocked(proc, tr->target.handle, true);
// 通过引用对象(Client 端代理组件)找到实体对象(Server 组件)
if (ref) {
target_node = binder_get_node_refs_for_txn(...);
}
...
} else {
// system_manager 对象,handle 默认是0
...
}
...
// 创建线程处理的工作项 binder_work
w = list_first_entry_or_null(&thread->todo, struct binder_work, entry);
}
// 分配内核缓冲区
t = kzalloc(sizeof(*t), GFP_KERNEL);
...
// 设置目标进程
t->to_proc = target_proc;
// 设置目标线程
t->to_thread = target_thread;
// 设置请求 code, 即业务方法的编码
t->code = tr->code;
...
// binder_proc->alloc 在 binder_mmap 函数中被初始化,成员记录的是进程的内存映射关系
// 将用户空间的数据拷贝到目标进程的内核空间
if (binder_alloc_copy_user_to_buffer(
&target_proc->alloc,
t->buffer, 0,
(const void __user *)
(uintptr_t)tr->data.ptr.buffer,
tr->data_size)) {
...
}
// 将用户空间的 offsets 内容(binder对象)拷贝到目标进程的内核空间
if (binder_alloc_copy_user_to_buffer(
&target_proc->alloc,
t->buffer,
ALIGN(tr->data_size, sizeof(void *)),
(const void __user *)
(uintptr_t)tr->data.ptr.offsets,
tr->offsets_size)) {
...
}
...
}
binder_thread_write 职责是将用户空间数据拷贝到内核空间,并将通信请求转入到目标组件的事务队列;
用户空间向驱动传递数据,主要分为4步:
- 处理命令协议 BC_TRANSACTION(Client 请求命令)/BC_REPLY(Server 回复命令,非 ONE_WAY 需回复),由 binder_transaction 方法处理;
- 确定通信双方的信息,目标组件信息、发起请求端信息等;
- 确定了目标进程之后,为目标组件分配内核空间,将用户空间数据拷贝到内核空间;
- 将双方信息、通信数据封和通信类型封装到 binder_transaction 事务中,并将其提交到目标组件的事务队列;
3.4.3、内核空间向用户空间数据拷贝的过程
static int binder_thread_read(struct binder_proc *proc,
struct binder_thread *thread,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed, int non_block)
{
// 内核空间的数据
void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
// 通过内核空间与用户空间的固定差值,计算出用户空间地址
void __user *ptr = buffer + *consumed;
// 计算出本次使用的用户空间结束地址
void __user *end = buffer + size;
...
retry:
...
while (1) {
uint32_t cmd;
struct binder_transaction_data_secctx tr;
struct binder_transaction_data *trd = &tr.transaction_data;
struct binder_work *w = NULL;
struct list_head *list = NULL;
struct binder_transaction *t = NULL;
struct binder_thread *t_from;
...
// 将 Binder 驱动回复进程的数据从内核空间拷贝到用户空间
if (copy_to_user(ptr, &tr, trsize)) {
...
}
ptr += trsize;
...
}
...
}
binder_thread_write 方法处理进程发送的数据之后,Binder 驱动需要给进程一个答复;binder_thread_read 即负责驱动的答复操作,binder_thread_read 方法主要处理 Binder 驱动向用户进程回复的命令协议(BR_XXX)。
内核空间向用户空间传递数据,主要分为2步:
- 根据通信的命令协议类型(binder_work),驱动作出对应的答复命令(BR_XXX);
- 进程接收到 BR_XXX 命令,处理返回数据;
4、Binder 驱动模型
binder 方法映射关系
用户空间方法 | 内核空间方法 | 方法描述 |
---|---|---|
init | binder_init | binder 驱动初始化方法 |
open | binder_open | binder 驱动打开方法 |
mmap | binder_mmap | binder 驱动内存映射方法 |
ioctl | binder_ioctl | binder 驱动通信方法 |
// Binder 驱动中的进程对象
binder_proc {
// Binder 驱动中的进程对象链表
struct hlist_node proc_node;
// 进程中的线程红黑树
struct rb_root threads;
// 进程中的实体对象红黑树(实体对象红黑树)
struct rb_root nodes;
// 进程中描述引用对象的句柄值红黑树(引用对象红黑树)
struct rb_root refs_by_desc;
// 进程中描述引用对象所关联的实体对象红黑树(引用对象对应的实体对象红黑树)
struct rb_root refs_by_node;
// 进程内存映射关系
struct binder_alloc alloc;
...
}
// Binder 驱动中的 Server 组件,即实体对象
binder_node {
// 实体对象所在的 Server 端进程
struct binder_proc *proc;
// 实体对应被引用的对象集合(引用对象集合)
struct hlist_head refs;
...
}
// Binder 驱动中的 Server 代理组件,即引用对象
binder_ref {
// 引用对象描述符号的红黑树节点
struct rb_node rb_node_desc;
// 引用对象所对应的实体对象红黑树节点
struct rb_node rb_node_node;
// 引用对象所在的 Client 端进程
struct binder_proc *proc;
// 引用对象所对应的实体对象
struct binder_node *node;
...
}
// Binder 驱动中传递数据的数据结构,记录通信相关的进程、线程、组件、数据等信息
binder_transaction {
// 描述数据类型
struct binder_work work;
// 描述发起请求的线程
struct binder_thread *from;
struct binder_transaction *from_parent;
// 描述处理请求的进程
struct binder_proc *to_proc;
// 描述处理请求的线程
struct binder_thread *to_thread;
struct binder_transaction *to_parent;
// 是否需要等待返回结果,1:需要; 0: 不需要;
unsigned need_reply:1;
// 保存请求参数 data、实体对象 binder_node 等数据,指向内核空间的内存
struct binder_buffer *buffer;
// 进程间约定的通信Code,指向某个方法的常量值
unsigned int code;
// 通信特征标识,eg: ONW_WAY
unsigned int flags;
...
}
// 进程与驱动通信时,ioctl 系统调用函数传输数据的载体
// write/read 是针对 Binder 驱动而言;
// write: 标识输入数据,进程写入驱动;
// read: 标识输出数据,驱动写回进程;
struct binder_write_read {
// 输入数据大小,单位字节
binder_size_t write_size;
// 输入数据已处理的大小,单位字节
binder_size_t write_consumed;
// 输入数据
binder_uintptr_t write_buffer;
// 输出数据大小,单位字节
binder_size_t read_size;
// 输出数据已处理的大小,单位字节
binder_size_t read_consumed;
// 输出数据
binder_uintptr_t read_buffer;
};
// 描述进程通信请求的数据,会被放入 binder_write_read 的 write_buffer 成员
struct binder_transaction_data {
// target 描述目标 Binder 实体对象或 Binder 引用对象
union {
// 指向 Binder 引用对象
__u32 handle;
// 指向 Binder 实体对象
binder_uintptr_t ptr;
} target;
// 描述 service 组件的地址,即 BBinder 对象
binder_uintptr_t cookie;
// 进程间约定的通信Code,指向某个方法的常量值
__u32 code;
// 通信特征标识,eg: ONW_WAY
__u32 flags;
// 发起通信的进程PID
pid_t sender_pid;
// 发起通信的进程UID
uid_t sender_euid;
// 通信数据缓冲区大小
binder_size_t data_size;
// 通信数据偏移数组大小
binder_size_t offsets_size;
// data 描述通信数据缓冲区
union {
struct {
// 指向 IPC 数据,_data(Parcel)
binder_uintptr_t buffer;
// 指向 Binder 对象
binder_uintptr_t offsets;
} ptr;
__u8 buf[8];
} data;
};
通过 binder_proc、binder_node、binder_ref、binder_transaction 等数据结构描述了驱动中的 Binder 通信模型;
- binder_proc: 描述进程在 Binder 驱动中的对象;
- binder_node: 描述 Binder 实体对象;
- binder_ref: 描述 Binder 引用对象;
- binder_transaction: 描述 Binder 驱动将传递通信数据的事务,会被提交到目标组件todo对象的事务;
通过 binder_write_read、binder_transaction_data 等数据结构描述了进程与驱动间的 Binder 通信模型;
- binder_write_read: 描述了 ioctl 函数传递的数据形式;
- binder_transaction_data: 描述了通信参数、目标组件、通信方式等信息,并将其放入 binder_write_read 数据结构;
5、总结
Binder 通信是 Android IPC 的基础,组件间的通信几乎都采用了 Binder 通信方式,我们可以向调用本地方法一样实现跨进程间的数据交换,所以理解 Binder 通信的机制,对我们学习 Android 有很大帮助。最后通过 Binder 通信协议和通信数据传递两个方面再回顾下 Binder 通信的过程。
5.1、Binder 通信命令协议交互流程
Binder 通信定义了一套轻量的通信协议,下图梳理了 Binder 通信时命令协议的交互过程。
Binder 命令协议包含在 IPC 数据中,分为两类:
BINDER_COMMAND_PROTOCOL
:Binder 请求码,以 "BC_" 开头,简称 BC 码,用于从 IPC 层传递到 Binder Driver 层;BINDER_RETURN_PROTOCOL
:Binder 响应码,以 "BR_" 开头,简称 BR 码,用于从 Binder Driver 层传递到 IPC 层;
Binder 通信协议的交互规则:
- Client 进程执行 binder_thread_write,根据 BC_XXX 命令,生成相应的 binder_work;
- Server 进程执行 binder_thread_write,返回 BC_XXX 命令,生成相应的 binder_work;
- Driver 进程执行 binder_thread_read,根据 binder_work.type 类型,生成 BR_XXX,发送到用户空间处理;
5.2、Binder 通信数据传递流程
Binder 通信数据的传递流程细节很复杂,下图梳理了 Binder 通信时数据的传递过程。
Binder 通信数据传递过程,主要分为7步:
- Client 端发起请求,从 Java 层开始,经 JNI 调用,将请求转入 Native 层,通过 ioctl 系统函数,发起 BINDER_WRITER_READ 通信协议,将请求命令 BC_TRANSACTION 转入驱动层;
- Binder 驱动收到请求后,给 Client 端回复 BR_TRANSACTION_COMPLETE 命令;
- 通过 handle 找到代理组件,进而确定请求的目标组件,将通信数据从 Client 进程的用户空间拷贝到 Server 进程的内核空间(copy_from_user);
- 创建通信事务 binder_transaction,将其提交到 Server 端的事务队列中,通知 Server 端 Binder 线程处理事务;
- Server 端收到通信请求之后,将请求转发到 service 组件的业务方法进行处理,如果需要回复,则通过 ioctl 系统调用函数,发起 BINDER_WRITE_READ 通信协议,将处理结果和返回命令 BC_REPLY 发送到 Binder 驱动;
- Binder 驱动收到答复之后,给 Server 端回复 BR_TRANSACTION_COMPLETE 命令,并将结果从 Server 进程的内核空间返回到 Client 的用户空间(copy_to_user),并回复 Client 端 BR_REPLY 命令;
- Client 端收到驱动返回的数据之后,本次通信结束;
Binder 通信从上层到驱动层,Binder 通信库和驱动层实现链路较长,涉及多个框架层的调用,梳理清楚 Binder 通信的框架流程,对我们分析实现细节很有帮助,Binder 通信使用的技术和设计思想,在我们开发中同样可以应用到项目内,很有借鉴意义,比如:JNI 层的调用、mmap 内存映射的方案、C/S架构模型、代理模式等;
任何疑问欢迎在评论区交流~