聊聊怎样学习Binder

时间:2022-07-25
本文章向大家介绍聊聊怎样学习Binder,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

Binder可以说是整个Android框架最重要的一个基础。如果不能吃透Binder,就谈不上对Android有多么深刻的理解。这个道理相信大部分Android开发者都清楚。但是Binder整个框架又看起来好像深不可测,上至Java,下至kernel。又横跨众多进程和服务。想看看源代码来学习。看着看着就看不明白了,不由得发出“我是谁?我在哪里?”这样的疑问。这篇文章就想从作者个人的经验来聊聊如何学习Binder。

Binder的学习

Binder的学习和学习其他源代码一样并没有什么特殊的诀窍。那就是硬着头皮看源码,看文章。反复的看,不停的想,至少把妨碍自己理解的堵塞点都搞懂了,整个链路从上到下,从下到上都无障碍的跑通了那才算真正掌握了Binder。当然这也是学习所有其他框架源码的要求。关于学习源码的更多方法,可以参考我的另一篇文章我是如何学习Flutter源码的

源码浩如烟海,看着看着就不知道跑哪里去了,没有关系。可以参考前人的经验来为自己带路。然而互联网上说Binder的文章可谓多如牛毛。如何选择呢?这里我推荐一位大牛的Binder系列文章。一遍看不懂那就多看几遍,而且要结合Android源码去看。文章里的每一个代码片段最好都自己去源码里对照着研究。这个一是为了加深记忆,二是为了促进消化。

如果全搞明白了,那么恭喜你,这篇文章剩下的内容不用再看了。如果是那种刚看完觉得自己都会了,过一段时间别人问你你又说不出来个子丑寅卯。这种情况是因为你只是死记硬背记住了关于Binder的一些东西而没有完全理解Binder,有一些关于Binder通信的关键点没有搞清楚,知其然而不知其所以然。处于“半懂不懂”的状态。如果此时放弃学习Binder那就太可惜了。

写这篇文章呢,就是希望能用尽量少的搬运源码的方式来捋一下Binder。希望能帮你疏通堵点,彻底理解Binder。

正如我在前言里说的,Binder深不可测,上至Java,下至kernel。而程序员似乎会有个习惯,不太喜欢去挖自己领域之外的东西,喜欢Java的就不想去看C/C++的东西,搞APP的,搞Framework的不想去瞅一眼kernel里面长啥样。“我只要知道这东西API是啥,会用就行了。。。”。某些情况下这可能是没什么问题的。但不适用于Binder。

要彻底理解Binder,必须要理解Binder驱动(Driver)。

很多对Binder一知半解的同学通常都是卡在这个地方了,不是说硬记住那几个“BC_TRANSACTION”,“BR_REPLY”命令,然后交互流程是啥样就可以了。但如果此时你把这些东西也忘了,这篇文章也就不用往下看了,这边建议你回头把上面说的Binder系列文章撸几遍再来。

如果你现在处于“半懂不懂”的状态,那么接着看下面的章节能不能帮到你来理解Binder。

Binder的理解

本章节假设读者对Binder架构已经有了初步的了解。所以并不会很详细的搬源码。大部分东西都会直接拿来用。涉及驱动的部分也不会做详尽说明,只会把一些关键点串起来。文中也不会涉及Java Binder。Java Binder在这里完全是干扰。

概述

这里首先列一下Binder中的一些内容:

  • 除了service_manager,其他应用级进程/线程,和Binder驱动打交道的都是IPCThreadState
  • 我们只关注进程间通信的核心流程:transaction
  • 传输发起者需要告诉Binder三个东西:handle(你找谁?), cmd(你要干啥?)和data(东西呢?)。
  • 在客户端handleBpBinder持有。不管什么样的服务在Binder眼里就是一串数字。记住这一点。
  • 在server端真正的服务是BBinder来完成的。
error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,
                            &reply, tr.flags);
  • 显然Binder驱动帮我们完成了从本地的一串数字handle到远端实体服务BBinder的转换。

