1

前言

本文主要对Binder驱动下的数据结构和设备的初始化过程进行简要的分析,以理清数据结构对象之间的关系。基于Linux-4.15

Binider进程的通信机制如下图所示:

图片描述

Client、Service以及ServiceManager都运行在用户空间中,而Binder Driver运行在内核空间中,其中ServiceManager和Binder驱动由系统负责提供,Clien和Services由应用程序实现。而Binder驱动与Service、Client、Servicemanager的交互是通过open、mmap、ioctl等接口实现通信的。为了更好地理解上层的Binder通信,因此有必要对下层驱动进行简单的分析。

1. 数据结构

1.1 binder_work 处理的工作项

struct binder_work{
    //用于嵌入宿主,包括进程,线程
    struct list_head entry;
    //描述工作项类型
    enum {
        BINDER_WORK_TRANSACTION = 1,
        BINDER_WORK_TRANSACTION_COMPLETE,
        BINDER_WORK_RETURN_ERROR,
        BINDER_WORK_NODE,
        //下面三项为死亡通知类型
        BINDER_WORK_DEAD_BINDER,
        BINDER_WORK_DEAD_BINDER_AND_CLEAR,
        BINDER_WORK_CLEAR_DEATH_NOTIFICATION,
    } type;
}
  • binder_work主要是为了描述待处理的工作项,由于工作项有可能属于进程或线程,利用entry可以将该结构体嵌入到宿主的结构当中,而list_head的数据结构是双向链表的节点,而list_head本身是不带有数据阈的,其常用的用法即嵌入到其他数据结构如binder_work形成链表,双向链表的结构如图所示:

图片描述

有关kernel双向链表的实现,可参考Kernel list数据结构学习

  • type描述的是工作项的类型,可以分为

    • BINDER_WORK_TRANSACTION
    • BINDER_WORK_TRANSACTION_COMPLETE
    • BINDER_WORK_TRANSACTION_NODE
    • BINDER_WORK_DEAD_BINDER
    • BINDER_WORK_DEAD_BINDER_AND_CLEAR
    • BINDER_WORK_CLEAR_DEATH_NOTIFICATION

1.2 binder_node 实体对象

binder_node用于描述Binder实体对象,每个Service组件都在Binder驱动中对应一个binder_node实体,以描述其在内核的状态。其数据结构如下:

struct binder_node {
    int debug_id;
    spinlock_t lock;
    struct binder_work work;//Binder实体需要处理的工作项
    union {
        struct rb_node rb_node; //红黑树节点,宿主进程有一棵红黑树维护所有Binder实体
        struct hlist_node dead_node;//hash节点,用于保存在全局hash列表中
    };
    struct binder_proc *proc; //用于指向Binder实体对象宿主进程
    struct hlist_head refs; //所有Client引用了该Binder实体都会保存在该hash变量中
    int internal_strong_refs;//强引用计数
    int local_weak_refs; //弱引用计数
    int local_strong_refs; //强引用计数
    int tmp_refs;
    binder_uintptr_t ptr;//指向Service组件地址
    binder_uintptr_t cookie;//指向Service组件内部的引用计数
    struct {
        /*
         * bitfield elements protected by
         * proc inner_lock
         */
        u8 has_strong_ref:1;
        u8 pending_strong_ref:1;
        u8 has_weak_ref:1;
        u8 pending_weak_ref:1;
    };
    struct {
        /*
         * invariant after initialization
         */
        u8 accept_fds:1;
        u8 min_priority;
    };
    bool has_async_transaction;//Binder实体是否正在处理一个异步事务
    struct list_head async_todo;//用于保存异步事务队列
};
  • debug_id用于在debug中标志Binder实体的ID身份
  • binder_node包含一个binder工作项
  • proc是指向Binder实体对象的宿主进程
  • 假如宿主并没Dead,那么宿主进程将会使用红黑树来维护其内部的Binder_node实体,而binder_node实体中将会有一个rb_node(红黑树)节点结构,作为宿主红黑树中的一员。
  • 假如宿主已经Dead了,那么将会将节点加入到全局Hash表中。

