Android-Binder机制一探究竟

136 阅读16分钟

Android Binder机制?

image.png 图片来源于gityuan.com

什么是Binder?

Binder是Android中一个类,实现了IBinder接口,从IPC角度来讲,Binder是一种进程间通信的方式,还可以理解为一种虚拟的物理设备,设备驱动是/dev/binder,从framework层讲,Binder是ServiceManager连接各种XXXManager和XXXManagerService之间的桥梁,从应用角度来讲,Binder是客户端和服务端进行通信的媒介,bindService时,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或数据,这里的服务包括普通服务和基于AIDL的服务。
从系统角度讲,Binder机制分为Java Binder/Native Binder/Kernel Binder。
用户空间和内核空间是隔离的,用户空间想要访问内核空间就必须通过系统调用,copy_from_user 和 copy_to_user 。

内存映射?
应用程序不能直接操作设备硬件地址,所以系统提供了一种机制:内存映射,把设备地址映射到进程虚拟内存区。

  • 如果不采用内存映射,当用户空间想要读取磁盘文件时,就需要在内核空间建立一个页缓存,页缓存去复制磁盘的文件,然后用户空间复制页缓存的文件,这就进行了两次复制。
  • 如果采用内存映射,用户空间调用系统函数mmap来将用户空间的一块内存映射到内核空间。内核空间会创建一个虚拟内存区域,用来与磁盘文件直接映射,映射关系建好后,用户对这块内存区域的修改就直接反映到内核空间了,内存映射能减少数据复制次数,实现用户空间和内核空间的高效互动。

Binder基于内存映射来实现的。Binder通信的步骤如下:

  • Binder驱动在内核空间创建一个 数据接收缓存区
  • 在内核空间开辟一块 内核缓存区 ,建立内核缓存区和数据接收缓存区之间的映射关系,以及数据接收缓存区和接收进程用户空间地址的映射关系。
  • 发送方进程通过 copy_to_user 将数据复制到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此就是相当于把数据发给了接收进程的用户空间,完成一次进程间通信。

为什么选择Binder?

  • 性能好,管道、消息队列、Socket都需要复制两次,Binder只复制一次,仅次于共享内存(不需要复制)
  • 稳定性好,基于C/S架构。共享内存没有分层,并发同步访问临界资源时,还可能发生死锁。
  • 安全性高,传统IPC无法获得对方可靠的进程用户ID/进程ID,无法鉴别身份。Android为每个安装好的APP分配了自己的UID,通过进程的UID来鉴别进程身份。另外Android系统中的服务端会判断UID/PID是否满足访问权限,而对外只暴露客户端,加强了系统的安全性。

关于Binder,有几个重要操作:

驱动初始化

Binder驱动程序的初始化是在init进程中进行的,通过binder_init实现。

// kernel/goldfish/drivers/staging/android/binder.c
static int __init binder_init(void)
{
    ......
    // 创建设备名称
    device_names = kzalloc(strlen(binder_devices_param) + 1, GFP_KERNEL);
    strcpy(device_names, binder_devices_param);
    while ((device_name = strsep(&device_names, ","))) {
        // 1. 调用了 init_binder_device 初始化设备
        ret = init_binder_device(device_name);
        ......
    }
    // 声明了 Binder 驱动可使用的目录: /proc/binder
    binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
    // 2. 为当前进程创建了: /proc/binder/proc 目录
    // 每个使用了 Binder 进程通信机制的进程在该目录下都对应一个文件
    if (binder_debugfs_dir_entry_root) {
        binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",binder_debugfs_dir_entry_root);
    }
    // 3. 在 /proc/binder 目录下创建 state, stats, transactions, transaction_log, failed_transaction_log 这几个文件
    // 用于读取 Binder 驱动运行状况
    if (binder_debugfs_dir_entry_root) {
        debugfs_create_file("state",
                    S_IRUGO,
                    binder_debugfs_dir_entry_root,
                    NULL,
                    &binder_state_fops);
        ...
    }
    return ret;
}

static int __init init_binder_device(const char *name)
{
    int ret;
    struct binder_device *binder_device;
    struct binder_context *context;

    binder_device = kzalloc(sizeof(*binder_device), GFP_KERNEL);
    if (!binder_device)
        return -ENOMEM;

    binder_device->miscdev.fops = &binder_fops;
    binder_device->miscdev.minor = MISC_DYNAMIC_MINOR;
    binder_device->miscdev.name = name;
    ......
    // 调用了 misc_register 来注册一个 Binder 设备
    ret = misc_register(&binder_device->miscdev);
    ......
    return ret;
}

