Android 重学系列 Binder的总结

前言

本文实际上是Android 重学系列 Binder驱动相关知识的总结。关于Binder驱动的源码分析我划分出了6部分:

详细的源码分析最好还是阅读原文,本文只是总结之前欠下的总结文章。

接下来我就按照这6个部分进行总结。时隔一年回来,在看了极客时间的刘超大神的Linux内核专栏之后,我又多了几分感悟,本文将会结合刘超大神的思路一起来总结Binder驱动。

如果遇到什么问题欢迎来到本文进行讨论,https://www.jianshu.com/p/62eee6cf03a3

正文

先展示一副大家最常见的Binder设计图,也就是罗升阳大神的Binder示意图:


Binder.png

包含了4个角色:

  • 1.Binder 客户端(本地端)
  • 2.Binder服务端(远程端)
  • 3.service_manager
  • 4.Binder 驱动

能看到在Android系统中,存在一个客户端和服务端。两者之间通过通过Binder驱动进行IPC通信。

Android系统会通过init.rc的解析,从而启动一个service_manager进程,这个进程就是整个Android系统的Binder服务启动核心中心,也是所有Binder服务注册缓存的地方。

整个IPC基础的模型如下图:


binder IPC基本模型.png

然而这个过程可能会发生阻塞也可能不发生阻塞,这个就是要Binder进行传输的是否传输一个特殊的flag:FLAG_ONEWAY 0x00000001,也就是aidl中在方法前设置的oneway标识就不会阻塞等待。我们这边讨论的是阻塞等待的情况,因此,我们隐藏好binder驱动和service_manager,只观察Client和Service之间的关系,则有:

Binder中service 和 client.png

实际上在整个Binder驱动通信体系中存在着三种不同的Looper:

  • 1.service_manager 的Looper监听来自其他Binder的查询服务。在Looper中主要是提供给其他App进程,那些SystemServer注册在Binder中的服务,如AMS,PMS等等

  • 2.当调用Binder时候,构建的阻塞等待循环。

  • 3.应用启动初期的Looper,这个Looper主要负责初始化当前应用进程在Binder驱动中的配置。用于接受从服务端返回来的异步消息,以及处理类似BC_REPLY等消息,以及死亡代理的回调

接下来,我们从第一个Looper service_manager的初始化查询服务,以及初始化第一个Binder服务的原来开始聊

Binder的启动

Binder 身为内核模块的一份子,必须通过陷入内核空间才能进行访问,因此我们必须理解Linux的syscall 系统调用是调用的。

原理图如下:


syscall 工作原理.png

以kill为例子,首先每一个系统调用都有自己的.S实现的汇编代码,在这个文件中会通过软中断,查找对应的中断地址。而在内核中会维护一张系统调用的表,在图中为call.S.此时对应的中断偏移量就是这种系统调用对应的方法。而这些方法就声明在syscall.h中。

此时可以通过这张表所对应的方法指针,找到真正的系统调用的实现,更加具体的图,刘超大神总结的很好:


系统调用.png

可以结合这张图,和我写的Binder驱动的初始化 syscall原理效果更好。

Binder身为驱动,不可避免的需要在系统启动的时候进行初始化,注意这里只是初始化内核模块而不是启动内核模块。

我们在进程初始化之初或者service_manager,就会尝试的打开一个本地文件/dev/binder。其实这个文件并不是真的是文件,只是这个驱动程序表现形式是文件罢了。经常看我的文章就知道我经常提到一句话Linux中一切皆为文件,只是这个“文件”很特殊,重写了面向操作系统的open,mmap等操作。

我们操作Binder驱动本质上是操作一个内存中的一个特殊的file结构体,这个结构体的file_operation(文件操作结构体)重新写成Binder特殊的操作。

驱动加载流程

  • 1.Linux刚开始通过module_init类似字符设备方式把当前作为dev_t注册到全局的cdev_map。

  • 2.module_init是用户态启动时候调用insmod调用加载内核模块。

  • 3.就会mknod系统调用,层层查找,找到/dev/xxx如果没有就会创建一个文件,此时这个内存文件inode的i_rdev指向dev_t,这样文件系统就和内核模块关联起来。

  • 4.当我们调用这个路径下的open时候,就会找到该内存文件对应inode,也就找到我们binder驱动。驱动本质上是为了屏蔽设备控制器,而这里Binder借助驱动的机制,巧妙的实现了跨进程通信

注释:这里面冒出了不少的名词。你可以想象inode实际上是在磁盘中的一个单位节点,关于用户态的启动流程,inode之后在Linux内核专栏会专门解析。

刘超大神也有一副很好的图,展示了整个流程:


驱动的加载流程.png

Binder 驱动的示意图

在聊Binder驱动之前,先上一副思维图,先要对Binder驱动中几个核心的对象有印象:


binder核心成员.png
  • binder_proc 每一个进程都会在binder中映射一个binder_proc对象。并保存到当前进程对应file结构体的私有数据中

  • binder_node 是指每一个同进程下的Binder对象,里面记载了binder_proc 和 cookie

  • binder_thread 是每一个binder执行的环境,和binder_proc的定位类似。不过当一个进程在不同的线程进行binder通信的时候,binder_thread就是binder运行环境

  • free_buffers 是binder 在mmap阶段进行映射的时候,一口气获得的绑定了同一套物理页的(用户空间,内核空间)虚拟内存

  • binder_buffer Binder驱动的内核缓冲区,是用于承载和转移数据的

  • binder_transaction 当进行传输数据时候,数据,命令的承载体。他持有binder_buffer.

  • binder_ref 挂载在binder_proc的refs_by_noderefs_by_desc两棵树中.他是记录了远端进程对应的句柄,binder_proc,binder_thread. 前者可以通过binder_node找到binder_ref,后者可以通过句柄desc(对应用户空间的handle)找到binder_ref

service_manager 初始化与启动Binder

Binder身为驱动的一份子不可避免也是走这一套流程,module_init 是用户态启动时候调用insmod把dev_t注册到cdev_map;mknod又创建了/dev/binder的文件,并且让dev_t和inode的i_rdev关联起来;那么service_manager只剩下一个open的流程,才能正式启动Binder驱动。

而ServiceManager的启动流程可以分为三个步骤:

  • 1.open 打开/dev/binder 从而调用Binder驱动重写的file_op中的open方法
  • 2.ioctl 对binder驱动进行通信,发送BINDER_VERSION命令
  • 3.mmap 调用/dev/binder 从而调用Binder驱动重写的file_op中的mmap方法(注意参数是MAP_PRIVATE,而不是MAP_SHARE所以内容不会同步到磁盘)
  • 4.进入到looper等待,等待其他进程通过Binder通信进来ServiceManager查询Android系统提供的Binder服务。

Binder open

在open中如下几件重要的事情:

  • 1.通过kzalloc为当前调用open的进程分配一个binder_proc结构体
  • 2.初始化binder_proctodo链表
  • 3.初始化binder_proc的wait 等待队列头
  • 4.初始化binder_proc中的双向链表proc_node 添加到静态变量(没初始化的放在BSS,初始化的放在.Data中)binder_procs中。
  • 5.初始化delivered_death链表,保存那些正在监听目标进程是否死亡的进程队列,一旦死亡了就会执行这个列表中所有进程对应的死亡监听
  • 6.把binder_proc对象设置到当前file结构体的私有数据中。之后每一次进程通过申请到的file结构体去访问binder驱动时候,就能通过file结构体的私有数据确定当前进程对应的binder_proc

关于这一块详细的解析可以阅读我写的https://www.jianshu.com/p/ba0a34826b27

Binder ioctl BINDER_VERSION

  • 1.初始化binder_proc中的binder_thread结构体。从binder_procthreads红黑树中查找和当前pid一致的binder_thread
    • 1.1.如果找不到,则生成一个全新的binder_thread结构体并初始化binder_threadtodo队列以及wait等待队列,并且绑定binder_proc对象和pid

注意binder_procbinder_thread可以说是Binder驱动执行对应进程事务的环境

  • 2.同ioctl BINDER_VERSION从Binder驱动中获取Binder的版本号。

mmap 原理

在聊Binder的mmap之前,先来总结mmap系统调用做了什么?接下来这段解析,最好先看看我写的Linux内存基础篇章https://www.jianshu.com/p/82b4454697ce

下面这一幅图:


mmap映射原理.png

记住mmap不仅仅只是可以映射物理内存和虚拟内存之间的关系,还能映射虚拟内存和内存文件之间的关系。

关于Binder相关的mmap原理,具体的源码分析可以阅读https://www.jianshu.com/p/4399aedb4d42

mmap的映射的核心方法为do_mmap_pgoff,这里面可以分为如下几个流程:

  • 1.get_unmapped_area 检索一个没有进行映射的用户空间虚拟内存区域(32为是0~3G中查找),注意在Linux内核中分为两种虚拟内存结构体,一个代表用户空间的vm_area_struct,另一个则是代表内核空间的vm_struct.

    • 1.1.get_unmapped_area会判断当前是否是匿名映射,也就是是否把file的内容映射到虚拟内存中。是匿名映射,则从mm_struct的get_unmapped_area方法从的vm_area_struct 红黑树中查找空闲的vm_area_struct对应的区域

    • 1.2.如果是文件映射,则调用文件系统的get_unmapped_area。一般都是讨论ext4虚拟文件系统,在这里面还是调用当前进程的mm_struct的get_unmapped_area方法。

  • 2.mmap_region 映射这个新的vm_area_struct虚拟内存区域。首先判断新的vm_area_struct能否和之前的虚拟内存合并起来。