在这里可以稍微补充下关于红黑树的基本特性:

  • 每个节点要么是红色要么是黑的
  • 根节点是黑的
  • 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的
  • 如果一个结点是红的,那么它的两个儿子都是黑的。
  • 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。
红黑树的优势在于能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。

图片描述

  • 值得称奇的是由于一个Binder实体的宿主程序只会alive或者dead,那么其节点不是存在红黑树中就是存在哈希表中,因此设计者使用了union结构将空间使用率压缩到了极致,在后面的数据结构也经常碰到。
  • 由于一个binder实体可能会被多个client组件所引用,因此binder通过数据结构binder_ref来进行描述(后面会介绍),因此将所有引用了该binder实体对象的所有引用都放在了哈希表当中,即hlist_head refs
  • internal_strong_refs,local_strong_refs均描述binder实体对象的强引用数,local_weak_refs描述了其弱引用数。has_strong_ref,has_weak_ref,pending_strong_ref,pending_weak_ref则是相关的标志位。其中又有如下规则:

    • 当Binder实体对象请求Service组件执行某操作时,将会增强Service组件的强引用数以及弱引用数。此时has_strong_ref,has_weak_ref均置1。
    • 反之当Service组件完成Binder的请求操作时,将会减少其强引用计数以及弱引用计数。
    • 当增加或减少的过程中,pending变量将置1,而当完成增加减少后,才会置0。
  • ptr和cookie分别指向一个用户空间地址,其中cookie指向Service组件的地址,ptr指向Service组件内部的一个引用计数对象地址。这两个变量用以描述用户空间的Service组件。
  • has_async_transaction描述是否正在处理异步事务。一般而言Binder驱动程序都将一个事务保存在线程的todo队列中,表明线程将要处理该事务。而每个事务和一个binder实体相关联,表示该事务的处理目标对象。而通过binder_node是可以找到对应的Service组件,那么与该Binder对应的Service组件将会处理该事务。

图片描述

注:异步的概念就是单向的进程间通信请求,即不需要等待应答的进程间通信请求。同步则需要等待应答才会做下一步的动作。由于不需要应答,Binder驱动认为异步事务优先级比同步优先级要低。
  • min_priority表示Binder实体在处理来自Client的请求时,处理线程(Server进程中的线程)所应该具备的最小线程优先级
  • accept_fds描述Binder实体对象是否可以接收包含有文件描述符的进程通信数据。为1时可以接收。

1.3 binder_ref_death 死亡接收通知

struct binder_ref_death {
    /**
     * @work: worklist element for death notifications
     *        (protected by inner_lock of the proc that
     *        this ref belongs to)
     */
    struct binder_work work;
    binder_uintptr_t cookie;
};

binder_ref_death用来描述Binder的死亡接收通知。正常而言,当Service组件被其他Client组件所引用时,是不能被销毁的,但是存在Service以外崩溃的情景,此时,Client能够通过Binder驱动得知Service Dead的消息。因此client会将一个能够接收死亡通知的地址注册在Binder驱动程序中。

  • work是Binder工作项,可以取值为有关dead的类型,即标志一个具体的死亡通知类型。
  • cookie保存的是接收死亡通知对象的地址。
  • 当Binder驱动决定要向Client发送一个Service组件死亡通知时,将会把这个结构封装成一个工作项,并依据场景设置work值,最后将工作项添加到Client的todo队列中处理!

Client接收Binder驱动死亡通知情景:

  1. Binder驱动程序检测到Service组件死亡,通过该死亡Service组件找到对应的Binder实体,然后通过该实体对象的成员变量refs找到所有Client进程,最后找到这些client进程注册的所有死亡接收通知,即binder_ref_death结构体,并将这些结构体添加到Client进程的todo队列中等待处理,死亡通知类型设置为BINDER_WORK_DEAD_BINDER
  2. 当Client进程向Binder驱动注册一个死亡接受通知时,如果Service组件已死亡,则Binder驱动立刻发送死亡通知给该Client进程,死亡通知类型设置为BINDER_WORK_DEAD_BINDER

