听说你Binder机制学的不错,来面试下这几个问题

作者:看书的小蜗牛

博客:https://www.jianshu.com/u/3b1099674c2c

引言

Binder承担了绝大部分Android进程通信的职责,可以看做是Android的血管系统,负责不同服务模块进程间的通信。

在对Binder的理解上,可大可小,日常APP开发并不怎么涉及Binder通信知识,最多就是Service及AIDL的使用会涉及部分Binder知识。

Binder往小了说可总结成一句话:

一种IPC进程间通信方式,负责进程A的数据,发送到进程B。

往大了说,其实涉及的知识还是很多的,如Android 对于原Binder驱动的扩展、Zygote进程孵化中对于Binder通信的支持、Java层Binder封装,Native层对于Binder通信的封装、Binder讣告机制等等。很多分析Binder框架的文都是从ServiceManager、Binder驱动、addService、getService来分析等来分析,其实这些主要是针对系统提供的服务,但是bindService启动的服务走的却还是有很大不同的。

本篇文章主要简述一些Binder难以理解的点,但不会太细的跟踪分析。

提纲

以下是本文简述的点,由于篇幅关系有删减,感兴趣的同学可以阅读原文。

  • Binder的定向制导,如何找到目标Binder,唤起进程或者线程

  • Binder中的红黑树,为什么会有两棵binder_ref红黑树

  • Binder一次拷贝原理(直接拷贝到目标线程的内核空间,内核空间与用户空间对应)

  • 系统服务与bindService等启动的服务的区别

  • Binder线程、Binder主线程、Client请求线程的概念与区别

1. Binder如何精确制导

Binder实体服务其实有两种,一是通过addService注册到ServiceManager中的服务,比如:ActivityManagerService、PackageManagerService、PowerManagerService等,一般都是系统服务;

还有一种是通过bindService拉起的一些服务,一般是开发者自己实现的服务。

这里先看通过addService添加的被ServiceManager所管理的服务。有很多分析ServiceManager的文章,本文不分析ServiceManager,只是简单提一下。

ServiceManager是比较特殊的服务,所有应用都能直接使用,因为ServiceManager对于Client端来说Handle句柄是固定的,都是0,所以ServiceManager服务并不需要查询,可以直接使用。

理解Binder定向制导的关键是理解Binder的四棵红黑树,先看一下binder_proc结构体,在它内部有四棵红黑树,threads,nodes,refs_by_desc,refs_by_node。

nodes就是Binder实体在内核中对应的数据结构,binder_node里记录进程相关的binder_proc,还有Binder实体自身的地址等信息,nodes红黑树位于binder_proc,可以知道Binder实体其实是进程内可见,而不是线程内。

(P.S., 对红黑树要是不太了解的同学,先自行脑补下二叉搜索树的样子,有机会专门写一篇来说明)

struct binder_proc {
    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 list_head todo;
    wait_queue_head_t wait;
   ...
};

现在假设存在一堆Client与Service,Client如何才能访问Service呢?

首先Service会通过addService将binder实体注册到ServiceManager中去,Client如果想要使用Servcie,就需要通过getService向ServiceManager请求该服务。

Service通过addService向ServiceManager注册的时候,ServiceManager会将服务相关的信息存储到自己进程的Service列表中去,同时在ServiceManager进程的binder_ref红黑树中为Service添加binder_ref节点,这样ServiceManager就能获取Service的Binder实体信息。

而当Client通过getService向ServiceManager请求该Service服务的时候,ServiceManager会在注册的Service列表中查找该服务,如果找到就将该服务返回给Client。

在这个过程中,ServiceManager会在Client进程的binder_ref红黑树中添加binder_ref节点,可见本进程中的binder_ref红黑树节点都不是本进程自己创建的,要么是Service进程将binder_ref插入到ServiceManager中去,要么是ServiceManager进程将binder_ref插入到Client中去。之后,Client就能通过Handle句柄获取binder_ref,进而访问Service服务。

binder_ref添加逻辑

getService之后,便可以获取binder_ref引用,进而获取到binder_procbinder_node信息,之后Client便可有目的的将binder_transaction事务插入到binder_proc的待处理列表,并且,如果进程正在睡眠,就唤起进程。