如果不能则通过kmem_cache_zalloc生成一个新的vm_area_struct。并把vm_area_struct和刚才从get_unmapped_area 拿到的地址以及映射大小进行绑定。最后把这个vm_area_struct绑定到mm_struct的红黑树上。

  • 2.1.如果是文件映射,还会调用了文件操作file_operationmmap。还记得在上面说过的吗?驱动也是一个特殊的文件,此时就会调用驱动文件的file_operationmmap。也就是调用Binder 驱动的mmap方法。

  • 2.2.如果是文件映射,当调用了驱动的mmap方法后,还会把这个vm_area_struct挂载到file结构体的address_space结构体中的i_mmap的红黑树中。这样就file结构体记录映射的虚拟内存了。

  • 3.如果不是Binder驱动的mmap方法,一般的匿名映射在第1和第2步把vm_area_struct和虚拟内存地址关联起来了,也就是说虚拟地址和vm_area_struct在逻辑上关联起来,并没有真正的申请物理内存。

    • 3.1.当进行访问的时候,就会爆出缺页异常,就进入到中断,调用do_page_fault方法中开始分配物理内存,就会调用__handle_mm_fault 先进行分配页目录。其中调用了handle_pte_fault物理页的绑定,这里分为三种情况:

    • 3.1.如果是匿名映射,do_anonymous_page从伙伴系统中分配出物理页面

    • 3.2.如果是文件映射,调用filemap_fault查找文件是否有对应的物理内存缓存,有就预读缓存的内存,没有就先分配一个缓存页,接着调用kmap_atomic将物理页面临时映射到内核虚拟地址,读取文件到这个缓存页虚拟地址中。

    • 3.3.物理内存如果长时间不用,就会把内容换出到磁盘中。此时就会调用do_swap_page方法。先查找swap文件是否存在缓存页,没有则调用swapin_readahead从文件中读取生成新的内存页,并通过mk_pte生成页表项,插入到页表,接着把文件清理了。整个读取文件的过程还是使用kmap_atomic进行映射读取内容。

当然为了加快映射可以使用硬件设备直接缓存虚拟内存和物理内存的映射关系,就不需要想链表一样层层寻找,一步到位。这种硬件成为TLB,快表。

Binder mmap

Binder 驱动重写了mmap的file_operation,此时mmap就会调用Binder的mmap

按照步骤做了如下几件事:

    1. 准备内核空间准备内核的虚拟内存 为结构体vm_area设置和在用户空间获取到的vm_area_struct相同mmapsize大小。注意在serviceManager中申请的大小为128*1024
    • 1.1.把vm_area绑定到binder_procbinder_buffer
    • 1.2.为了快速查找用户空间申请的虚拟内存和内核中申请的虚拟内存,计算出vm_areavm_area_struct之间的地址差值,保存到binder_procuser_buffer_offset

每个进程对应映射区的内核线性区 + user_buffer_offset = 每个进程映射区的用户态线性区

  • 2.为binder_procbinder_bufferbinder内核缓冲区绑定物理页.

    • 2.1.通过kzalloc为binder_proc-> pages链表中每一个page元素申请大小。binder驱动为当前的用户空间虚拟内存vm_area_struct的操作结构体设置一个全新的binder_vm_ops.除了close之外,都没做什么事情。相当于屏蔽了一些系统对这段用户空间虚拟内存vm_area_struct的默认操作。

    • 2.2.vm_area_struct-> vm_private_data保存binder_proc结构体

    • 2.3.binder_update_page_range在上层函数为数组申请了页框数组的内存,这里就要通过循环,从vm的start开始到end,每隔4kb申请一次页框(因为Linux内核中是以4kb为一个页框,这样有利于Linux处理简单)。每一次通过alloc_page通过伙伴算法去申请物理页面,最后通过map_vm_areavm_area(内核空间的线性区)和物理地址真正的绑定起来。根据计算上面总结,我们同时可以计算出每一页对应的用户空间的页面地址多少,并且最后插入到pagetable(页表)中管理。

  • 3.把binder_buffer 插入到binder_procfree_buffers红黑树中.来研究研究binder_buffer的构成
struct binder_buffer {
    struct list_head entry; //binder_buffer的链表
    struct rb_node rb_node; //binder_node的红黑树
    unsigned free:1;
    unsigned allow_user_free:1;
    unsigned async_transaction:1;
    unsigned debug_id:29;

    struct binder_transaction *transaction;//binder通信时候的事务

    struct binder_node *target_node;//目标binder实体
    size_t data_size;//数据缓冲区大小
    size_t offsets_size;//元数据区的偏移量
    uint8_t data[0];//指向数据缓冲区的指针
};

简单的说,binder_buffer可以分为两个部分。一部分是binder_buffer 内核缓冲区持有的属性,一部分是binder_buffer持有的缓存数据.而数据只记录了指针,所以还需要记录缓冲的数据大小。

因此插入链表,需要计算binder_buffer的大小,主要还是计算binder_buffer的缓冲数据的大小,可以分为两种情况来讨论,如何查找binder_buffer的大小。之所以要查找大小,目的就是为了找到合适的位置插入到binder_proc->free_buffers中:

buffer大小在中间时候的计算.png
buffer大小在末尾时候的计算.png

binder_buffer的申请内存的核心原理:binder会尝试着从当前的大缓冲区切割一个小的buffer,当可以满足当前内核缓冲区的使用同时,并且能够满足一个binder_buffer的大小,就把当前的这个小的buffer切割下来,放进空闲内核缓冲区中

  • 4.保存vm_area,以及free_async_space

整个 binder的mmap的原理流程可以看成如下:

binder_mmap原理图.png

经过系统默认的mmap和binder的mmap比较。可以发现最大的不同是什么呢?

  • 1.系统默认的mmap是按需获取物理页。而binder的mmap是一旦调用了mmap就会绑定物理内存。这么做最大的好处是,加速了binder在后续的通信,特别是Android的应用进程,时时刻刻都需要binder进行通信,一开始就申请好,比起需要使用时候,发生中断再去申请性能体验上更好

  • 2.binder的mmap需要做到用户空间的虚拟内存和内核申请的内核缓冲区也就是内核的虚拟内存需要一一对应上,把一个物理页同时绑定在用户空间的虚拟内存以及内核空间的虚拟内存中;系统的mmap则不需要,系统的mmap会根据是匿名映射还是文件映射都不需要(我们忽略掉换入换出),前者是通过伙伴系统绑定物理内存,后者则是把文件的缓存读到到文件绑定的vm_area_structpages链表中。

更加详细的解析在:https://www.jianshu.com/p/4399aedb4d42 一文中

service_manager 进入到Binder的Looper阻塞等待

service_manager进入到Binder的Looper阻塞可以分为如下几个步骤:

    1. 调用ioctl系统调用,发送命令BINDER_SET_CONTEXT_MGR到Binder驱动中.
    • 1.1.把service_manager 进程在Binder中申请一个特殊的binder_node在全局静态变量binder_context_mgr_node中.并把这个binder_node插入到binder_proc->nodes红黑树中。
    • 1.2.设置binder_context_mgr_nodecookie和ptr都是0.设置binder_work的type为BINDER_WORK_NODE
    • 1.3.初始化binder_node的异步队列以及binder_node中的binder_workentry队列
    • 1.4.binder_node持有当前为他申请的内存的进程binder_proc对象

为什么这么做?因为Android应用经常通过Binder驱动去service_manager中查找,Android提供服务的Binder 服务端对象。因此直接独立出来,当需要查找时候,直接拿到这个对象通信即可。

  • 2.service_manager启动消息等待循环 分为如下两个大步骤

    • 2.1.调用ioctl系统调用发送BINDER_WRITE_READ命令,发送BC_ENTER_LOOPER数据,不过设置的read_size为0,write_size为32位。这样就能避免Binder驱动进行读取操作。

      • 2.1.1.通过get_user拷贝用户空间传递下来的数据;并在binder_thread_write 方法的switch BC_ENTER_LOOPER分支,设置binder->looperBINDER_LOOPER_STATE_ENTERED ;最后通过copy_to_user把处理的结果返回给用户
    • 2.2.调用ioctl系统调用发送BINDER_WRITE_READ命令,发送BC_ENTER_LOOPER数据。这时候设置了write_size为0,read_size的为BC_ENTER_LOOPER的大小。

      • 2.2.1.此时在binder_thread_read会判断binder_thread->transaction_stack是否为空和binder_thread->todotodo队列中是否有任务消费。如果没有任何事务,binder调用wait_event_freezable_exclusive进入到schdule模块,把binder_proc->wait进程的等待队列添加到系统中,让出cpu进入休眠。直到有人唤醒,也就是有进程往service_manager进程写入数据,并唤醒。

原理图如下:


binder驱动在Android service系统初始化.png

更加详细的原理,阅读https://www.jianshu.com/p/2ab3aaf2aeb6一文

到这里就完成了Android系统的初始化Binder驱动,以及ServiceManager服务。


binder驱动初始化.png

也就是画红框的区域。解析来让我们来总结App进程初始化,以及App进程是如何和SystemServer进行Binder交互。

App进程 Binder 服务的初始化第二种Looper

App想要通过Binder驱动和其他App应用或者系统服务,必须自己也要在binder驱动中申请属于自己的binder_proc对象,这样才能通过binder驱动中,类似消息队列的机制,把信息跨进程的通信。

App进程 Binder 服务的初始化

所以进程启动期间调用RuntimeInit.zygoteInit的时候,在Binder驱动中通过ProcessState进行初始化。注意ProcessState是一个单例对象,进程内全局唯一。

ProcessState实例化流程