Client注销Binder驱动死亡通知情景

  1. 如果Client进程注销一死亡通知时,对应的Service组件未死,那么Binder驱动会找到之前注册的binder_ref_death结构体,并将类型work修改成为BINDER_WORK_CLEAR_DEATH_NOTIFICATION,然后将该结构体封装成一个工作项添加到client进程的todo队列等待处理。
  2. 如果Service组件已死,流程与上述类似,但类型为BINDER_WORK_DEAD_BINDER_AND_CLEAR

1.4 binder_ref 引用对象

与Service组件在Binder驱动中存在一个Binder实体类似,Client在Binder驱动中也对应一个Binder引用对象以描述其在内核的状态。同样也是使用强弱计数来控制生命周期。

binder_ref_datadebug_id,desc,strong,weak等属性封装起来。

struct binder_ref_data {
    int debug_id;
    uint32_t desc;
    int strong;
    int weak;
};
  • debug_id标志Binder引用对象,帮助Binder驱动程序进行调试。
  • desc(descriptor)是句柄值(描述符),它是用来专门描述引用对象的。在Client用户空间中,Binder引用对象是使用句柄至进行描述的。 因此当Client需通过Binder驱动来访问Service组件时,只需要通过描述符来寻找对应的Binder引用对象,再找到对应的Binder实体对象,从而找到相关的Service组件。
  • strong,weak是描述强弱引用计数,通过它们维护Binder引用对象的生命周期。
struct binder_ref {
    /* Lookups needed: */
    /*   node + proc => ref (transaction) */
    /*   desc + proc => ref (transaction, inc/dec ref) */
    /*   node => refs + procs (proc exit) */
    struct binder_ref_data data;
    struct rb_node rb_node_desc;//以desc为红黑树节点
    struct rb_node rb_node_node;//以binder实体为红黑树节点
    struct hlist_node node_entry;//哈希节点,放置在Binder实体哈希列表中
    struct binder_proc *proc;//binder引用对象的宿主
    struct binder_node *node;//指向binder实体
    struct binder_ref_death *death;
};
  • 在以binder_proc为结构的宿主中(进程或者线程)会有两棵红黑树(事实上宿主有多棵红黑树进行管理数据结构)保存Binder引用对象,而rb_node_descrb_node_node分别是红黑树中的节点,分别是以句柄值(desc)和对应的Binder实体对象的地址来作为保存的关键字。
  • proc是Binder引用对象的宿主。
  • binder_node *node是引用指向的Binder实体对象。
  • binder实体中存在哈希表来保存引用对象(即保存引用binder实体的Client),那么node_entry就是指放在哈希表里的节点。
  • death指向的是死亡通知,当Client向Binder驱动注册一个Service组件死亡通知时,驱动将会创建一个binder_ref_death结构体保存在death中。

图片描述

1.5 binder_buffer内核缓冲区

binder_buffer用来描述内核缓冲区,它是用于进程通信中传输数据的。每个参与Binder进程间通信的进程在Binder驱动进程中都有一个内核缓冲区列表,用于保存为进程分配的内核缓冲区,其中binder_buffer中的entry就是链表的节点。

struct binder_buffer {
    struct list_head entry; /* free and allocated entries by address */ 
    struct rb_node rb_node; /* free entry by size or allocated entry */
                /* by address */
    unsigned free:1; //判断该内核缓冲区是由空闲内核缓冲区红黑树还是正在使用的空闲内核缓冲区红黑树
    unsigned allow_user_free:1; //置1时,Service组件会请求Binder驱动程序释放内核缓冲区
    unsigned async_transaction:1; //关联的事务是否为异步
    unsigned free_in_progress:1;//判断是否正在释放内核缓冲区
    unsigned debug_id:28;