其实这里到底是唤起进程还是线程也有讲究,对于Client向Service发送请求的状况,一般都是唤醒binder_proc上睡眠的线程:

struct binder_ref {
    int debug_id;
    struct rb_node rb_node_desc;
    struct rb_node rb_node_node;
    struct hlist_node node_entry;
    struct binder_proc *proc;
    struct binder_node *node;
    uint32_t desc;
    int strong;
    int weak;
    struct binder_ref_death *death;
};

2. binder_proc为何会有两棵binder_ref红黑树

binder_proc中存在两棵binder_ref红黑树,其实两棵红黑树中的节点是复用的,只是查询方式不同,一个通过handle句柄,一个通过node节点查找。

个人理解:

refs_by_node红黑树主要是为了binder驱动往用户空间写数据所使用的,而refs_by_desc是用户空间向Binder驱动写数据使用的,只是方向问题。

比如在服务addService的时候,binder驱动会在在ServiceManager进程的binder_proc中查找binder_ref结构体,如果没有就会新建binder_ref结构体。

再比如在Client端getService的时候,binder驱动会在Client进程中通过 binder_get_ref_for_node为Client创建binder_ref结构体,并分配句柄,同时插入到refs_by_desc红黑树中。

可见refs_by_node红黑树,主要是给binder驱动往用户空间写数据使用的。相对的refs_by_desc主要是为了用户空间往binder驱动写数据使用的,当用户空间已经获得Binder驱动为其创建的binder_ref引用句柄后,就可以通过binder_get_ref从refs_by_desc找到响应binder_ref,进而找到目标binder_node。

可见有两棵红黑树主要是区分使用对象及数据流动方向,看下面的代码就能理解:

// 根据32位的uint32_t desc来查找,可以看到,binder_get_ref不会新建binder_ref节点
static struct binder_ref *binder_get_ref(struct binder_proc *proc,
                     uint32_t desc)
{
    struct rb_node *n = proc->refs_by_desc.rb_node;
    struct binder_ref *ref;
    while (n) {
        ref = rb_entry(n, struct binder_ref, rb_node_desc);
        if (desc < ref->desc)
            n = n->rb_left;
        else if (desc > ref->desc)
            n = n->rb_right;
        else
            return ref;
    }
    return NULL;
}

可以看到binder_get_ref并具备binder_ref的创建功能,相对应的看一下binder_get_ref_for_node,binder_get_ref_for_node红黑树主要通过binder_node进行查找,如果找不到,就新建binder_ref,同时插入到两棵红黑树中去

static struct binder_ref *binder_get_ref_for_node(struct binder_proc *proc,
                          struct binder_node *node)
{
    struct rb_node *n;
    struct rb_node **p = &proc->refs_by_node.rb_node;
    struct rb_node *parent = NULL;
    struct binder_ref *ref, *new_ref;
    while (*p) {
        parent = *p;
        ref = rb_entry(parent, struct binder_ref, rb_node_node);
        if (node < ref->node)
            p = &(*p)->rb_left;
        else if (node > ref->node)
            p = &(*p)->rb_right;
        else
            return ref;
    }

    // binder_ref 可以在两棵树里面,但是,两棵树的查询方式不同,并且通过desc查询,不具备新建功能
    new_ref = kzalloc(sizeof(*ref), GFP_KERNEL);
    if (new_ref == NULL)
        return NULL;
    binder_stats_created(BINDER_STAT_REF);
    new_ref->debug_id = ++binder_last_id;
    new_ref->proc = proc;
    new_ref->node = node;
    rb_link_node(&new_ref->rb_node_node, parent, p);
    // 插入到proc->refs_by_node红黑树中去
    rb_insert_color(&new_ref->rb_node_node, &proc->refs_by_node);
    // 是不是ServiceManager的
    new_ref->desc = (node == binder_context_mgr_node) ? 0 : 1;
    // 分配Handle句柄,为了插入到refs_by_desc
    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->desc > new_ref->desc)
            break;
        new_ref->desc = ref->desc + 1;
    }
    // 找到目标位置
    p = &proc->refs_by_desc.rb_node;
    while (*p) {
        parent = *p;
        ref = rb_entry(parent, struct binder_ref, rb_node_desc);
        if (new_ref->desc < ref->desc)
            p = &(*p)->rb_left;
        else if (new_ref->desc > ref->desc)
            p = &(*p)->rb_right;
        else
            BUG();
    }
    rb_link_node(&new_ref->rb_node_desc, parent, p);
    // 插入到refs_by_desc红黑树中区
    rb_insert_color(&new_ref->rb_node_desc, &proc->refs_by_desc);

    if (node) {
        hlist_add_head(&new_ref->node_entry, &node->refs);
        binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                 "binder: %d new ref %d desc %d for "
                 "node %d\n", proc->pid, new_ref->debug_id,
                 new_ref->desc, node->debug_id);
    } else {
        binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                 "binder: %d new ref %d desc %d for "
                 "dead node\n", proc->pid, new_ref->debug_id,
                  new_ref->desc);
    }
    return new_ref;
}

