Android Binder 机制初步学习 笔记(二)—— Binder 设备基本操作实现

来源:互联网 发布:林忆软件 编辑:程序博客网 时间:2024/06/07 07:40

  • NOTE
  • Binder 设备基本操作实现
    • Binder 设备初始化
      • 1 binder_init
      • 2 binder_fops
    • Binder 设备文件打开
      • 1 binder_open
    • Binder 设备文件的内存映射
      • 1 binder_mmap
      • 2 binder_update_page_range
      • 3 binder_buffer_size
      • 4 binder_insert_free_buffer
      • 5 内存映射示意图
    • Binder 内核缓冲区管理
      • 1 分配内核缓冲区
        • 11 binder_alloc_buf 1st
        • 12 binder_alloc_buf 2nd
        • 13 binder_alloc_buf 3rd
        • 14 binder_alloc_buf 4th
        • 15 binder_insert_allocated_buffer
      • 2 释放内核缓冲区
        • 21 binder_free_buf
        • 22 buffer_start_page buffer_end_page
        • 23 binder_delete_free_buffer
      • 3 查询内核缓冲区
        • 31 binder_buffer_lookup


NOTE

  • 源码版本:Android 7.1.2。
  • 内核版本:android-goldfish-3.4
  • 内核下载:git clone https://aosp.tuna.tsinghua.edu.cn/kernel/goldfish.git (清华镜像站)
  • 以下分析思路均来自老罗的《Android 系统源代码情景分析(修订版)》

Binder 设备基本操作实现

  • 从这里开始分析 binder.c 中关于 binder 设备操作的实现:
    • init:初始化。
    • open:打开设备。
    • mmap:内存映射。
    • ioctlI/O 管理。
  • 没有特别说明时,文件位置均为:
    • kernel/goldfish/drivers/staging/android/binder.c

1. Binder 设备初始化

1.1 binder_init( )

  1. 调用 create_singlethread_workqueue() 创建一个工作队列。
  2. 调用 debugfs_create_dir() 创建目录 binder 作为根目录
  3. 根目录下创建一个目录 proc
    • 每个使用了 Binder 机制的进程都在此有一个对应的文件。
    • 这些文件以进程 ID 命名。
    • 通过文件可读取到对应的 Binder 线程池、实体、引用对象以及内核缓冲区等信息。
  4. 调用 misc_register() 创建一个 Binder 设备。
  5. 在根目录下创建五个文件:
    • state
    • stats
    • transactions
    • transaction_log
    • failed_transaction_log
    • 通过它们读取驱动运行状况,如协议请求次数、日志记录信息等。
static int __init binder_init(void){        int ret;        binder_deferred_workqueue = create_singlethread_workqueue("binder");        if (!binder_deferred_workqueue)                return -ENOMEM;        binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);        if (binder_debugfs_dir_entry_root)                binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",                                                 binder_debugfs_dir_entry_root);        ret = misc_register(&binder_miscdev);        if (binder_debugfs_dir_entry_root) {                debugfs_create_file("state",                                    S_IRUGO,                                    binder_debugfs_dir_entry_root,                                    NULL,                                    &binder_state_fops);                debugfs_create_file("stats",                                    S_IRUGO,                                    binder_debugfs_dir_entry_root,                                    NULL,                                    &binder_stats_fops);                debugfs_create_file("transactions",                                    S_IRUGO,                                    binder_debugfs_dir_entry_root,                                    NULL,                                    &binder_transactions_fops);                debugfs_create_file("transaction_log",                                    S_IRUGO,                                    binder_debugfs_dir_entry_root,                                    &binder_transaction_log,                                    &binder_transaction_log_fops);                debugfs_create_file("failed_transaction_log",                                    S_IRUGO,                                    binder_debugfs_dir_entry_root,                                    &binder_transaction_log_failed,                                    &binder_transaction_log_fops);        }        return ret;}

1.2 binder_fops

  • 全局变量 binder_fops 指定了设备文件的操作方法列表:
    • 文件打开:binder_open()
    • 内存映射:binder_mmap()
    • IO 控制:binder_ioctl()
static const struct file_operations binder_fops = {        .owner = THIS_MODULE,        .poll = binder_poll,        .unlocked_ioctl = binder_ioctl,        .mmap = binder_mmap,        .open = binder_open,        .flush = binder_flush,        .release = binder_release,};static struct miscdevice binder_miscdev = {        .minor = MISC_DYNAMIC_MINOR,        .name = "binder",        .fops = &binder_fops};

2. Binder 设备文件打开

  • 进程使用 Binder 机制之前,要先打开设备文件以获得一个文件描述符
  • 通过文件描述符,进程可以与驱动交互,从而可使用 Binder 通信。