初始化主要完成:

  • init_binder_device 初始化Binder设备

    • 调用misc_register 注册一个Binder设备
  • 创建目录 /proc/binder/

    • 每一个使用了 Binder 进程间通信机制的进程, 在该目录下都对应一个文件, 文件以进程 ID 命名。通过他们就可以读取到各个进程的 Binder 线程池, Binder 实体对象, Binder 引用对象以及内核缓冲区等消息
    • 创建了 5个文件 state, stats, transactions, transaction_log, failed_transaction_log,可以读取到Binder驱动的运行状况

open系统调用

Binder设备文件的open会调用binder_open函数
binder_open主要流程:

  • 为当前进程创建binder_proc对象proc

  • 初始化binder_proc对象proc

  • 将proc加入全局维护的hash队列binder_procs中

    • 遍历这个队列,就知道当前有多少个进程正在使用binder进程间通信
  • 将这个proc写入打开的binder设备文件的结构体中

  • 在/proc/binder/proc中创建以该进程ID为名的只读文件

binder_proc与进程是一一对应的,内核只需要从binder_procs中找到当前进程对应的binder_proc,就可以进行Binder驱动相关操作了

binder_proc 结构
// kernel/goldfish/drivers/staging/android/binder.c
struct binder_proc {
    // 打开设备文件时, Binder 驱动会创建一个Binder_proc 保存在 hash 表中 
    // proc_node 描述 binder_proc 为其中的一个节点
    struct hlist_node proc_node;
    
    // Binder 对象缓存
    struct rb_root nodes;             // 保存 binder_proc 进程内的 Binder 实体对象
    struct rb_root refs_by_desc;      // 保存其他进程 Binder 引用对象, 以句柄作 key 值来组织
    struct rb_root refs_by_node;      // 保存其他进程 Binder 引用对象, 以地址作 key 值来组织
    
    // 描述线程池
    struct rb_root threads;           // 以线程的 id 作为关键字来组织一个进程的 Binder 线程池
    struct max_threads;               // 进程本身可以注册的最大线程数
    int ready_threads;                // 空闲的线程数
    
    // 内核缓冲区首地址
    struct vm_area_struct *vma;       // 用户空间地址
    void *buffer;                     // 内核空间地址
    ptrdiff_t user_buffer_offset;     // 用户空间地址与内核空间地址的差值
    
    // 内核缓冲区
    struct list_head buffers;         // 内核缓冲区链表
    struct rb_root free_buffers;      // 空闲缓冲区红黑树
    struct rb_root allocated_buffers; // 被使用的缓冲区红黑树
    
    // binder 请求待处理工作项
    struct list_head todo;
}

binder_proc中定义了Binder驱动的核心模块

  • Binder对象缓存

    • binder_node 实体对象
    • binder_ref 引用对象
  • 用户处理任务的binder线程池、最大线程数

  • binder缓冲区的首地址

  • 用于数据交互的binder内核缓冲区链表

  • 待处理的任务队列

binder 对象

参考:sharrychoo.github.io/blog/androi…
binder_node
描述一个Binder的实体对象,在Server端使用

binder_ref
描述一个Binder的引用实例,在Client端使用

// kernel/goldfish/drivers/staging/android/binder.c
struct binder_ref {
    int debug_id;
    // 当前 binder_ref 在 binder_proc 红黑树中的结点
    struct rb_node rb_node_desc;
    struct rb_node rb_node_node;
    
    // 当前 binder 引用对象, 对应的 binder_node 对象的结点
    struct hlist_node node_entry;
    
    // 指向这个 binder 引用对象的宿主进程
    struct binder_proc *proc;
    
    // 这个 binder 引用对象所引用的实体对象
    struct binder_node *node;
    
    // 用于描述这个 binder 引用对象的句柄值
    unit32_t desc;
    ......
}

binder_ref中保存了指向binder_node对象的指针,因此通过binder_ref找到其对应的binder_node 就可以进行跨进程数据的传输了

binder_thread