    struct binder_transaction *transaction;

    struct binder_node *target_node;//内核缓冲区正在使用的binde实体对象
    size_t data_size;
    size_t offsets_size;//数据缓冲区的偏移量,偏移数组保存Binde对象
    size_t extra_buffers_size;
    void *data;//指向大小可变的数据缓冲区
};
  • 与此同时,进程又维护着两棵红黑树分别管理正在使用的内核缓冲区以及空闲的内核缓冲区。因此rb_node就是在指红黑树的节点,而区别是那棵树是通过变量free来区别,假如free为1,那么是空闲内核缓冲区。
注:Binder驱动中经常看到unsigned 变量:n这样的写法,那是因为C中并没有bool类型,C Programmer会利用unsigned(这里的unsigned实际是unsigned int)并指定需要用的bits数目来实现,但令我疑惑的是虽然只指定使用其中的n bits,但是实际上还是以unsigned int的大小来存储数据的,但当struct程序是以__attribute((packed))来规定数据规格来编译时,那么其数据结构所占的大小就为1
  • binder_transaction为事务结构体,用以描述该内核缓冲区正在处理哪一个事务,值得留意的是binder_transaction中也存在者指向内核缓冲区的指针,即它们的关系是双向的。前面说到过事务与对应的Binder对象是相关联的,这是因为binder_buffer中存在着指向binder实体的指针,那么transaction通过找到内核缓冲区即可找到相关联的binder实体。从而将内核缓冲区内容交给Service处理。
  • allow_user_free为1时,则Service处理完事务后将会请求Binder驱动程序释放内核缓冲区。
  • data、data_sizeoffset_size分别用来描述数据缓冲区,其中data指向的是实际的数据内容。数据缓冲区保存的数据分为两种类型,一种是普通数据,另外的是Binder对象,Binder驱动不关心数据的内容,但是要知道其中的Binder对象,从而根据它们来维护内核中实体对象以及引用对象的生命周期。
  • 数据缓冲区后有一个偏移数组保存着每一个Binder对象的偏移位置,偏移数组大小保存在offset_size中。

1.6 binder_proc 进程间通信进程

struct binder_proc {
    struct hlist_node proc_node;
    struct rb_root threads;//Binder线程池根节点,以线程ID作为关键字
    struct rb_root nodes;//Binder实体对象红黑树根节点 
    struct rb_root refs_by_desc;//binder引用对象红黑树,以desc为关键字
    struct rb_root refs_by_node;//binder引用对象红黑树,以binder_node地址为关键字
    struct list_head waiting_threads;
    int pid;
    struct task_struct *tsk;
    struct files_struct *files;
    struct mutex files_lock;
    struct hlist_node deferred_work_node;//保存进程可以延迟执行的工作项,为哈希列表
    int deferred_work;
    bool is_dead;

    struct list_head todo;//待处理工作项,当进程接收到进程间通信请求时,Binder驱动封装工作项加入todo队列
    wait_queue_head_t wait;//等待队列
    struct binder_stats stats;//统计进程数据
    struct list_head delivered_death;
    int max_threads;
    int requested_threads;
    int requested_threads_started;
    int tmp_ref;
    long default_priority;
    struct dentry *debugfs_entry;
    struct binder_alloc alloc;
    struct binder_context *context;
    spinlock_t inner_lock;
    spinlock_t outer_lock;
};
  • binder_proc用来描绘使用Binder通信机制进行通信的进程,当进程调用函数open时打开/dev/binder时,Binder驱动将会为其创建一个binder_proc结构体,并将其保存在一个全局的哈希表当中,其中proc_node即为该进程在哈希表的节点。