2.1 binder_open( )

  1. 创建一个 binder_proc 结构体 proc,并对它进行初始化:
    • 申请必要的内存空间kzalloc()
    • 初始化任务控制块proc->tsk = current
    • 初始化工作项队列INIT_LIST_HEAD(&proc->todo)
    • 初始化等待队列init_waitqueue_head(&proc->wait)
    • 设置默认优先级proc->default_priority = task_nice(current)
    • 创建统计信息binder_stats_created()
    • 将 proc 加入全局 hash 队列hlist_add_head(&proc->proc_node, &binder_procs)
    • 设置进程号proc->pid = current->group_leader->pid
    • 初始化死亡通知工作项队列INIT_LIST_HEAD(&proc->delivered_death)
  2. 将初始化成功的 proc 保存
    • filp 指向一个打开文件结构体。
    • proc 保存在其成员 private_data 中。
    • 进程调用 open 打开设备后,内核会返回一个文件描述符给进程,而这个描述符与 filp 指向的结构体是关联在一起的。
    • 进程以描述符为参数调用 mmapioctl 与驱动交互时,驱动可通过 private_data 获取 proc
  3. 读取调试相关的信息
    • 在目标设备上的 /proc/binder/proc 目录下创建以进程 ID 为名的只读文件。
    • 通过这个文件,我们可以获得相应进程的线程池、实体对象、引用对象、缓冲区等信息。