// kernel/goldfish/drivers/staging/android/binder.c
struct binder_thread {
    struct binder_proc *proc;  // 宿主进程
    struct rb_node rb_node;    // 当前线程在宿主进程线程池中的结点
    int pid;                   // 线程的 id
    // 描述要处理的事务
    struct binder_transaction * transaction_stack;
    strcut list_head todo;     // 要处理的 client 请求
    wait_queue_head_t wait;    // 当前线程依赖于其他线程处理后才能继续, 则将它添加到 wait 中
    struct binder_stats stats; // 当前线程接收到进程间通信的次数
}

binder_thread通过binder_transaction 来描述一个待处理的事务

// kernel/goldfish/drivers/staging/android/binder.c
struct binder_transaction {
    struct binder_thread from;// 发起这个事务的线程
    struct binder_thread *to_thread;// 事务负责处理的线程
    struct binder_proc *to_proc;// 事务负责处理的进程
    
    // 当前事务所依赖的另一个事务
    struct binder_transaction *from_parent;
    struct binder_transaction *to_parent;
}

binder_buffer
每个使用Binder机制的进程在Binder驱动程序中都有一个内核缓冲区列表,用来保存Binder驱动为它分配的内核缓存区

// kernel/goldfish/drivers/staging/android/binder.c
struct binder_buffer {
    // 当前缓冲区在 binder_proc->buffers 中的结点
    struct list_head entry;
    
    // 若内核缓冲区是空闲的, rb_node 就是 binder_proc 空闲的内核缓冲区红黑树的结点
    // 若内核缓冲区正在使用的, rb_node 就是 binder_proc 使用的内核缓冲区红黑树的结点
    struct rb_node rb_node;
    
    ......
    
    // 保存使用缓冲区的事务
    struct binder_transaction *transaction;
    
    // 保存使用缓冲区的 binder 实体对象
    struct binder_node* target_node;
    
    // 记录数据缓冲大小
    size_t data_size;
    
    // 偏移数组的大小, 这个偏移数组在数据缓冲区之后, 用来记录数据缓冲中 binder 对象的位置
    siez_t offset_size;
    
    // 描述真正的数据缓冲区
    uint8_t data[0];       
}

mmap系统调用

Binder 设备文件的mmap系统调用,binder_mmap

// drivers/staging/android/binder.c

// vm_area_struct 由 Linux 内核传入, 它表示一个进程的用户虚拟地址空间
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int ret;
    // 1. 声明一个 vm_struct 结构体 area, 它描述的是一个进程的内核态虚拟内存
    struct vm_struct *area;
    
    // 2. 从 binder 设备文件中获取 binder_proc, 当进程调用了 open 函数时, 便会创建一个 binder_proc
    struct binder_proc *proc = filp->private_data;
    struct binder_buffer *buffer;
    
    // 3. 判断要映射的用户虚拟地址空间范围是否超过了 4M
    if ((vma->vm_end - vma->vm_start) > SZ_4M)
        vma->vm_end = vma->vm_start + SZ_4M;// 若超过 4M, 则截断为 4M
    // 3.1 检查要映射的进程用户地址空间是否可写
    if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
        ret = -EPERM;
        failure_string = "bad vm_flags";
        goto err_bad_arg;// 若可写, 则映射失败
    }
    // 3.2 给 binder 用户地址的 flags 中添加一条不可复制的约定
    vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
    // 3.3 验证进程的 buffer 中, 是否已经指向一块内存区域了
    if (proc->buffer) {
        ret = -EBUSY;
        failure_string = "already mapped";
        goto err_already_mapped;// 若已存在内存区域, 则映射失败
    }
    
    // 4. 分配一块内核态的虚拟内存, 保存在 binder_proc 中
    area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);// 空间大小为用户地址空间的大小, 最大为 4M
    proc->buffer = area->addr;
    // 计算 binder 用户地址与内核地址的偏移量
    proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
    ......
    
    // 5. 创建物理页面结构体指针数组, PAGE_SIZE 一般定义为 4k
    proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
    ......
    
    // 6. 调用 binder_update_page_range 给进程的(用户/内核)虚拟地址空间分配物理页面
    if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
        .......
    }
    
    // 7. 物理页面分配成功之后, 将这个内核缓冲区添加到进程结构体 proc 的内核缓冲区列表总
    buffer = proc->buffer;
    INIT_LIST_HEAD(&proc->buffers);// 初始化链表头
    list_add(&buffer->entry, &proc->buffers);// 添加到 proc 的内核缓冲区列表中
    ......
    
    // 8. 将这个内核缓冲区添加到 proc 空闲内核缓冲区的红黑树 free_buffer 中
    binder_insert_free_buffer(proc, buffer);
    // 将该进程最大可用于执行异步事务的内核缓冲区大小设置为总大小的一半
    // 防止异步事务消耗过多内核缓冲区, 影响同步事务的执行
    proc->free_async_space = proc->buffer_size / 2;
    ......
    return 0;
    ......
}