该函数调用在binder_transaction函数中,其实就是在binder驱动访问target_proc的时候,这也也很容易理解,Handle句柄对于跨进程没有任何意义,进程A中的Handle,放到进程B中是无效的。

两棵binder_ref红黑树

3. Binder一次拷贝原理

Android选择Binder作为主要进程通信的方式同其性能高也有关系,Binder只需要一次拷贝就能将A进程用户空间的数据为B进程所用。这里主要涉及两个点:

  • Bindermap函数,会将内核空间直接与用户空间对应,用户空间可以直接访问内核空间的数据

  • A进程的数据会被直接拷贝到B进程的内核空间(一次拷贝)

#define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))
  
  ProcessState::ProcessState()
      : mDriverFD(open_driver())
      , mVMStart(MAP_FAILED)
      , mManagesContexts(false)
      , mBinderContextCheckFunc(NULL)
      , mBinderContextUserData(NULL)
      , mThreadPoolStarted(false)
      , mThreadPoolSeq(1){
     if (mDriverFD >= 0) {
      ....
          // mmap the binder, providing a chunk of virtual address space to receive transactions.
          mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
     ...
   
      }
  }

mmap函数属于系统调用,mmap会从当前进程中获取用户态可用的虚拟地址空间(vm_area_struct *vma),并在mmap_region中真正获取vma,然后调用file->f_op->mmap(file, vma),进入驱动处理,之后就会在内存中分配一块连续的虚拟地址空间,并预先分配好页表、已使用的与未使用的标识、初始地址、与用户空间的偏移等等,通过这一步之后,就能把Binder在内核空间的数据直接通过指针地址映射到用户空间,供进程在用户空间使用,这是一次拷贝的基础,一次拷贝在内核中的标识如下:

struct binder_proc {
    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;
    int pid;
    struct vm_area_struct *vma; //虚拟地址空间,用户控件传过来
    struct mm_struct *vma_vm_mm;
    struct task_struct *tsk;
    struct files_struct *files;
    struct hlist_node deferred_work_node;
    int deferred_work;
    void *buffer; //初始地址
    ptrdiff_t user_buffer_offset; //这里是偏移
    
    struct list_head buffers;//这个列表连接所有的内存块,以地址的大小为顺序,各内存块首尾相连
    struct rb_root free_buffers;//连接所有的已建立映射的虚拟内存块,以内存的大小为index组织在以该节点为根的红黑树下
    struct rb_root allocated_buffers;//连接所有已经分配的虚拟内存块,以内存块的开始地址为index组织在以该节点为根的红黑树下
}

上面只是在APP启动的时候开启的地址映射,但并未涉及到数据的拷贝,下面看数据的拷贝操作。