static HLIST_HEAD(binder_procs);static int binder_open(struct inode *nodp, struct file *filp){        struct binder_proc *proc;        binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n",                     current->group_leader->pid, current->pid);        proc = kzalloc(sizeof(*proc), GFP_KERNEL);        if (proc == NULL)                return -ENOMEM;        get_task_struct(current);        proc->tsk = current;        INIT_LIST_HEAD(&proc->todo);        init_waitqueue_head(&proc->wait);        proc->default_priority = task_nice(current);        binder_lock(__func__);        binder_stats_created(BINDER_STAT_PROC);        hlist_add_head(&proc->proc_node, &binder_procs);        proc->pid = current->group_leader->pid;        INIT_LIST_HEAD(&proc->delivered_death);        filp->private_data = proc;        binder_unlock(__func__);        if (binder_debugfs_dir_entry_proc) {                char strbuf[11];                snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);                proc->debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,                        binder_debugfs_dir_entry_proc, proc, &binder_proc_fops);        }        return 0;}

3. Binder 设备文件的内存映射

  • 打开设备文件后,需要调用 mmap 函数将这个设备文件映射到进程的地址空间,然后才可以使用 Binder 进程间通信机制。
  • 内存映射目的是给进程分配内核缓冲区,以便进行数据通信。

3.1 binder_mmap( )

  • 该函数比较长,根据参考书所述,可以分成两段来分析。
  • 第一段
    1. 参数 vma 指向结构体 vm_area_struct,用于描述一段虚拟地址空间。
    2. 变量 area 指向一个结构体 vm_struct,这也是用来描述虚拟地址空间的。
    3. 它们描述的虚拟地址是连续的,但对应的物理页面可以不连续。
    4. vm_area_structvm_struct 区别
      • 在 Linux 内核中,一个进程可占虚拟地址空间为 4G,其中 0G ~ 3G 为用户地址空间,剩下为内核地址空间。
      • vm_area_struct
        • 描述用户地址空间。
        • 空间范围 0G ~ 3G
      • vm_struct
        • 描述内核地址空间。
        • 空间范围 (3G + 896M + 8M)~ 4G
      • 注意到 4G 的空间中有一段空出来的地址:
        • 3G ~(3G + 896M):映射物理内存的前 896M,它们之间是简单线性关系。
        • (3G + 896M)~(3G + 896M + 8M):安全保护区,用于检测非法指针。
    5. filp 指向一个打开文件结构体
      • 成员变量 private_data 指向一个进程结构体 binder_proc
      • private_data 转为 proc,即获取了对应的进程。
    6. vma 的成员变量 vma_startvma_end 指定了要映射的用户地址空间范围
      • 若范围大于 4M,则将其截断为 4M(断尾)。
      • 从这可以知道,Binder 驱动最多可以为进程分配 4M 内核缓冲区
    7. 驱动分配的内核缓冲区在用户空间只读
      • #define FORBIDDEN_MMAP_FLAGS (VM_WRITE)
      • 若检查出指定要映射的用户地址空间可写,则返回。
    8. 驱动分配的内核缓冲区在用户空间不可拷贝
      • VM_DONTCOPY 位置 1,表示不可拷贝。
      • VM_MAYWRITE 位置 0,禁止设置可能会执行写操作标志位的操作。
    9. 检查 buffer 是否已经指向一块内核缓冲区,若是则出错返回。
    int ret;    struct vm_struct *area;    struct binder_proc *proc = filp->private_data;    const char *failure_string;    struct binder_buffer *buffer;    if ((vma->vm_end - vma->vm_start) > SZ_4M)            vma->vm_end = vma->vm_start + SZ_4M;    ......    ......    if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {            ret = -EPERM;            failure_string = "bad vm_flags";            goto err_bad_arg;    }    vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;    mutex_lock(&binder_mmap_lock);    if (proc->buffer) {            ret = -EBUSY;            failure_string = "already mapped";            goto err_already_mapped;    }
  • 第二段
    1. 在进程内核地址空间中分配大小为 vma->vm_end - vma ->vm_start 的空间。
    2. proc->buffer 保存空间起始地址,proc->buffer_size 保存大小。
    3. 指定该空间的打开和关闭函数为 binder_vma_openbinder_vma_close
    4. 计算缓冲区的用户空间地址与内核空间地址差值 proc->user_buffer_offset
    5. 调用 kzalloc() 创建一个物理页面结构体指针数组(每一页虚拟地址空间都对应有一个物理页面),并将数组地址存放在 proc->pages 中。
    6. 调用 binder_update_page_range() 为虚拟地址空间 area 分配物理页面。
    7. 用一个 binder_buffer 结构的 buffer 来描述 area
    8. buffer 加入内核缓冲区列表 proc->buffers 中。
    9. 由于 area 对应物理页面空闲,调用 binder_insert_free_buffer 将其加入空闲内核缓冲区红黑树 proc->free_buffers 中。
    10. 将进程最大可用异步事务缓冲区大小设置为 proc->buffer_size / 2。(防止异步事务消耗过多内核缓冲区,影响同步事务)
    area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);    if (area == NULL) {            ret = -ENOMEM;            failure_string = "get_vm_area";            goto err_get_vm_area_failed;    }    proc->buffer = area->addr;    proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;    mutex_unlock(&binder_mmap_lock);    ......    ......    proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);    if (proc->pages == NULL) {            ret = -ENOMEM;            failure_string = "alloc page array";            goto err_alloc_pages_failed;    }    proc->buffer_size = vma->vm_end - vma->vm_start;    vma->vm_ops = &binder_vm_ops;    vma->vm_private_data = proc;    if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {            ret = -ENOMEM;            failure_string = "alloc small buf";            goto err_alloc_small_buf_failed;    }    buffer = proc->buffer;    INIT_LIST_HEAD(&proc->buffers);    list_add(&buffer->entry, &proc->buffers);    buffer->free = 1;    binder_insert_free_buffer(proc, buffer);    proc->free_async_space = proc->buffer_size / 2;    barrier();    proc->files = get_files_struct(proc->tsk);    proc->vma = vma;    proc->vma_vm_mm = vma->vm_mm;    /*printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p\n",             proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/    return 0;    ......    ......

3.2 binder_update_page_range( )

  • 这个函数的作用,是为一段指定的虚拟地址空间分配或释放物理页面
  • 参数解释:
    1. proc:描述了目标进程。
    2. allocate:值为 0 则释放物理界面,否则分配物理页面。
    3. start && end:指定操作的内核地址空间的开始、结束地址。
    4. vma:指向要映射的用户地址空间。
  • 实现逻辑
    1. 判断 vma 是否指向一个空地址
      • 空,则从 proc 中获取 vma。并判断目标进程的 vma_mm 与当前变量 mm 是否匹配,若不匹配则会报错,并把 vma 置空。
      • 非空,则将变量 mm 置为空,不需要用目标进程中获取对应的 vma
    2. 通过 allocate 值判断目前走的是分配还是释放操作,若为释放操作,则直接跳到 free_range 的部分。
    3. 若为分配操作,则进入第一个 for 循环
      • 内核地址 start ~ end 可能包含多个页面,因此需要循环操作依次为每个虚拟地址页面分配一个物理页面。
      • 首先从目标进程 proc 的物理页面指针数组中,获得一个与 page_addr ~(page_addr + PAGE_SIZE)对应的物理页面指针。
      • 调用 alloc_page() 分配物理页面。
      • 分配成功后,对 tmp_area 的相关操作对应着内核地址的映射。注意,给它分配的大小是两倍 PAGE_SIZE,这是因为 Linux 内核规定,这个范围内的内核地址都必须在后保留一块空地址空间作为安全保护区,以检测非法指针。
      • user_page_addr 的操作则对应着用户地址空间的映射。
    4. 若为释放操作,则进入 free_range 中的 for 循环
      • 同样,在地址区间内可能存在多个页面,因此需要循环。
      • 首先获得对应的页面指针。
      • 调用 zap_page_range() 解除该物理页面在用户地址的映射。
      • 调用 unmap_kernel_range() 解除在内核地址的映射。
      • 最后调用 __free_page() 释放本物理页面,并把页面指针置空。
static int binder_update_page_range(struct binder_proc *proc,                                     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;        struct mm_struct *mm;        ......        ......        if (vma)                mm = NULL;        else                mm = get_task_mm(proc->tsk);        if (mm) {                down_write(&mm->mmap_sem);                vma = proc->vma;                if (vma && mm != proc->vma_vm_mm) {                        pr_err("binder: %d: vma mm and task mm mismatch\n",                                proc->pid);                        vma = NULL;                }        }        if (allocate == 0)                goto free_range;        ......        for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {                int ret;                struct page **page_array_ptr;                page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];                BUG_ON(*page);                *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);                ......                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);                ......                user_page_addr =                        (uintptr_t)page_addr + proc->user_buffer_offset;                ret = vm_insert_page(vma, user_page_addr, page[0]);                ......        }        if (mm) {                up_write(&mm->mmap_sem);                mmput(mm);        }        return 0;free_range:        for (page_addr = end - PAGE_SIZE; page_addr >= start;             page_addr -= PAGE_SIZE) {                page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];                if (vma)                        zap_page_range(vma, (uintptr_t)page_addr +                                proc->user_buffer_offset, PAGE_SIZE, NULL);err_vm_insert_page_failed:                unmap_kernel_range((unsigned long)page_addr, PAGE_SIZE);err_map_kernel_failed:                __free_page(*page);                *page = NULL;err_alloc_page_failed:                ;        }err_no_vma:        if (mm) {                up_write(&mm->mmap_sem);                mmput(mm);        }        return -ENOMEM;}

3.3 binder_buffer_size( )

  • 该函数作用是,计算相应内核缓冲区的大小
  • 结构 binder_buffer 所描述的内核缓冲区由两块数据:
    • 元数据块,描述缓冲区本身。
    • 有效数据块,保存真正的事务数据。
    • 内核缓冲区大小指的是有效数据块的大小
  • 实现逻辑
    1. 若该缓冲区是缓冲区列表 buffers 中最后一个元素
      • 有效数据块从成员变量 data 开始,一直到驱动为进程分配的连续内核地址的末尾。
      • 先计算连续内核地址的末尾地址,再减去 data 地址,则可得到大小。
      • 数据块信息如图 4 所示。
    2. 若非最后一个元素
      • 当前缓冲区大小等于下一个缓冲区起始位置减去当前缓冲区 data 地址。
      • 数据块信息如图 5 所示。

@图 4. 位于末尾的内核缓冲区内存布局
图 4. 位于末尾的内核缓冲区内存布局

@图 5. 位于头部或中间的内核缓冲区内存布局
图 5. 位于头部或中间的内核缓冲区内存布局

static size_t binder_buffer_size(struct binder_proc *proc,                                 struct binder_buffer *buffer){        if (list_is_last(&buffer->entry, &proc->buffers))                return proc->buffer + proc->buffer_size - (void *)buffer->data;        else                return (size_t)list_entry(buffer->entry.next,                        struct binder_buffer, entry) - (size_t)buffer->data;}

3.4 binder_insert_free_buffer( )

  • 该函数作用是,将一个空闲内核缓冲区加入到进程的空闲缓冲红黑树中。
  • 实现逻辑:
    1. 调用 binder_buffer_size() 计算传入的缓冲区大小。
    2. 进入 while 循环:
      • 在红黑树 proc->free_buffers 中找到 new_buffer 的合适位置。
    3. 调用 rb_link_node() 连接红黑树节点。
    4. 调用 rb_insert_color() 将 new_buffer 保存到对应位置。
static void binder_insert_free_buffer(struct binder_proc *proc,                                      struct binder_buffer *new_buffer){        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;        BUG_ON(!new_buffer->free);        new_buffer_size = binder_buffer_size(proc, new_buffer);        binder_debug(BINDER_DEBUG_BUFFER_ALLOC,                     "binder: %d: add free buffer, size %zd, "                     "at %p\n", proc->pid, new_buffer_size, new_buffer);        while (*p) {                parent = *p;                buffer = rb_entry(parent, struct binder_buffer, rb_node);                BUG_ON(!buffer->free);                buffer_size = binder_buffer_size(proc, buffer);                if (new_buffer_size < buffer_size)                        p = &parent->rb_left;                else                        p = &parent->rb_right;        }        rb_link_node(&new_buffer->rb_node, parent, p);        rb_insert_color(&new_buffer->rb_node, &proc->free_buffers);}

3.5 内存映射示意图

  • Binder 驱动为进程分配的内核缓冲区有两个地址:
    • 用户空间地址。
    • 内核空间地址。
    • 它们之间有简单的线性关系,如图 6 所示。
  • Binder 驱动需要将一块数据传输给进程时,它可以将数据保存在为进程所分配的一块内核缓冲区中,然后再把这块缓冲区的用户空间地址告诉进程,则进程就可以访问到相应的数据。
  • 如此,数据就不需要用内核空间拷贝到用户空间,从而提高数据传输效率。

@图 6. Binder 设备文件内存映射示意图
图 6. Binder 设备文件内存映射示意图

4. Binder 内核缓冲区管理

  • 开始时,Binder 驱动只为进程分配一个页面的物理内存。
  • 随着进程的需要,可以分配更多内存,但最多只能分配 4M
  • 驱动为进程维护一个内核缓冲区池:
    • 由于物理内存分配以页面为单位,而进程使用内存时不是以页面为单位。
    • 池中每块内存使用 binder_buffer 结构描述。
    • 将内存块保存在一个列表中。
  • 对于已分配的,和空闲的内存块,分别保存在两个红黑树中。
  • 对于这些缓冲区,需要有一定的管理机制(分配、释放、查询)。

4.1 分配内核缓冲区

  • 进程使用命令协议 BC_TRANSACTIONBC_REPLY 传递数据时:
    • 驱动需要将这些数据从用户空间拷贝到内核空间,再传递给目标进程。
    • 此时驱动需要在目标进程的内存池中分出一小块内核空间保存这些数据。

4.1.1 binder_alloc_buf( ) —— 1st

  • 该函数实现了缓冲区的分配操作。
  • 由于对应的代码比较长,分成几个小段来分析。
  • 参数含义:
    1. proc:目标进程。
    2. data_size:数据缓冲区大小。
    3. offset_size:偏移数组缓冲区大小。
    4. is_async:区别同步事务与异步事务。
  • 实现逻辑 —— 第一段:
    1. ALIGN()data_sizeoffset_size 对其到一个 void 指针大小边界。
    2. 将对其后的数据相加,得到要分配的内核缓冲区大小 size
    3. 检查 size 值:
      • 溢出,说明请求分配的缓冲区太大,出错退出函数。
      • 无溢出,继续下一步。
    4. 检查是否用于异步事务:
      • 若是,则检查请求分配的空间是否大于目标进程剩余可用于异步事务的空间,如果大于则出错退出。
      • 若否,则继续下一步。
static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,                                              size_t data_size,                                              size_t offsets_size, int is_async){        struct rb_node *n = proc->free_buffers.rb_node;        struct binder_buffer *buffer;        size_t buffer_size;        struct rb_node *best_fit = NULL;        void *has_page_addr;        void *end_page_addr;        size_t size;        ......        ......        size = ALIGN(data_size, sizeof(void *)) +                ALIGN(offsets_size, sizeof(void *));        if (size < data_size || size < offsets_size) {                binder_user_error("binder: %d: got transaction with invalid "                        "size %zd-%zd\n", proc->pid, data_size, offsets_size);                return NULL;        }        if (is_async &&            proc->free_async_space < size + sizeof(struct binder_buffer)) {                binder_debug(BINDER_DEBUG_BUFFER_ALLOC,                             "binder: %d: binder_alloc_buf size %zd"                             "failed, no async space left\n", proc->pid, size);                return NULL;        }

4.1.2 binder_alloc_buf( ) —— 2nd

  • 实现逻辑 —— 第二段:
    1. 进入 while 循环体:
      • 采用最佳适配算法在目标进程的空闲缓冲区红黑树中检查有没有最合适的内核缓冲区可用。
      • 若有,则保存到 best_fit 中。
      • 若无,best_fitNULL
    2. 若没有可用缓冲区,则出错返回。
    3. 若有可用缓冲区,而这个缓冲区大小并不是刚好合适,而是更大一些。此时 n == NULL 成立,则计算该 buffer 的大小,保存在 buffer_size 中。
    while (n) {            buffer = rb_entry(n, struct binder_buffer, rb_node);            BUG_ON(!buffer->free);            buffer_size = binder_buffer_size(proc, buffer);            if (size < buffer_size) {                    best_fit = n;                    n = n->rb_left;            } else if (size > buffer_size)                    n = n->rb_right;            else {                    best_fit = n;                    break;            }    }    if (best_fit == NULL) {            printk(KERN_ERR "binder: %d: binder_alloc_buf size %zd failed, "                   "no address space\n", proc->pid, size);            return NULL;    }    if (n == NULL) {            buffer = rb_entry(best_fit, struct binder_buffer, rb_node);            buffer_size = binder_buffer_size(proc, buffer);    }

4.1.3 binder_alloc_buf( ) —— 3rd

  • 这一段中需要用到的几个宏定义:
    • 位置:kernel/goldfish/arch/arm/include/asm/page.h
/* PAGE_SHIFT determines the page size */#define PAGE_SHIFT              12#define PAGE_SIZE               (_AC(1,UL) << PAGE_SHIFT)#define PAGE_MASK               (~(PAGE_SIZE-1))
  • 这一段中需要用到的几个宏定义:
    • 位置:kernel/goldfish/include/linux/mm.h