这里就引出了几个问题:

  1. 本地handle是如何转换为远端BBinder的?
  2. 这个handle是从哪里来的呢?

第二个问题可能有些同学会说是从service_manager那里查来的,然而并没有这么简单。这个问题我们稍后再解释。这里先假设我们已经有了这个handle来说明第一个问题。

Binder驱动传输流程

这个流程其实就只有三步:第一,客户端把数据写进来。第二,把数据从客户端传到服务端。第三,通知服务端把数据读走。

这个流程我希望大家能紧盯着handle因为它是我们理解问题1的唯一线索。

客户端把数据写进来

在用户空间,函数IPCThreadState::writeTransactionData()负责写入驱动。 这个函数会构造一个binder_transaction_data的结构体tr。我们的handle会被写进去。

status_t IPCThreadState::writeTransactionData(...
    int32_t handle, ...)
{
    binder_transaction_data tr;

   ...
   // handle在这里
    tr.target.handle = handle;
    tr.code = code;
   ...
}

然后就到了驱动那里

static int binder_thread_write(...) {
...
//我们只关注BC_TRANSACTION
case BC_TRANSACTION:
case BC_REPLY: {
            struct binder_transaction_data tr;
            //tr到了这里
            if (copy_from_user(&tr, ptr, sizeof(tr)))
                return -EFAULT;
            ptr += sizeof(tr);
            //这个函数做传输工作
            binder_transaction(proc, thread, &tr,
                       cmd == BC_REPLY, 0);
            break;
        }
 ...
}

写进来的可能是各种命令,我们只关注transaction

把数据从客户端传到服务端

这事是由函数binder_transaction()来干的。注意看一下那几个入参,proc代表客户端进程。thread代表客户端线程,tr呢就是最开始的那个,记住里面放着handle

传输的前提是要找到目标的进程线程。而我们唯一的线索就是handle。怎么通过handle来获得目标进程线程呢?

static void binder_transaction(...) {
...
if (tr->target.handle) {
            //通过handle查找目标引用
            target_ref = binder_get_ref(proc, tr->target.handle,
                         true);
            //通过目标引用得到目标节点。
            target_node = target_ref->node;
        }
        ...
}
//从node里拿到目标进程的信息
target_proc = target_node->proc;
...
//然后搞了个binder_transaction的结构体
t = kzalloc(sizeof(*t), GFP_KERNEL);
...
//设置目标进程/线程
t->to_proc = target_proc;
t->to_thread = target_thread;
...
//设置目标节点
t->buffer->target_node = target_node;
...
//要干的活是传输
t->work.type = BINDER_WORK_TRANSACTION;
...
//把传输任务插入到目标进程/线程的todo队列。
binder_proc_transaction(t, target_proc, target_thread);
...

把数据从客户端传到服务端阶段就完成了。从上述关键代码我们可以看到,要完成传输就需要拿到目标进程的信息。而我们只有handle。驱动会先拿这个handle在自身进程信息proc内查找节点的应用,然后由这个引用就能得到节点,得到节点就能拿到目标进程的信息target_proc了。那么ref是啥?node又是啥呢?为什么他们能通过handle找的到呢?

node可以说是Binder驱动的核心数据结构。node会以红黑树的形式保存在服务端进程结构体内,客户端则保存的是它的引用ref。而handle来自ref->desc

noderef是什么时候保存到Binder驱动中的呢?这个我们在后面解决handle从哪里来的问题再解释。

所以在传输阶段我们看到handle换成了node。在插入到目标进程todo队列的数据里只有node而没有handlehandle已经完成了它的使命,接下来让我们关注node

通知服务端把数据读走

static int binder_thread_read(...) {

...
//有活干了
w = list_first_entry(...);
...
//看看是什么活,我们只关心transaction
switch (w->type) {
case BINDER_WORK_TRANSACTION: {
            t = container_of(w, struct binder_transaction, work);
        } break;
}
...
if (t->buffer->target_node) {
            tr.target.ptr = target_node->ptr;
            tr.cookie =  target_node->cookie;
            ...
            cmd = BR_TRANSACTION;
        }
        ...
        tr.code = t->code;
}