当数据从用户空间拷贝到内核空间的时候,是直从当前进程的用户空间接拷贝到目标进程的内核空间,这个过程是在请求端线程中处理的,操作对象是目标进程的内核空间。看如下代码:

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply){
                   ...
        在通过进行binder事物的传递时,如果一个binder事物(用struct binder_transaction结构体表示)需要使用到内存,
        就会调用binder_alloc_buf函数分配此次binder事物需要的内存空间。
        需要注意的是:这里是从目标进程的binder内存空间分配所需的内存        //从target进程的binder内存空间分配所需的内存大小,这也是一次拷贝,完成通信的关键,直接拷贝到目标进程的内核空间
        //由于用户空间跟内核空间仅仅存在一个偏移地址,所以也算拷贝到用户空间
        t->buffer = binder_alloc_buf(target_proc, tr->data_size,
            tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
        t->buffer->allow_user_free = 0;
        t->buffer->debug_id = t->debug_id;        //该binder_buffer对应的事务    
        t->buffer->transaction = t;        //该事物对应的目标binder实体 ,因为目标进程中可能不仅仅有一个Binder实体
        t->buffer->target_node = target_node;
        trace_binder_transaction_alloc_buf(t->buffer);        if (target_node)
            binder_inc_node(target_node, 1, 0, NULL);        // 计算出存放flat_binder_object结构体偏移数组的起始地址,4字节对齐。
        offp = (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));           // struct flat_binder_object是binder在进程之间传输的表示方式 //
           // 这里就是完成binder通讯单边时候在用户进程同内核buffer之间的一次拷贝动作 //
          // 这里的数据拷贝,其实是拷贝到目标进程中去,因为t本身就是在目标进程的内核空间中分配的,
        if (copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)) {
            binder_user_error("binder: %d:%d got transaction with invalid "
                "data ptr\n", proc->pid, thread->pid);
            return_error = BR_FAILED_REPLY;            goto err_copy_data_failed;
        }

可以看到binder_alloc_buf(target_proc, tr->data_size,tr->offsets_size, !reply && (t->flags & TF_ONE_WAY))函数在申请内存的时候,是从target_proc进程空间中去申请的,这样在做数据拷贝的时候copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)),就会直接拷贝target_proc的内核空间,而由于Binder内核空间的数据能直接映射到用户空间,这里就不在需要拷贝到用户空间。

这就是一次拷贝的原理。

内核空间的数据映射到用户空间其实就是添加一个偏移地址,并且将数据的首地址、数据的大小都复制到一个用户空间的Parcel结构体,具体可以参考Parcel.cpp的Parcel::ipcSetDataReference函数。

Binder一次拷贝原理

4. 系统服务与bindService等启动的服务的区别

服务可分为系统服务与普通服务,系统服务一般是在系统启动的时候,由SystemServer进程创建并注册到ServiceManager中的。而普通服务一般是通过ActivityManagerService启动的服务,或者说通过四大组件中的Service组件启动的服务。这两种服务在实现跟使用上是有不同的,主要从以下几个方面:

  • 服务的启动方式

  • 服务的注册与管理

  • 服务的请求使用方式

首先看一下服务的启动上,系统服务一般都是SystemServer进程负责启动,比如AMS,WMS,PKMS,电源管理等,这些服务本身其实实现了Binder接口,作为Binder实体注册到ServiceManager中,被ServiceManager管理,而SystemServer进程里面会启动一些Binder线程,主要用于监听Client的请求,并分发给响应的服务实体类,可以看出,这些系统服务是位于SystemServer进程中(有例外,比如Media服务)。在来看一下bindService类型的服务,这类服务一般是通过ActivitystartService或者其他contextstartService启动的,这里的Service组件只是个封装,主要的是里面Binder服务实体类,这个启动过程不是ServcieManager管理的,而是通过ActivityManagerService进行管理的,同Activity管理类似。

再来看一下服务的注册与管理:系统服务一般都是通过ServiceManager的addService进行注册的,这些服务一般都是需要拥有特定的权限才能注册到ServiceManager,而bindService启动的服务可以算是注册到ActivityManagerService,只不过ActivityManagerService管理服务的方式同ServiceManager不一样,而是采用了Activity的管理模型,详细的可以自行分析。

最后看一下使用方式,使用系统服务一般都是通过ServiceManagergetService得到服务的句柄,这个过程其实就是去ServiceManager中查询注册系统服务。而bindService启动的服务,主要是去ActivityManagerService中去查找相应的Service组件,最终会将Service内部Binder的句柄传给Client。

系统服务与bindService启动服务的区别

5. Binder线程、Binder主线程、Client请求线程的概念与区别

Binder线程是执行Binder服务的载体,只对于服务端才有意义,对请求端来说,是不需要考虑Binder线程的,但Android系统的处理机制其实大部分是互为C/S的。比如APP与AMS进行交互的时候,都互为对方的C与S,这里先不讨论这个问题,先看Binder线程的概念。