在这个对象的实例化过程中一次执行如下的事情:

  • 1.调用open 去初始化Binder 驱动对应的内存文件路径。其行为和service_manager的open一致。主要是实例化一个binder_proc在内核中,并添加到binder_procsbinder的静态属性中

  • 2.调用ioctl 发送BINDER_VERSION 获得版本号

  • 3.调用ioctl 发送BINDER_SET_MAX_THREADS 命令,设置binder_proc->max_threads属性为15.

  • 4.调用mmap系统调用,在Binder驱动中映射一段内核虚拟内存和用户空间虚拟内存,并为这段用户空间的虚拟内存绑定物理页。binder_buffer直接绑定内核虚拟内存。注意这段映射的大小为((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2) 也就是1M - 4kb * 2 = 1016kb。

换句话说,应用进程和service_manager进程映射的大小是完全不同的。其实原因页很简单,service_manager只是负责查询注册在Android系统中的Binder信息,不需要这么的空间。而App进程往往需要传输各种各样的数据,因此需要更大的内存。

注意因为get_vm_area申请内核虚拟内存的时候使用的flag是VM_IOREMAP,则通过通过ioremap分配的页,将一个IO地址空间映射到内核的虚拟地址空间上去。

IPCThreadState实例化流程

当实例化好了ProcessState之后,就说明了该进程拥有了通过Binder进行通信的能力,此时还差一个looper,类似service_manager一样的进行消息循环。

  • 1.此时会调用startThreadPool方法,启动一个IPCThreadState对象

注意IPCThreadState 这个对象是线程唯一的,他会保存在线程的本地变量中。其实就是Java编程中ThreadLocal相似的概念。

由此可以得知,每一个线程想要对Binder进行通信,都会先创建一个IPCThreadState形成自己的阻塞。

  • 2.接着调用IPCThreadStatejoinThreadPool进入到阻塞监听

IPCThreadState 启动阻塞原理

  • 1.IPCThreadState joinThreadPool执行的时候就会设置如下命令:
 mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);

接下来joinThreadPool中进行一个无限遍历阻塞监听。

  do {
        processPendingDerefs();
        result = getAndExecuteCommand();

        if(result == TIMED_OUT && !isMain) {
            break;
        }
    } while (result != -ECONNREFUSED && result != -EBADF);

在getAndExecuteCommand中会执行talkWithDriver方法,执行系统调用ioctl发送命令BINDER_WRITE_READ告诉Binder驱动,当前App进程的Looper也初始化好了,Binder中对应的binder_procbinder_threadloop也可以设置为BINDER_LOOPER_STATE_ENTERED

而这个过程和service_manager不同,因为service_manager是阻塞了整个进程,只需要等到有人向他查询是否在Binder中存在这么一个服务才会继续运行。这里当然不可能阻塞整个进程,不然我们的app该怎么继续初始化后运行。

到这里就完成了App进程接受Binder驱动信息的Looper的初始化,直到等到Binder驱动返回了BC_ENTER_LOOPER的消息的处理结果。

整个流程图可以如下:


应用启动时启动的Binder初始化.jpg

在这里构建好的Looper,做的事情实际上就是为了处理aidl模块。不过aidl的通信前提是必须要从service_manager进程查询到对应的IBinder服务,才能进行通信。因此先来IPCThreadState 通信到Binder驱动的流程。

IPCThreadState交互原理

整个流程如下图:


binder数据交互时序图.png

我们就以ServiceManagerNative 注册一个AMS服务到service_manager中。

注意在Android系统中有三中ServiceManager,很多人容易搞混:

  • service_manager是指 一个独立的进程,里面保存了SystemServer或者其他应用开放给Android系统的Binder 服务
  • ServiceManager 是指在SystemServer中用于统一管理的服务
  • 还有一个是位于App进程的Context中SystemServiceRegistry对象,这个对象保存了每一个Android的Binder服务的代理类。如AMS对应ActivityManager。

先获取当前的目标进程也就是Binder服务端的句柄,调用getStrongProxyForHandle方法,lookupHandleLocked从本地中查找是否存在对应的IBinder对象,找不到且句柄刚好是0说明是向service_manager通信,此时就会进行调用:

IPCThreadState::self()->transact(
                        0, IBinder::PING_TRANSACTION, data, NULL, 0);