binder_mmap为当前进程binder_proc初始化内核缓冲区,如下

  • 验证是否满足创建条件

    • binder_mmap只有第一次调用有意义,已经初始化过直接返回失败
    • 一个进程的内核缓冲区最大为4M
  • 创建内核虚拟地址空间

  • 物理页面的创建与映射

    • 将页面映射到用户虚拟地址空间和内核虚拟地址空间
  • 缓存缓冲区

    • 将初始化好的内核缓冲区 binder_buffer 保存到binder_proc中
物理页面的创建与映射

在binder_mmap中,调用了binder_update_page_range分配内核缓冲区

// drivers/staging/android/binder.c

/**
 *  // binder_mmap 调用代码
    if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
        .......
    }
 */
static int binder_update_page_range(
                    struct binder_proc *proc,   // 描述当前进程的 binder 驱动管理者
                    int allocate,
                    void *start, void *end,     // 内核地址的起始 和 结束地址
                    struct vm_area_struct *vma  // 与内核地址映射的用户地址
                    )
{
    void *page_addr;
    unsigned long user_page_addr;
    struct vm_struct tmp_area;
    struct page **page;
    ......
    // 若该参数为 0, 则说明为释放内存, 显然这里为 1
    if (allocate == 0)
        goto free_range;
    ......
    // 分配物理内存
    for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
        int ret;
        struct page **page_array_ptr;
        // 1. 从进程的物理页面结构体指针数组 pages 中, 获取一个与内核地址空间 page_addr~(page_addr + PAGE_SIZE)对应的物理页面指针
        page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
        // 2. 给结构体指针分配物理内存
        *page = alloc_page(GFP_KERNEL | __GFP_ZERO);
        ......
        // 3. 将物理内存地址映射给 Linux 内核地址空间
        tmp_area.addr = page_addr;
        tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */;
        page_array_ptr = page;
        ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr);
        // 4. 将物理内存地址映射给 用户地址空间
        user_page_addr =
            (uintptr_t)page_addr + proc->user_buffer_offset;
        ret = vm_insert_page(vma, user_page_addr, page[0]);
    }
    ......
    return 0;

free_range:
    // allocate 为 0 的情况, 释放物理内存的操作
}

Binder驱动为这块内核缓冲区,分配了一个物理页面(一般为4k),这样数据从用户空间传递到内核空间时,就不用在用户态和内核态之间相互拷贝,只需要拷贝到虚拟地址指向的物理内存,就可以进行数据的读取

缓存空闲缓存区
// drivers/staging/android/binder.c

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    ......
    // 将这个内核缓冲区添加到 proc 空闲内核缓冲区的红黑树 free_buffer 中
    binder_insert_free_buffer(proc, buffer);
    ......
}

static void binder_insert_free_buffer(struct binder_proc *proc,
                      struct binder_buffer *new_buffer)
{
    // 1. 获取进程中维护的空闲缓冲区列表
    struct rb_node **p = &proc->free_buffers.rb_node;
    struct rb_node *parent = NULL;
    struct binder_buffer *buffer;
    size_t buffer_size;
    size_t new_buffer_size;
    ......
    // 2. 计算新加入的内核缓冲区的大小
    new_buffer_size = binder_buffer_size(proc, new_buffer);
    ......
    // 3. 从红黑树中找寻合适的插入结点
    while (*p) {
        parent = *p;
        buffer = rb_entry(parent, struct binder_buffer, rb_node);
        buffer_size = binder_buffer_size(proc, buffer);
        if (new_buffer_size < buffer_size)
            p = &parent->rb_left;
        else
            p = &parent->rb_right;
    }
    // 4. 链入红黑树中
    rb_link_node(&new_buffer->rb_node, parent, p);
    rb_insert_color(&new_buffer->rb_node, &proc->free_buffers);
}