Binder线程就是执行Binder实体业务的线程,一个普通线程如何才能成为Binder线程呢?很简单,只要开启一个监听Binder字符设备的Loop线程即可,在Android中有很多种方法,不过归根到底都是监听Binder,换成代码就是通过ioctl来进行监听。

拿ServerManager进程来说,其主线就是Binder线程,其做法是通过binder_loop实现不死线程:

void binder_loop(struct binder_state *bs, binder_handler func)
{
   ...
    for (;;) {
    <!--关键点1-->
        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
     <!--关键点2-->
        res = binder_parse(bs, 0, readbuf, bwr.read_consumed, func);
        。。
    }
}

上面的关键代码1就是阻塞监听客户端请求,2 就是处理请求,并且这是一个死循环,不退出。再来看SystemServer进程中的线程,在Android4.3(6.0以后打代码就不一样了)中SystemSever主线程便是Binder线程,同时一个Binder主线程。

Binder线程与Binder主线程的区别是:线程是否可以终止Loop,不过目前启动的Binder线程都是无法退出的,其实可以全部看做是Binder主线程,其实现原理是,在SystemServer主线程执行到最后的时候,Loop监听Binder设备,变身死循环线程,关键代码如下:

extern "C" status_t system_init()
{
    ...
    ALOGI("System server: entering thread pool.\n");
    ProcessState::self()->startThreadPool();
    IPCThreadState::self()->joinThreadPool();
    ALOGI("System server: exiting thread pool.\n");
    return NO_ERROR;
}

ProcessState::self()->startThreadPool()是新建一个Binder主线程,而PCThreadState::self()->joinThreadPool()是将当前线程变成Binder主线程。其实startThreadPool最终也会调用joinThreadPool,看下其关键函数:

void IPCThreadState::joinThreadPool(bool isMain)
{
    ...
    status_t result;
    do {
        int32_t cmd;
        ...关键点1 
        result = talkWithDriver();
        if (result >= NO_ERROR) {
           ...关键点2 
            result = executeCommand(cmd);
        }
        // 非主线程的可以退出
        if(result == TIMED_OUT && !isMain) {
            break;
        }
        // 死循环,不完结,调用了这个,就好比是开启了Binder监听循环,
    } while (result != -ECONNREFUSED && result != -EBADF);
 }

status_t IPCThreadState::talkWithDriver(bool doReceive)
{  
    do {
        ...关键点3 
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
   }

先看关键点1 talkWithDriver,其实质还是去掉用ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)去不断的监听Binder字符设备,获取到Client传输的数据后,再通过executeCommand去执行相应的请求,joinThreadPool是普通线程化身Binder线程最常见的方式。不信,就再看一个MediaService,看一下main_mediaserver的main函数:

int main(int argc, char** argv)
{
   。。。
        sp<ProcessState> proc(ProcessState::self());
        sp<IServiceManager> sm = defaultServiceManager();
        ALOGI("ServiceManager: %p", sm.get());
        AudioFlinger::instantiate();
        MediaPlayerService::instantiate();
        CameraService::instantiate();
        AudioPolicyService::instantiate();
        registerExtensions();
        ProcessState::self()->startThreadPool();
        IPCThreadState::self()->joinThreadPool();
    }

其实还是通过joinThreadPool变身Binder线程,至于是不是主线程,看一下下面的函数:

void IPCThreadState::joinThreadPool(bool isMain)

void ProcessState::spawnPooledThread(bool isMain)
{
    if (mThreadPoolStarted) {
        String8 name = makeBinderThreadName();
        ALOGV("Spawning new pooled thread, name=%s\n", name.string());
        sp<Thread> t = new PoolThread(isMain);
        t->run(name.string());
    }
}

其实关键就是就是传递给joinThreadPool函数的isMain是否是true,不过是否是Binder主线程并没有什么用,因为源码中并没有为这两者的不同处理留入口,感兴趣可以去查看一下binder中的TIMED_OUT。

最后来看一下普通Clientbinder请求线程,比如我们APP的主线程,在startActivity请求AMS的时候,APP的主线程成其实就是Binder请求线程,在进行Binder通信的过程中,Client的Binder请求线程会一直阻塞,知道Service处理完毕返回处理结果。

本文先讲到这里,内容比较多,多看看多理解。

近期文章:

今日问题:

Binder机制里有哪些难理解的点?

打卡格式:

打卡 X 天,答:xxx 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值