一问就懵逼的Binder(一)

一、为什么要用Binder机制

Android系统为每个应用在分配进程的时候都是有内存限制如现在常见的64M啊等等。同时很多应用为了避免自己的核心业务被系统杀死,这个时候就需要将核心业务运行在单独的进程中,且各个进程组成一个守护进程来达到保活和唤醒的功能。于是这个时候就要用到进程间通信了,而进程间通信的方式又很多种,如在C语言中我们常说的socket、共享内存、管道、信号量等等。在Android平台上当然也是能够用这些方法实现的,但是为什么Android没有用这些方式,却自己搞一个Binder机制呢?原因就是这些方式要么太消耗性能了,要进行多次的读写等操作;要么使用成本高。如下图所示。于是Android自己搞了一个Binder机制来达到我们想要的进程间通信,且不那么消耗性能。我们可以看一下图Binder相对其他进程间通信的一个比较。

总结一下就是一下5点优势:

  1. 从性能的角度 数据拷贝次数:Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存

  2. 从稳定性的角度

    • Binder是基于C/S架构的 C/S 相对独立,稳定性较好

    • 共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题

  3. 从安全的角度

    • 传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份

      1. 传统IPC只能由用户在数据包里填入UID/PID

      2. 可靠的身份标记只有由IPC机制本身在内核中添加

      3. 传统IPC访问接入点是开放的,无法建立私有通道

    • Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行

    • Android的UID权鉴是如何做的?

  4. 从语言层面的角度

    • Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句)

    • Binder恰恰也符合面向对象的思想 Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中

    • Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制,Android中的Kill Process采用的signal(信号)机制等等。而Binder更多则用在system_server进程与上层App层的IPC交互。

  5. 从公司战略的角度 总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。 而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下 开源与商业化共存的一个成功典范。

二、为什么要有进程间通信

这个问题其实在前面讲多线程,将JVM的时候都有提到过。我们都知道两个应用之间是不能够进行直接内存访问的。那么为什么不能呢?答案就是,系统在内存分配的时候做了隔离操作(避免出现一个应用操控另一个应用等混乱和不安全的情况)。系统将我们的系统内存分为两大类:用户空间和内核空间。用户空间对应着一个个应用,而内核空间对应着系统内核。然后又规定内核空间和用户空间之间能够通过系统调用相互访问,但是用户空间之间却不能够相互访问。如下图:

三、什么是Binder

讲了许多还是完全不知道Binder是个什么东西。在平时开发中好像也感受不到它的存在。那是因为Binder是Android系统底层一个封装完善的机制,我们开发过程中实际能够接触到它本身的机会并不多,所以导致很多开发者和我一样,一开始甚至都没听过Binder这个名词,别人一问,满脸的问号。但是它却是Android系统的基石贯穿着我们开发的整个应用。那么到底什么是Binder呢?

首先,Binder是一种进程间通信方式(IPC),这个我们要清楚,至于什么是进程间通信,我就不讲了(说简单点就是两家人相互串门)。不了解进程间通信概念的可以网上查查资料。其次Binder是基于C/S模型的一种机制。在这个模型中主要由如下四个角色扮演:Client、Service、ServiceManager、Binder驱动。

其中Client和Service一般是由开发者去完成,而ServiceManager和Binder驱动都是系统实现的,ServiceManager用来管理各个Service。而Binder驱动是运行在内核空间上的虚拟设备驱动,用来实现进程通信控制,它是连接Client、Service以及ServiceManager的桥梁。

C/S模型

 分层来看下C/S模型:

其实到现在说这么多都是些概念的东西,Binder在整个进程间通信的过程中到底扮演怎样的角色,它是如何实现让Client能够调用Service接口的。这些我们依旧不清楚。

四、Binder机制如何运作

(1)Binder是如何工作的

Client和Service建立联系后如何进行数据传输,那么多服务它是如何做到精确制导的,它的一次拷贝原理是怎么实现的?这些问题看了上面的图似乎还是弄不清楚?怎么办?学!!!

(2)Binder是如何精确制导,让Client和Service建立联系的

我们先来看个简单的图

 通过图我们知道了两个新的名词Binder实体和Binder引用。很多人会将这个Binder实体和Binder引用在理解的时候向上面提到的映射上面去靠,实际上两者是两个概念,映射是针对内存的,而Binder实体和引用可理解为是针对接口的。所以这里我们需要讲一下这两个新的名词。