cmd设置成了BR_TRANSACTIONbinder_thread_read()会拼一个binder_transaction_data。也就是tr出来。它是要被送往用户空间的,就像之前从用户空间往binder驱动里写的也是一个tr一样。这里注意驱动把node里的两个东西设置给了trptrcookie。记住它们。

接下来就回到用户空间了:

status_t IPCThreadState::executeCommand(int32_t cmd) {

//我们只关心传输
  case BR_TRANSACTION:
        {
            if (tr.target.ptr) {
                if (reinterpret_cast<RefBase::weakref_type*>(
                        tr.target.ptr)->attemptIncStrong(this)) {
                    error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,
                            &reply, tr.flags);
                    reinterpret_cast<BBinder*>(tr.cookie)->decStrong(this);
                } 
        }
        break;
}

啊哈,这里我们就能看出来原来cookie的真身就是BBinder。也就是我们服务的真身了啊。

所以捋一下Binder驱动的传输过程就是从客户端的handle在自身进程信息proc里查找ref。再通过ref就能得到node。而node则是存储在服务端进程信息里的。node就很关键了,从它身上能找到目标进程的信息和目标服务的真身BBinder实例。然后回到用户区直接调用就好了。

传输过程就介绍完了,接下来我们还有解决另外一个系列问题,handle从从哪里来?node又是什么时候给保存到内核里去的呢?

对了,这就涉及到services_manager了。

服务注册及获取

我们知道services_manager是个特殊的服务。有点像个DNS服务器,其他什么AMS,PMS等等都需要先到它那里注册以后,客户们才能在services_manager找到他们。既然services_manager这么特殊,那它在驱动中肯定会有个独一无二的node吧。

所以services_manager启动的时候会给Binder驱动发这么个命令BINDER_SET_CONTEXT_MGR。 Binder驱动照办:

static int binder_ioctl_set_ctx_mgr(struct file *filp)
{
    ...
    temp = binder_new_node(proc, 0, 0);
    ...
    context->binder_context_mgr_node = temp;
    binder_put_node(temp);
...
}

函数binder_new_node()是用来新建node

static struct binder_node *binder_new_node(struct binder_proc *proc,
                       binder_uintptr_t ptr,
                       binder_uintptr_t cookie)

三个入参,procptrcookie。回想一下上面介绍传输过程的时候这三个参数是不是都用到过?当然,对于services_manager来说只需要进程信息,其他两个入参都是0。这个特殊的node被设置给context->binder_context_mgr_node

接下来让我们看一下服务注册过程:

注册服务

注册服务的要点是盯住服务实体Binder是如何转化为node的。

我们都知道service_manager特殊,特殊就特殊在它的handle就是0。所以无论是注册服务还是获取服务,我们不需要去别处问,自己就可以搞出来个代表service_managerBpBinder。注册的时候是这样的

virtual status_t addService(const String16& name, const sp<IBinder>& service,
                                bool allowIsolated, int dumpsysPriority) {
        Parcel data, reply;
        data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
        data.writeString16(name);
        data.writeStrongBinder(service);
        data.writeInt32(allowIsolated ? 1 : 0);
        data.writeInt32(dumpsysPriority);
        status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);
        return err == NO_ERROR ? reply.readExceptionCode() : err;
    }

注意这句data.writeStrongBinder(service);也就是说把服务自身给写到要传输的数据里面去了。 写成啥样了呢?

status_t flatten_binder(){
...
flat_binder_object obj;
...
 IBinder *local = binder->localBinder();
        if (!local) {
            BpBinder *proxy = binder->remoteBinder();
            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;
            obj.binder = reinterpret_cast<uintptr_t>(local->getWeakRefs());
            obj.cookie = reinterpret_cast<uintptr_t>(local);
        }
...
}