/* to align the pointer to the (next) page boundary */#define PAGE_ALIGN(addr) ALIGN(addr, PAGE_SIZE)
  • 实现逻辑 —— 第三段:
    1. 使用宏 PAGE_MASK 计算空闲缓冲区 buffer 的结束地址所在页面的起始地址,保存在 has_page_addr 中。
    2. 若在第二段中提到的情况,n == NULL 成立,此时需要将获得的空闲缓冲区进行裁剪:
      • 其中一块用来分配。
      • 另一块继续留在目标进程的空闲缓冲区红黑树中。
      • 注意,若裁剪后的第二块缓冲区小于等于 4 字节时,就不进行裁剪。
      • 最终需要分配的内核缓冲区大小保存在 buffer_size 中。
    3. 使用宏 PAGE_ALIGN 将缓冲区结束地址对其到页面边界,保存在 end_page_addr 中。
    4. 若对齐后的结束地址大于页面起始地址,则将其修正为 has_page_addr
    5. 调用 binder_update_page_range() 分配物理页面。
    has_page_addr =            (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK);    if (n == NULL) {            if (size + sizeof(struct binder_buffer) + 4 >= buffer_size)                    buffer_size = size; /* no room for other buffers */            else                    buffer_size = size + sizeof(struct binder_buffer);    }    end_page_addr =            (void *)PAGE_ALIGN((uintptr_t)buffer->data + buffer_size);    if (end_page_addr > has_page_addr)            end_page_addr = has_page_addr;    if (binder_update_page_range(proc, 1,        (void *)PAGE_ALIGN((uintptr_t)buffer->data), end_page_addr, NULL))            return NULL;
  • 在这部分实现中,关于 end_page_addrhas_page_addr 的关系有三种情况。
  • 第一种情况如图 7 所示:
    • 空闲内核缓冲区 buffer 的结束地址刚好对齐到页面边界。
    • 此时 end_page_addr 必定小于等于 has_page_addr