struct binder_alloc {
    struct mutex mutex;
    struct vm_area_struct *vma;//内核缓冲区的用户空间地址
    struct mm_struct *vma_vm_mm;
    void *buffer;//内核缓冲区的内核空间地址
    ptrdiff_t user_buffer_offset;//内核缓冲区中用户空间地址与内核空间地址差值
    struct list_head buffers;//指向小块内核缓冲区链表的头部
    struct rb_root free_buffers;//空闲内核缓冲区红黑树根节点
    struct rb_root allocated_buffers;//已分配内核缓冲区红黑树根节点
    size_t free_async_space;//当前可以保存异步事务数据的内核缓冲区大小
    struct binder_lru_page *pages;
    size_t buffer_size;//binde驱动为进程分配的内核缓冲区大小
    uint32_t buffer_free;
    int pid;
};
  • 红黑树分别有之前提到的管理Binder引用对象的 refs_by_desc,refs_by_node,管理内核缓冲区的free_buffers,allocated_buffers,管理Binder实体对象的nodes。
  • 此外进程还有一个Binder线程池处理通信请求,也是使用红黑树进行处理,并且红黑树的根节点正是threads。线程红黑树的关键字是ID,进程通过ioctl能够将线程注册到Binder驱动程序中,当进程没有足够的线程处理时,Binder将会要求更多的线程注册到线程池当中。
  • ready_threads表示的是空闲的线程数目。
  • 当进程打开了设备文件后,还会调用mmap进行内存映射,即将设备映射到进程空间中,实际上是请求Binder为其分配一块内核缓冲区以便传输数据。而内核缓冲区有两个地址,分别是内核空间地址,一个是用户空间地址,内核空间地址是用buffer保存,用户空间是用vma保存。它们之间的相差的值为user_buffer_offset保存,因此只要知道其中一个地址便可以通过该相差值得出另外一个空间的地址。另外由于用户空间的地址高于内核空间地址,所以计算user_buffer_offset的差值时,都是使用用户空间地址减去内核空间地址。

clipboard.png

  • pid、tsk、files都是与进程相关的变量,即进程ID,任务控制块以及打开文件结构体数组。
  • 用户空间地址和内核空间地址都是虚拟地址,是线性地址,对应的物理地址是保存在类型为struct page*的pages的数组当中,每一个元素都指向一个物理页面。在最初的时候只为内核缓冲区分配看一个物理页面,到后面时继续分配。
进程收到进程间通信的请求时,将会把请求封装成工作项加入到进程的待处理工作项todo队列中。线程池中的wait的等待队列维护着空闲的Binder线程,当线程的宿主进程增加了新工作项后,Binder驱动就会唤醒这些线程以便它们能够处理新工作项。
  • Binder驱动还会将延迟工作的工作项保存在一个哈希表中,假如进程中有延迟执行的工作项,那么deffered_work_node正是哈希表的节点,并使用deffered_work维护延迟工作项的类型。

1.7 binder_thread

struct binder_thread {
    struct binder_proc *proc;
    struct rb_node rb_node;
    struct list_head waiting_thread_node;
    int pid;
    int looper;              /* only modified by this thread */
    bool looper_need_return; /* can be written by other thread */
    struct binder_transaction *transaction_stack;
    struct list_head todo;
    struct binder_error return_error;
    struct binder_error reply_error;
    wait_queue_head_t wait;
    struct binder_stats stats;
    atomic_t tmp_ref;
    bool is_dead;
};
  • binder_thread用来描绘Binder线程池的一个线程。其中proc指向线程的宿主进程。
  • 由于一个进程下有多个线程,rb_node正是进程维护的红黑树中的节点。
  • pid是线程的ID
  • looper是指线程的状态,其状态有以下几种:
enum {
    BINDER_LOOPER_STATE_REGISTERED  = 0x01,
    BINDER_LOOPER_STATE_ENTERED     = 0x02,
    BINDER_LOOPER_STATE_EXITED      = 0x04,
    BINDER_LOOPER_STATE_INVALID     = 0x08,
    BINDER_LOOPER_STATE_WAITING     = 0x10,
    BINDER_LOOPER_STATE_POLL        = 0x20,
};

参考文献: 《Android系统源代码情景分析》


西瓜大佬
14 声望2 粉丝