当Binder需要发送消息的时候,就会调用IPCThreadStateself方法获得当前线程中IPCThreadState实例,并调用transact方法,往Binder通信。其中的核心方法还是waitForResponse方法,
整个transact通信流程大致可以分为如下几个步骤:

  • 1.writeTransactionData 在Parcel中构造第一段数据BC_TRANSACTION命令的binder_transaction_data数据,命令内容如下:

    • 1.1.cmd:BC_TRANSACTION
    • 1.2.tr : binder_transaction_data
    • 1.3.code: IBinder::PING_TRANSACTION
    1. waitForResponse 中会构造一个Looper 调用talkWithDriver方法,不断的等待Binder处理完事务后的结果:
    while (1) {
        if ((err=talkWithDriver()) < NO_ERROR) break;
        err = mIn.errorCheck();
        if (err < NO_ERROR) break;
        if (mIn.dataAvail() == 0) continue;
  • 3.talkWithDriver 会继续往Parcel写入第二段数据,保存到binder_write_readbuffer属性中 ,并记录其写入的长度。最后通过ioctl发送命令BINDER_WRITE_READ 把数据传入到底层。注意这个发送过程也是一个循环遍历,知道发送成功,或者Binder返回了非-EINTR的异常信号才会退出。
    do {
#if defined(__ANDROID__)
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        else
            err = -errno;
#else
        err = INVALID_OPERATION;
#endif
        if (mProcess->mDriverFD <= 0) {
            err = -EBADF;
        }

    } while (err == -EINTR);

所以此时发送到Binder驱动中的数据结构如下:


用户空间传递到Binder驱动的数据封装.jpg
  • 4.由于此时需要同步接受数据:
    if (doReceive && needRead) {
        bwr.read_size = mIn.dataCapacity();
        bwr.read_buffer = (uintptr_t)mIn.data();
    } else {
        bwr.read_size = 0;
        bwr.read_buffer = 0;
    }

binder_write_read中设置了读取的大小以及读取对应的Parcel数据块,因此当在binder驱动调用完binder_thread_write后就会调用binder_thread_read阻塞住整个流程。

到这里,就构建出了第三种Looper,这个Looper是为了处理IPCThreadState调用 self方法直接通信,并阻塞等待Binder‘服务端处理后的结果。

Binder通信流程

整个流程,我们可以分开两个部分来看,一个是Binder的客户端,一个是Binder的服务端。

我们就以1个例子来解释整个通信流程:

  • 1.App进程通过service_manager查询AMS服务为例子。

此时Binder客户端就是App进程,Binder的服务端就是指service_manager进程。

App进程查询AMS服务

在这里我分为客户端和服务端两端来总结:

App进程 Binder客户端发送向Binder 服务管理者通信请求查询Binder 服务对应的IBinder

在这个过程就会把上面封装好的binder_write_read结构体通信到Binder驱动中,Binder驱动必定会进行解包处理。由于此时只有写入的内容,我们只需要关注写入的binder_thread_write的逻辑,这里一次做了如下的事情:

  • 1.首先获取binder_write_read中的write_buffer内容,在这里也就是binder_transaction_data 事务数据结构体。

  • 2.解析结构体中的cmd 为BC_TRANSACTION,则执行binder_transaction处理客户端传递过来的事务。

  • 3.接着判断cmd是否是BC_REPLY,是则说明这个事务是从服务端传递过来,处理完事务的返回消息。由于是发送,则判断binder_transaction_data中需要通信的Binder服务端的handle句柄。此时是0,设置为默认的binder_context_mgr_node 此时就直接找到存放在全局的静态变量,代表service_managerbinder_node对象。也就找到接下来需要通信的Binder服务端是什么了。

  • 4.通过需要通信的Binder服务端对应的binder_node中获取到binder_proc对象。

  • 5.遍历当前Binder客户端 也就是App的binder_procbinder_thread中的transaction_stack 事务处理栈。准备构建binder_transaction结构体,通信到Binder服务端,在这里就是service_manager.

    • 5.1.如果发现这个堆栈有事务,说明当前进程的其他线程有正在执行的binder通信任务,并且查找通信的目标,和本次通信的目标是一致的。说明Binder服务端可能在处理Binder通信的事务正准备返回呢?

    • 5.2.当没有任务事务依赖,双方都是第一次通信,另一个第一次接受。此时会给binder_transaction->from设置为当前Binder 客户端的binder_thread.更加复杂的场景下,客户端和服务端可能正在通信,因此会为把目标的target_thread 设置为上一个正在通信到Binder客户端的binder_thread. 通过这种方式设置每一个事务的依赖。

  • 6.为binder_workbinder_transaction结构体申请一段内核的内存.

  • 7.binder_transaction 记录下目标通信的Binder服务端对应的binder_proc,binder_thread;从IPCThreadState传下来的binder_transaction_datacode,flag;

    • 7.1.通过binder_alloc_buf方法为binder_transaction-> buffer(也就是binder_buffer)申请一段内存。binder_buffertransaction指向当前的binder_transaction以及保存了Binder服务端的binder_node.把binder_transaction_data->data.ptr.buffer 也就是IPCThreadState用于缓冲的数据段,拷贝到binder_buffer->data

    • 7.2.获取binder_transaction_data-> data.ptr.offsets这个偏移量实际上指向的是flat_binder_obj.这个对象代表了Binder服务端或者客户端在用户空间的中压缩数据的表示,如果是本进程的binder对象在这个结构体里面就保存了指针,如果是另一个进程的Binder 服务端就是一个handle句柄。如果是本地对象cookie保存的是在内核中对应binder_nodebinder_proc->nodes红黑树中的位置。

struct flat_binder_object {
    /* 8 bytes for large_flat_header. */
    __u32       type;
    __u32       flags;

    /* 8 bytes of data. */
    union {
        binder_uintptr_t    binder; /* local object */
        __u32           handle; /* remote object */
    };

    /* extra data associated with local object */
    binder_uintptr_t    cookie;
};
  • 8.通过binder_transaction_data-> data.ptr.offsets 获取到flat_binder_object。接下来就根据flat_binder_object->type的Binder类型进行相应的处理:
BINDER_TYPE 意义 处理方式
BINDER_TYPE_BINDER 传输的是本地Binder对象 通过cookiebinder_proc寻找binder_node,找不到则根据flat_binder_object->cookieflat_binder_object->binder生成新的binder_node,根据binder_get_ref_for_node寻找或者生成新的binder_ref,把flat_binder_object->type转化成BINDER_TYPE_HANDLE,并在flat_binder_object->handle保存binder_ref->desc
BINDER_TYPE_WEAK_BINDER 传输的是本地Binder对象弱引用 通过cookiebinder_proc寻找binder_node,找不到则根据flat_binder_object->cookieflat_binder_object->binder生成新的binder_node,根据binder_get_ref_for_node寻找或者生成新的binder_ref,把flat_binder_object->type转化成BINDER_TYPE_WEAK_HANDLE,并在flat_binder_object->handle保存binder_ref->desc
BINDER_TYPE_HANDLE 传输的是远程Binder对象句柄 通过binder_get_refbinder_proc->refs_by_desc找到对应的binder_ref对象,如果binder_ref对应的binder_node刚好是Binder服务端的binder_node,则把flat_binder_object->type转化为 BINDER_TYPE_BINDER;否则则从binder_get_ref_for_node获取到另一个Binder服务端的binder_node,flat_binder_object->handle保存binder_ref->desc
BINDER_TYPE_WEAK_HANDLE 传输的是远程Binder对象句柄弱引用 通过binder_get_refbinder_proc->refs_by_desc找到对应的binder_ref对象,如果binder_ref对应的binder_node刚好是Binder服务端的binder_node,则把flat_binder_object->type转化为 BINDER_TYPE_WEAK_BINDER;否则则从binder_get_ref_for_node获取到另一个Binder服务端的binder_node,flat_binder_object->handle保存binder_ref->desc
BINDER_TYPE_FD 传输的是文件 暂时不做事情

在这里面出现了一个很重要的数据结构binder_ref。这是Binder从本地端往远程端转化的核心。

总结下来如图:


binder对象传送过程.png

根据是否和Binder 服务端管理端是同一个进程,是否和客户端进程是同一个进程,从而诞生了三种返回。

这一段的逻辑主要是进行Binder对象的返回。之所以我在原文里面不喜欢说Binder客户端和服务端,只说本地端和远程端也是基于这种考虑。他们之间其实并没有很明确的区分界限,Binder的客户端也可以当服务端,Binder的服务端可以当客户端。这里的Binder 服务端不仅仅只是指Android系统中的Binder服务,还可以是Service在onBind返回的Binder对象的服务,也可以是RePlugin中通过ContentProvider管理并返回的Binder的服务。

  • 8.把binder_transaction t中的binder_work添加到目标binder_proc或者binder_threadtodo队列中.设置binder_work- >typeBINDER_WORK_TRANSACTION
    当然这个过程中,如果没有事务依赖也不是Binder服务端处理完事务后返回,binder_thread则为null,会设置到binder_proc->todo

  • 9.把当前binder_threadtransaction_stack设置为binder_transaction,并把当前的binder_transaction设置到binder_transaction tfrom_parent这个binder_transaction链表

  • 10.除了binder_transaction之中存在一个binder_work之外,这个过程也构造名字为to_completebinder_work,这个binder_work的type设置为BINDER_WORK_TRANSACTION_COMPLETE,加入到当前进程对应的binder_procbinder_threadtodo队列

  • 11.通过wake_up_interruptible唤醒目标进程的等待队列

代入App发送一个命令,往service_manager查询服务的场景。这个时候,首先往service_manager中传入空的数据,但是命令为BC_TRANSACTION.由于handle默认是0,就直接拿到了service_manager对应的binder_node,也就拿到了binder_node中进程相关的信息binder_proc。并往service_managerbinder_proc->todo中添加一个binder-work工作事务。在这个binder_work中的内核缓冲区,保存好了数据,等待service_manager读取。

Binder 服务管理者到查询Binder 服务后返回给App进程

当唤醒了service_manager,就会解除binder_thread_read的阻塞。下面分为如下几步,处理完后到service_manager进程中处理

首先是一个while的循环,不断的从当前进程的service_manager binder_threadbinder_proctodo队列中获取binder_work 事务进行消费。

  • 1.检查出binder_work- >typeBINDER_WORK_TRANSACTION,则取出其中的binder_transaction

  • 2.把传递过来的binder_transaction中的binder_node中的cookieptr都拷贝到binder_transactioncookieptr,并把命令转化为binder_transaction中的命令从BC_TRANSACTION转化为BR_TRANSACTION

  • 3.把解析出来的binder_transactionto_parent保存了当前Binder服务端的transaction_stack,以及to_thread设置为当前Binder服务端的binder_thread

  • 4.把binder_transaction设置为Binder服务的binder_thread->transaction_stack.并数据拷贝回用户空间并返回。

Binder服务管理器 查询服务

此时管理Binder 众多服务的是service_manager进程,此时就会解开binder_loop的死循环,进入到binder_parse中解析从Binder 驱动回调的命令。

BR_TRANSACTION的分支中,会再度解析存在binder_transaction_data的code。

还记得,当进程第一次Binder通信时候,就会发送code就是PING_TRANSACTION的binder通信。所以每一个进程必定在Binder驱动中有属于自己的binder_node,这么做的目的很简单,就是为了保证了Binder的客户端和service_manager进程都存活。

那么当App进程向service_manager查询AMS服务,和AMS诞生时候添加的服务又是如何的?实际上整个过程变化的只有binder_transaction_data的code:

  • ADD_SERVICE_TRANSACTION 对应 SVC_MGR_ADD_SERVICE代表Binder服务初始化完成进行注册
  • SVC_MGR_GET_SERVICE 代表App进程查询服务

注意在service_manager进程中,保存就是binder_ref 对应的desc 句柄。

当完成了这些之后,就会调用binder_send_reply方法,把BC_FREE_BUFFER,BC_REPLY两个命令压缩到一起发送到binder驱动中,通过ioctlBINDER_WRITE_READ命令写回Binder驱动。

Binder服务管理器 根据句柄返回Binder驱动的Binder对象

  • BC_FREE_BUFFER 释放了binder_procbinder_buffer的内核缓冲区。注意这里的binder_buffer实际上是指写入时候的binder_transaction_data->data.ptr.buffer内核缓冲区

  • BC_REPLY 执行的流程和BC_TRANSACTION分支重合。不同点在于,如下:

  • 1.在service_manager读取的最后一个步骤,把binder_threadtransaction_stack设置为从App进程获取到的binder_transaction,此时就能很简单的知道,需要返回的Binder客户端是什么
if (reply) {
        in_reply_to = thread->transaction_stack;
...
        binder_set_nice(in_reply_to->saved_priority);
...
        thread->transaction_stack = in_reply_to->to_parent;
        target_thread = in_reply_to->from;
...
        target_proc = target_thread->proc;
    }

这里面包含了Binder客户端的binder_threadbinder_proc.

  • 2.经过Binder 通过句柄handle转化后,就能获得了flat_binder_obj。这里面要么存在着远程端的句柄,要么就存在着对应在Binder客户端cookie

  • 3.继续重复发送Binder消息一小节的的步骤,添加一个binder_transactionbinder_proc或者binder_thread的todo队列中,打开Binder客户端的也就是App进程的阻塞,执行binder_thread_read方法。

图解整个流程如下:


发送数据.png

第二部分是返回消息在清除binder_transaction_stack之前:


返回消息在清除binder_transaction_stack.png

第三部分:


binder传输原理.png

从这里面可以得知,为什么说Binder驱动只是进行了一次拷贝。这个拷贝不是我们常说的拷贝,而是特指两个进程之间需要同步物理内存所需要的拷贝次数。

检查整个过程就是,为binder_work中的binder_buffer申请缓存并缓存好需要传输的binder_transaction.每一次进行数据传输,实际上就是在共享binder_transaction对应的binder_buffer内存缓冲区。

而这个binder_buffer实际上就是在binder_mmap阶段一口气申请出来的大内存切割出来的。而这个大内存是通过binder_page_update_range把一个物理页同时绑定在用户空间的虚拟内存以及内核空间的虚拟内存中。

这种方式下,访问了用户空间的虚拟内存就是访问了用户空间的虚拟内存。那么相对的Binder客户端和Binder服务端,都有一个虚拟内存映射到内核中。

因此当从虚拟内存从内核空间往用户空间,或者用户空间往内核转移时候不会有任何的中转站。

真正发生物理页拷贝的是Binder客户端读取数据拷贝到binder_transaction,以及binder_transaction需要从内核拷贝到Binder服务端的用户空间中。

当然常说的一次拷贝是忽略了前者的拷贝,也就是忽略了内核中的拷贝次数,而只关注进程切换内核态和用户态的过程中需要获得数据的后者步骤。

在这个过程中,也不是一口气拷贝binder_transaction下来,而是分段拷贝。单次通信有6次小拷贝,但是一个Binder通信的完成需要一个来回,因此需要12次小来回。

最后再来看看整个Binder 在内核中的数据传输封包

binder传输数据的封包.png

关于这里的详情可以看https://www.jianshu.com/p/04e53fd86ca2

AIDL与Java层的交互

先来看看面向Java的UML图


binder类依赖图.png

从图中可以看到,有几个很关键的类:

  • 1.BBinder 是native层的Binder对象,他是一个真正持有对应binder驱动下binder_node的cookie对象
  • 2.JavaBBinder 是BBinder的派生类,每当消息来了就会反射Binder的onTransact方法
  • 3.Binder java层代表本进程中对应的Binder类,他持有了JavaBBinder
  • 4.BpBinder 是native层的代表远端进程的Binder对象类,持有了远端进程的句柄handle
  • 2.BinderProxy java层代表远端进程的Binder类,持有了BpBinder

换句话说,整个流程实际上就是Binder客户端的Java层调用BinderProxy类调用transact 方法后,就会反射到aidl的Binder服务端的Binder类中的onTransact执行解析以及业务处理。

最后来看看状态转移:


binder握手通信.png

关于这一块的内容可以阅读https://www.jianshu.com/p/84b18387992f里面有更详细对aidl的解析。

Binder整个过程用一个示意图来表示如下:


binder整体模型.png

binder 的死亡代理

下面是linkToDeath时序图:


binder 注册死亡代理.png

下面是当App进程时候的死亡回调时序图


binder死亡唤起死亡注册的回调.png

注意:红色线代表了跨进程

更多的请阅读https://www.jianshu.com/p/e22005e5c411里面包含了关于死亡代理的详细内容。

后话

写这篇文章主要还是为了对之前Binder的回顾,希望有对这6篇文章对总结,加上这几年看了更多的Linux内核的源码有了更加深刻的认识。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349