@图 7. 情况一,buffer结束地址对齐到页面边界
图 7. 情况一,buffer结束地址对齐到页面边界

  • 第二种情况如图 8 所示:
    • buffer 的结束地址没有对齐到页面边界,并且大于要分配的缓冲区的结束地址。
    • 此时 end_page_addr 依然小于 has_page_addr

@图 8. 情况二,buffer结束地址未对齐到页面边界,且大于要分配的缓冲区结束地址
图 8. 情况二,buffer结束地址未对齐到页面边界,且大于要分配的缓冲区结束地址

  • 第三中情况如图 9 所示:
    • buffer 结束地址没有对齐到页面边界,并且小于要分配的缓冲区的结束地址。
    • 此时 end_page_addr 大于 has_page_addr,需要修正。

@图 9. 情况三,buffer结束地址未对齐到页面边界,且小于要分配的缓冲区结束地址
图 9. 情况三,buffer结束地址未对齐到页面边界,且小于要分配的缓冲区结束地址

4.1.4 binder_alloc_buf( ) —— 4th

  • 实现逻辑 —— 第四段:
    1. 调用 rb_erase() 将空闲缓冲区从目标进程的相应红黑树中删除。
    2. 调用 binder_insert_allocated_buffer() 将分配的缓冲区加入到目标进程相应的红黑树中。
    3. 检查是否有多余的缓冲区,若有,则需要将其封装成一个新的空闲缓冲区 new_buffer,并添加到目标进程的缓冲区列表,以及空闲红黑树中。
    4. 最后对新分配的缓冲区 buffer 进行一些初始化操作:
      • 设置数据缓冲区大小。
      • 设置偏移数组缓冲区大小。
      • 标记是否应用于异步事务。
      • 若用于异步事务,则要减少目标进程中可用于异步事务的内核缓冲区的大小。
    5. buffer 返回给调用者。
    rb_erase(best_fit, &proc->free_buffers);    buffer->free = 0;    binder_insert_allocated_buffer(proc, buffer);    if (buffer_size != size) {            struct binder_buffer *new_buffer = (void *)buffer->data + size;            list_add(&new_buffer->entry, &buffer->entry);            new_buffer->free = 1;            binder_insert_free_buffer(proc, new_buffer);    }    binder_debug(BINDER_DEBUG_BUFFER_ALLOC,                 "binder: %d: binder_alloc_buf size %zd got "                 "%p\n", proc->pid, size, buffer);    buffer->data_size = data_size;    buffer->offsets_size = offsets_size;    buffer->async_transaction = is_async;    if (is_async) {            proc->free_async_space -= size + sizeof(struct binder_buffer);            binder_debug(BINDER_DEBUG_BUFFER_ALLOC_ASYNC,                         "binder: %d: binder_alloc_buf size %zd "                         "async free %zd\n", proc->pid, size,                         proc->free_async_space);    }    return buffer;