因为这里是注册服务,所以给的Binder是实体Binder,走的是else分支。类型为BINDER_TYPE_BINDERcookie设置为service实体。

注册服务是一次handle为0的Binder传输。回想我们之前讨论的传输过程,handle如果是0的话会在传输过程走不一样的分支:

static void binder_transaction(...) {
...
if (tr->target.handle) {
            ...
        } else {
            target_node = context->binder_context_mgr_node;
        }
//从node里拿到目标进程的信息
target_proc = target_node->proc;

...
        switch (hdr->type) {
        case BINDER_TYPE_BINDER:
        case BINDER_TYPE_WEAK_BINDER: {
            struct flat_binder_object *fp;
            fp = to_flat_binder_object(hdr);
            ret = binder_translate_binder(fp, t, thread);
        } break;
        case BINDER_TYPE_HANDLE:
        case BINDER_TYPE_WEAK_HANDLE: {
            struct flat_binder_object *fp;
            fp = to_flat_binder_object(hdr);
            ret = binder_translate_handle(fp, t, thread);
        } break;

...

//把传输任务插入到目标进程/线程的todo队列。
binder_proc_transaction(t, target_proc, target_thread);
...

对于注册服务的流程来说在传输的时候有两个地方需要注意,

  1. 由于handle是0,所以直接会找到binder_context_mgr_node。这个node也就是我们之前看到的service_manager初始化的时候创建的,也就是说我们直接就扎到了service_manager
  2. 还记得我们之前把服务实体放在传输数据里面了吗?这里驱动会检查传输的数据,如果发现有BINDER_TYPE_XXX类型的都要做转换,为啥要转呢?因为我的实体到你那里就得是个handle啊, 否则都不在同一个进程,不转换的话你拿我的实体也没用啊。所以,这里我们的服务实体要做一次转换了:
static int binder_translate_binder(struct flat_binder_object *fp,
                   struct binder_transaction *t,
                   struct binder_thread *thread)
{
    struct binder_node *node;
    struct binder_ref *ref;
    struct binder_proc *proc = thread->proc;
    struct binder_proc *target_proc = t->to_proc;

    node = binder_get_node(proc, fp->binder);
    if (!node) {
        node = binder_new_node(proc, fp->binder, fp->cookie);
    }

    ref = binder_get_ref_for_node(target_proc, node, &thread->todo);

    if (fp->hdr.type == BINDER_TYPE_BINDER)
        fp->hdr.type = BINDER_TYPE_HANDLE;
    else
        fp->hdr.type = BINDER_TYPE_WEAK_HANDLE;
    fp->binder = 0;
    fp->handle = ref->desc;
    fp->cookie = 0;
...
}

注意看这里的转换,首先是在自己的node树里面找有没有实体服务Binder对应的node。这里因为我们是要注册服务,所以node不存在,需要新建一个。你看至此我们的新服务已经有了自己的node了。然后呢会在目标进程信息,也就是service_manager中找这个node的引用,没有的话会新建一个引用的,这个过程会产生一个数放在ref->desc。对了,这个数就是我们的新服务在service_manager那边的handle了。最后就是对原始数据做变身了。类型从BINDER变为HANDLE。清空bindercookie。给handle字段赋值,我们的实体Binder就这样华丽的变成自己的分身了。

千万不要搞混了,这里做转换的是我们要注册的服务,完全是处在事务数据中。而传输这些事务数据的是handle为0,对应node是binder_context_mgr_node的事务。

然后就是service_manager那边读取传输的数据。拿到那个转换以后的fp->handle和服务的名字一起保存起来。

总体而言注册服务:

  • 是一次对service_manager的传输,只不过需要在开始的时候把服务实体Binder放在要传输的数据里。
  • Binder驱动会在传输过程中做一次从实体服务Binder到对应handle的变换。
  • 在这个变换的过程中会创建实体服务Binder对应的node

接下来我们看一下获取服务的过程。貌似应该已经一目了然了。客户端拿着服务名字去service_manager那里去请求handle。拿到以后就可以实例化BpBinder去调用服务了。

获取服务

获取服务的过程分两段,一个是把服务名字传过去,另一个是把handle传回来。去程没啥好说的,和上面的注册服务过程差不多,还少了中间做转换的步骤。回程就是不一样的操作了。service_manager把查到的handle通过BC_REPLY给送回来的。别看handle只是个数,在传输的时候必须把它包装一下,变成这样的:

    obj->hdr.type = BINDER_TYPE_HANDLE;
    obj->handle = handle;
    obj->cookie = 0;

类型是BINDER_TYPE_HANDLE

这个数据结构会写入Binder驱动,命令BC_REPLY会走到和BC_TRANSACTION一样的函数,也就是binder_transaction(),但是某些分支有有所不同。

binder_transaction(...){

if (reply) {
in_reply_to = thread->transaction_stack;
target_thread = in_reply_to->from;
target_proc = target_thread->proc;
}
...
case BINDER_TYPE_HANDLE:
case BINDER_TYPE_WEAK_HANDLE: {
            struct flat_binder_object *fp;
            fp = to_flat_binder_object(hdr);
            ret = binder_translate_handle(fp, t, thread);
        } break;
        ...
}

可见返回过程不需要再查找node因为调用端是谁是清楚的。直接把from变成target就行了。

因为返回数据块里面放着获取到的服务handle。同样的这里会给它做个变换。

看一下binder_translate_handle()如何做变换:

static int binder_translate_handle(struct flat_binder_object *fp,
                   struct binder_transaction *t,
                   struct binder_thread *thread)
{
    struct binder_ref *ref;
    struct binder_proc *proc = thread->proc;
    struct binder_proc *target_proc = t->to_proc;

    ref = binder_get_ref(proc, fp->handle,
                 fp->hdr.type == BINDER_TYPE_HANDLE);

    if (ref->node->proc == target_proc) {
        if (fp->hdr.type == BINDER_TYPE_HANDLE)
            fp->hdr.type = BINDER_TYPE_BINDER;
        else
            fp->hdr.type = BINDER_TYPE_WEAK_BINDER;
        fp->binder = ref->node->ptr;
        fp->cookie = ref->node->cookie;
        
    } else {
        struct binder_ref *new_ref;
        new_ref = binder_get_ref_for_node(target_proc, ref->node, NULL);
        fp->binder = 0;
        fp->handle = new_ref->desc;
        fp->cookie = 0;
    }
}

这里的转换有两个分支,如果此服务进程和目标进程相同,则将handle转换成实体Binder。也就是说你去service_manager那里获取到了和你同一个进程的服务,自然可以转换为实体Binder直接拿来用了。

如果不是同一个进程呢?则会到目标进程信息那里去获取一个新的handle。替换掉原来的handle返回给调用者。

为什么会这样呢?这就需要深入了解noderef的关系。

  • node实体只保存在创建者进程所对应的proc中。
  • ref是保存在当前进程中对外部node实体的引用,不同的进程对同一个noderef是不同的。
  • 所以可以认为handle是进程私有的,不要想当然的认为从service_manager拿到的handle就是和自己进程拿到的一样。

总结

Binder虽然结构复杂,难以全面掌握。但有时又是Android开发者在实际工作中不得不面对的问题。掌握Binder需要投入大量精力和努力。通过一些文章的指引,是可以在学习Binder的路途上减轻一些压力,避免一些误区的。本文就希望能帮助开发者打通一些在学习Binder时可能会遇到的阻塞点,特别是这些阻塞点基本上会存在于Binder驱动层。毕竟楼再高,盖起来还是要先从地基打起,Binder驱动就是Binder这座大厦的基础。打好这个基础,往上的就都是包裹和通道了,理解起来也就会比较轻松。希望能对大家有所帮助。