(1)、Binder实体服务其实有两种,一是通过addService注册到ServiceManager中的服务,比如:ActivityManagerService、PackageManagerService、PowerManagerService等,一般都是系统服务;还有一种是通过bindService拉起的一些服务,一般是开发者自己实现的服务,它针对一个个的功能接口。

(2)、Binder引用服务其实可以理解为是实体服务的一个句柄,两者之间通过红黑树建立联系。

等一下什么又是红黑树?这个词好熟悉啊,是不是面试的时候一问我就答不上来的问题?

对红黑树要是不太了解的同学,先自行脑补下二叉搜索树的样子。或者查一下相关资料。这里我们就不详细介绍了。我们还是先来看看红黑树在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;
   ...
   struct list_head todo;
   wait_queue_head_t wait;
   ...
};

没错,就是这个结构体。在它内部有四棵红黑树,threads,nodes,refs_by_desc,refs_by_node。这些红黑树都承担着怎样的角色呢?个人理解如下:

  • nodes:就是Binder实体在内核中对应的数据结构
  • refs_by_desc:用来用户空间向Binder驱动写数据使用的,类似如BC
  • refs_by_node:主要是为了binder驱动往用户空间写数据所使用的,类似如BR
  • threads:顾名思义,主要与Binder线程相关

我们就以Client和Service建立联系来看红黑树在这中间是如何运作的。

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

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

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

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

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

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

 当然,你如果想再深入了解一下它的底层实现,可以看一下大神的文章,我觉得写的还是狠到位的.

(3)Binder的一次拷贝原理

很多人都听过Binder的一次拷贝,但是再一问这个一次拷贝是怎么实现的时候,就会像我一样两眼一抹黑了。Android之所以自己搞一个Binder通信,就是得益于它的高效。所以一次拷贝原理是我们必须理解的问题。Binder只需要一次拷贝就能将A进程用户空间的数据为B进程所用。这里主要涉及两个点:

  • Binder的map函数,会将内核空间直接与用户空间对应,用户空间可以直接访问内核空间的数据
  • A进程的数据会被直接拷贝到B进程的内核空间的缓存区,然后B进程通过映射关系直接访问内核空间的数据(一次拷贝)

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组织在以该节点为根的红黑树下
    
    }

但是上面这些知只是地址映射,没有涉及到核心的数据拷贝,下面看数据的拷贝操作。当数据从用户空间拷贝到内核空间的时候,是直从当前进程的用户空间接拷贝到目标进程的内核空间,这个过程是在请求端线程中处理的,操作对象是目标进程的内核空间。看如下代码:

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驱动来完成。这样就实现了两个进程之间间接实现了通信功能。

日常开发中,我们能够接触到Binder机制的机会就是AIDL开发了,那么在AIDL中Binder的c/s分别由谁来扮演呢?答案是,通常我们自定义aidl文件后,重新编译,Android Studio会利用工具自动帮我们生成一个aidl接口的Java代码。然后里面有两个关键的内部类Stub和Proxy。这里Stub就充当Client接收数据,而Proxy充当Service来提供数据。

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

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

  • 服务的启动方式
  • 服务的注册与管理
  • 服务的请求使用方式

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

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

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

六、面试常被坑的知识点

(1)Binder请求的同步与异步

很多人都会说,Binder是对Client端同步,而对Service端异步,其实并不完全正确,在单次Binder数据传递的过程中,其实都是同步的。只不过,Client在请求Server端服务的过程中,是需要返回结果的,即使是你看不到返回数据,其实还是会有个成功与失败的处理结果返回给Client,这就是所说的Client端是同步的。至于说服务端是异步的,可以这么理解:在服务端在被唤醒后,就去处理请求,处理结束后,服务端就将结果返回给正在等待的Client线程,将结果写入到Client的内核空间后,服务端就会直接返回了,不会再等待Client端的确认,这就是所说的服务端是异步的.

(2)APP有多少Binder线程,是固定的么?

首先,Android APP在启动的时候是由Zygote进程fork出来的一个子进程,而该进程在启动的时候做了很多初始化操作,这其中就包括了对Binder的支持,会去创建一个Binder主线程,所以APP天生是支持Binder线程的。那么APP中Binder线程的数量是否是固定的或者是有上限的呢?答案是否定的,驱动会根据目标进程中是否存在足够多的Binder线程来告诉进程是不是要新建Binder线程,且没有设置上限。

发布了29 篇原创文章 · 获赞 3 · 访问量 889

猜你喜欢

转载自blog.csdn.net/LVEfrist/article/details/101769376
今日推荐