4.1.5 binder_insert_allocated_buffer( )

  • 实现逻辑:
    1. 通过 while 循环中的逻辑在已分配页面缓冲区的红黑树 proc->allocated_buffers 中找寻一个合适的位置 p
    2. 找到位置后,将当前已分配页面的缓冲区 new_buffer 加入到红黑树的相应位置。
static void binder_insert_allocated_buffer(struct binder_proc *proc,                                           struct binder_buffer *new_buffer){        struct rb_node **p = &proc->allocated_buffers.rb_node;        struct rb_node *parent = NULL;        struct binder_buffer *buffer;        BUG_ON(new_buffer->free);        while (*p) {                parent = *p;                buffer = rb_entry(parent, struct binder_buffer, rb_node);                BUG_ON(buffer->free);                if (new_buffer < buffer)                        p = &parent->rb_left;                else if (new_buffer > buffer)                        p = &parent->rb_right;                else                        BUG();        }        rb_link_node(&new_buffer->rb_node, parent, p);        rb_insert_color(&new_buffer->rb_node, &proc->allocated_buffers);}

4.2 释放内核缓冲区

  • 当进程处理完驱动给它发送的返回协议 BR_TRANSACTIONBR_REPLY 之后,它就会使用命令协议 BC_FREE_BUFFER 通知驱动释放缓冲区。