缓存内核缓冲区的操作,就是将这个binder_buffer 添加到binder_proc的红黑树中,方便后续使用,此时的空闲缓冲区是刚分配的。

还不太理解?
最大缓冲区为4MB,为什么只进行了一个物理页面的分配?

  • Binder驱动采用了按需分配的策略,当需要进行数据拷贝时,会从空闲缓存区中找寻缓冲区,进行物理页面的映射,按需分配会降低Binder驱动闲置时的内存消耗。

跨进程通信频繁时,4MB是否够用?
对于大数据跨进程通信不够,需要使用共享内存的方式进行跨进程通信,Binder驱动是为了解决小规模跨进程通讯而生的

跨进程共享内存 VS Binder驱动

  • 跨进程共享内存虽然可以进行大数据传输,但需要手动进行数据的读写的同步,即便使用Android封装好的Asheme共享内存,成本也比较高
  • 通过Binder驱动,可简化跨进程代码

ioctl系统调用

binder设备文件的ioctl系统调用是由binder_ioctl实现的

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret;
    struct binder_proc *proc = filp->private_data;
    struct binder_thread *thread;
    unsigned int size = _IOC_SIZE(cmd);
    void __user *ubuf = (void __user *)arg;
    
    ......
    
    ret = wait_event_interruptible(binder_user_error_wait, binder_stop_on_user_error < 2);
    if (ret)
        goto err_unlocked;

    binder_lock(__func__);
    
    // 2. 获取描述当前线程的 binder_thread 结构体
    thread = binder_get_thread(proc);
    ......
    // IOCTL 请求码
    switch (cmd) {
    // 2.1 数据读写请求
    case BINDER_WRITE_READ:
        ret = binder_ioctl_write_read(filp, cmd, arg, thread);
        if (ret)
            goto err;
        break;
    ......
    // 2.2 注册上下文的请求
    case BINDER_SET_CONTEXT_MGR:
        ret = binder_ioctl_set_ctx_mgr(filp);
        if (ret)
            goto err;
        ret = security_binder_set_context_mgr(proc->tsk);
        if (ret < 0)
            goto err;
        break;
    ......
err:
    if (thread)
        thread->looper &= ~BINDER_LOOPER_STATE_NEED_RETURN;
    binder_unlock(__func__);
    wait_event_interruptible(binder_user_error_wait, binder_stop_on_user_error < 2);
    ......
}

binder_ioctl主要负责处理用户空间传递的ioctl指令

  • BINDER_WRITE_READ : 数据的读写
  • BINDER_SET_CONTEXT_MGR : 注册Binder上下文的管理者

Binder驱动 总结

Binder驱动程序设备文件的几个基础操作

  • binder_init : 创建Binder驱动相关的文件

  • binder_open : 打开binder设备文件,为当前进程创建binder_proc,可以理解为binder驱动上下文

    • binder_proc 对象

      • binder_node : 实体对象(服务端)
      • binder_ref : 引用对象(客户端)
    • binder_thread : binder线程

    • binder_buffer : binder缓冲区

  • binder_mmap : 初始化当前进程第一个binder缓冲区binder_buffer,最大值为4MB

    • 只有首次调用有效(?)
    • 为缓冲区分配物理页面,将物理页映射到用户虚拟地址内核虚拟地址
    • 将缓冲区添加到binder_proc中缓存
  • binder_ioctl : 寻找空闲的线程,处理ioctl请求

    • BINDER_WRITE_READ : 数据的读写
    • BINDER_SET_CONTEXT_MGR : 注册Binder上下文的管理者(ServiceManager干的事)

一次完成的Binder IPC通信过程是怎样的?

  • Binder驱动在内核空间创建一个数据接收缓存区
  • 在内核空间开辟一块内核缓冲区,建立「内核缓冲区」和「数据接收缓冲区」之间的映射关系,以及内核中「数据接收缓冲区」和「接收进程用户空间地址」的映射关系。
  • 发送方进程通过系统调用copyFromUser()将数据copy到内核中的「内核缓冲区」,由于「内核缓冲区」和「接收进程的用户空间」存在内存映射,也就相当于把数据发送到了接收进程的用户空间,这样就完成了一次进程间通信过程。