4.2.1 binder_free_buf( )

  • 该函数实现了释放内核缓冲区的操作
  • 实现逻辑
    1. 通过 binder_buffer_size() 计算缓冲区大小。
    2. 通过 ALIGN() 将数据缓冲区大小,偏移数组缓冲区大小分别对齐后相加,保存在 size 中。
    3. 检查要释放的空间是否用于异步事务,若是,则将其占用的大小增加到 proc->free_async_space 中。
    4. 调用 binder_update_page_range() 释放 buffer 用于保存数据的那一部分地址所占用的物理页面。
    5. 调用 rb_erase()buffer 从对应的红黑树 proc->allocated_buffers 中删除。
    6. buffer 不是缓冲区列表 proc->buffers 的最后一个元素,且它前后缓冲区也是空闲的,则此时需要将它们合并成一个大的连续的空闲缓冲区。
    7. 合并的方法是将后一个空闲缓冲区删掉,然后将它占用的地址空间追加到前一个空闲缓冲区中。(如图 10 所示)

@图 10. 合并两个连续的空闲内核缓冲区
图 10. 合并两个连续的空闲内核缓冲区

static void binder_free_buf(struct binder_proc *proc,                            struct binder_buffer *buffer){        size_t size, buffer_size;        buffer_size = binder_buffer_size(proc, buffer);        size = ALIGN(buffer->data_size, sizeof(void *)) +                ALIGN(buffer->offsets_size, sizeof(void *));        ......        ......        if (buffer->async_transaction) {                proc->free_async_space += size + sizeof(struct binder_buffer);                ......                ......        }        binder_update_page_range(proc, 0,                (void *)PAGE_ALIGN((uintptr_t)buffer->data),                (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK),                NULL);        rb_erase(&buffer->rb_node, &proc->allocated_buffers);        buffer->free = 1;        if (!list_is_last(&buffer->entry, &proc->buffers)) {                struct binder_buffer *next = list_entry(buffer->entry.next,                                                struct binder_buffer, entry);                if (next->free) {                        rb_erase(&next->rb_node, &proc->free_buffers);                        binder_delete_free_buffer(proc, next);                }        }        if (proc->buffers.next != &buffer->entry) {                struct binder_buffer *prev = list_entry(buffer->entry.prev,                                                struct binder_buffer, entry);                if (prev->free) {                        binder_delete_free_buffer(proc, buffer);                        rb_erase(&prev->rb_node, &proc->free_buffers);                        buffer = prev;                }        }        binder_insert_free_buffer(proc, buffer);}

4.2.2 buffer_start_page( ) && buffer_end_page( )

  • Binder 驱动以页面大小为单位来分配物理页面,因此在删除空闲缓冲区时,首先需要找到用于描述它的结构体 binder_buffer 所在的虚拟页面的地址。
  • 通过函数 buffer_start_page()buffer_end_page() 就可计算出所需的地址。
  • binder_buffer 结构体所在的虚拟地址有两种情况:
    1. 横跨两个虚拟地址页面,此时第一个函数返回第一个页面地址,另一个函数返回第二个页面地址。
    2. 在同一虚拟地址页面,此时两个函数返回的是同一个页面地址。
  • 只分析横跨的情况:
    1. buffer_start_page():将结构体 binder_buffer 的地址与宏 PAGE_MASK 进行按位与操作,即可得到第一个虚拟页面地址。
    2. buffer_end_page():将 binder_buffer 结构体指针加 1,相当于将它前移一个结构体大小,即得到这个结构体的末尾地址。用末尾地址与宏 PAGE_MASK 进行按位与操作,则得到第二个虚拟页面地址。
    3. 如图 11 所示,一个计算实例。

@图 11. 计算binder_buffer横跨时的两个虚拟地址页面地址
图 11. 计算binder_buffer横跨时的两个虚拟地址页面地址

static void *buffer_start_page(struct binder_buffer *buffer){        return (void *)((uintptr_t)buffer & PAGE_MASK);}static void *buffer_end_page(struct binder_buffer *buffer){        return (void *)(((uintptr_t)(buffer + 1) - 1) & PAGE_MASK);}

4.2.3 binder_delete_free_buffer( )

  • 该函数作为是删除对应的 binder_buffer 结构体
  • 调用该函数删除缓冲区 buffer 时:
    • 要保证 buffer 指向的不是目标进程的第一个缓冲区。
    • 该缓冲区及它前面一个缓冲区都必须是空闲的。
  • 假设 binder_buffer 结构体 bufferprevnext 都是横跨两个虚拟页面的。
  • 这个函数的实现逻辑输出来有点绕口,一下子虚拟页面,一下又变成物理页面。总觉得这部分直接看代码更好理解。
  • 实现逻辑
    1. prevnext 初始化为 NULL,它们分别表示 buffer 的前一个和后一个缓冲区。
    2. free_page_endfree_page_start 初始化为 1,这表示需要释放 buffer 所横跨的第一个虚拟页面地址与第二个虚拟页面地址所对应的物理页面。
    3. 检查 bufferprev 的关系
      • prev第二个虚拟地址页面buffer第一个是同一个页面,则将 free_page_start 设置为 0,此时表示 buffer 所在的第一个虚拟页面对应的物理页面不可释放。
      • 在上述情况下,若 prev第二个虚拟地址页面buffer第二个也是同一页面,则将 free_page_end 设置为 0,此时 buffer 所在的第二个虚拟页面对应的物理页面也不可释放。这种情况如图 12 所示。
    4. 检查 buffernext 的关系
      • next第一个虚拟页面buffer第二个是同一个页面,则将 free_page_end 设置为 0,即不释放 buffer 的第二个虚拟页面对应的物理页面。
      • 在上述情况下,若 next第二个虚拟页面buffer第一个是同一个页面,则将 free_page_start 设置为 0,即也不释放 buffer 的第一个虚拟页面对应的物理页面。这种情况如图 13 所示。
    5. 通过上述检查,调整好 free_page_startfree_page_end 的值后,就根据这两个值进行相应的释放操作。两个变量的取值与对应的释放操作如图 14 中的表格所示。

@图 12. 待删除的buffer是与prev位于同一虚拟地址页面
图 12. 待删除的buffer是与prev位于同一虚拟地址页面

@图 13. 待删除的buffer是与next位于同一虚拟地址页面
图 13. 待删除的buffer是与next位于同一虚拟地址页面

@图 14. free_page_end与free_page_start的值与物理页面释放的对应关系
图 14. free_page_end与free_page_start的值与物理页面释放的对应关系

static void binder_delete_free_buffer(struct binder_proc *proc,                                      struct binder_buffer *buffer){        struct binder_buffer *prev, *next = NULL;        int free_page_end = 1;        int free_page_start = 1;        BUG_ON(proc->buffers.next == &buffer->entry);        prev = list_entry(buffer->entry.prev, struct binder_buffer, entry);        BUG_ON(!prev->free);        if (buffer_end_page(prev) == buffer_start_page(buffer)) {                free_page_start = 0;                if (buffer_end_page(prev) == buffer_end_page(buffer))                        free_page_end = 0;                ......        }        if (!list_is_last(&buffer->entry, &proc->buffers)) {                next = list_entry(buffer->entry.next,                                  struct binder_buffer, entry);                if (buffer_start_page(next) == buffer_end_page(buffer)) {                        free_page_end = 0;                        if (buffer_start_page(next) ==                            buffer_start_page(buffer))                                free_page_start = 0;                        ......                }        }        list_del(&buffer->entry);        if (free_page_start || free_page_end) {                ......                binder_update_page_range(proc, 0, free_page_start ?                        buffer_start_page(buffer) : buffer_end_page(buffer),                        (free_page_end ? buffer_end_page(buffer) :                        buffer_start_page(buffer)) + PAGE_SIZE, NULL);        }}

4.3 查询内核缓冲区

  • 进程使用完一个内核缓冲区后:
    • 主动使用命令协议 BC_FREE_BUFFER 通知驱动释放缓冲区对应的物理页面。
    • 进程只知道缓冲区的用户空间地址。
    • 驱动需要知道用于描述该缓冲区的 binder_buffer 结构体,才可以进行释放。
    • 为了找到这个结构体,驱动提供了一个查询函数。

4.3.1 binder_buffer_lookup( )

  • 该函数作用是根据一个用户空间地址来查询一个内核缓冲区
  • 实现逻辑
    1. kern_ptr 的计算
      • user_ptr 是用户空间地址,用它减去 offsetof(struct binder_buffer, data) 就能得到一个 binder_buffer 结构体的用户空间地址。
      • 接着再减去 proc->user_buffer_offset 即可得到我们所需的内核空间地址。
    2. 计算出的内核空间地址不一定指向一个有效内核缓冲区
      • 对于进程已分配物理页面的缓冲区,以其内核空间地址为关键字保存在红黑树 proc->allocated_buffers 中。
      • 若可在红黑树中找到对应节点,则说明计算得到的 binder_buffer 结构体是指向一个有效内核缓冲区的。
    3. while 循环体中的逻辑即是寻找这个对应的节点
      • 若能找到,则将该节点对应的 binder_buffer 返回给调用者。
      • 若不能找到,则返回 NULL,表示未找到相应缓冲区。
static struct binder_buffer *binder_buffer_lookup(struct binder_proc *proc,                                                  void __user *user_ptr){        struct rb_node *n = proc->allocated_buffers.rb_node;        struct binder_buffer *buffer;        struct binder_buffer *kern_ptr;        kern_ptr = user_ptr - proc->user_buffer_offset                - offsetof(struct binder_buffer, data);        while (n) {                buffer = rb_entry(n, struct binder_buffer, rb_node);                BUG_ON(buffer->free);                if (kern_ptr < buffer)                        n = n->rb_left;                else if (kern_ptr > buffer)                        n = n->rb_right;                else                        return buffer;        }        return NULL;}
原创粉丝点击