Binder - 6、Binder中的一次拷贝

894 阅读6分钟

一、前言

众所周知,Binder之所以高效,是因为它只发生了一次内存拷贝,那么它的“一次拷贝”到底是怎么实现的呢?

我们在之前在分析binder_transaction的时候,提到了一个方法,这个方法是一次拷贝的核心,我们在这里来仔细分析一下

二、源码分析

2.1 入口-binder_transaction

Kernel\drivers\android\binder.c

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply,
                   binder_size_t extra_buffers_size)
{
    //申请内存
    t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size,
        tr->offsets_size, extra_buffers_size,
        !reply && (t->flags & TF_ONE_WAY), current->tgid);
    //拷贝数据
    if (binder_alloc_copy_user_to_buffer(
                &target_proc->alloc,
                t->buffer, 0,
                (const void __user *)
                    (uintptr_t)tr->data.ptr.buffer,
                tr->data_size)) {
        //error
    }
}

在这个方法中,有两个函数对拷贝数据非常重要,一个是binder_alloc_new_buf,另一个是binder_alloc_copy_user_to_bufferbinder_alloc_new_buf是用来申请内存用的,而binder_alloc_copy_user_to_buffer是执行具体的数据拷贝。

可以看到这里两个方法传递的参数都是target_proc的,代表我们此时在操纵接收端的内存。

我们分开来看一下,先看下binder_alloc_new_buf

2.2 buffer申请-binder_alloc_new_buf

struct binder_buffer *binder_alloc_new_buf(struct binder_alloc *alloc,
                       size_t data_size,
                       size_t offsets_size,
                       size_t extra_buffers_size,
                       int is_async,
                       int pid)
{
    struct binder_buffer *buffer;
    mutex_lock(&alloc->mutex);
    buffer = binder_alloc_new_buf_locked(alloc, data_size, offsets_size,
                         extra_buffers_size, is_async, pid);
    mutex_unlock(&alloc->mutex);
    return buffer;
}

直接调用了binder_alloc_new_buf_locked方法:

static struct binder_buffer *binder_alloc_new_buf_locked(
                struct binder_alloc *alloc,
                size_t data_size,
                size_t offsets_size,
                size_t extra_buffers_size,
                int is_async,
                int pid)
{
    struct rb_node *n = alloc->free_buffers.rb_node;
    struct binder_buffer *buffer;
    size_t buffer_size;
    struct rb_node *best_fit = NULL;
    void __user *has_page_addr;
    void __user *end_page_addr;
    size_t size, data_offsets_size;
    int ret;
    //数据对齐
    data_offsets_size = ALIGN(data_size, sizeof(void *)) +
        ALIGN(offsets_size, sizeof(void *));
    size = data_offsets_size + ALIGN(extra_buffers_size, sizeof(void *));
    /* Pad 0-size buffers so they get assigned unique addresses */
    size = max(size, sizeof(void *));
    //寻找有没有空闲的buffer
    while (n) {
        buffer = rb_entry(n, struct binder_buffer, rb_node);
        buffer_size = binder_alloc_buffer_size(alloc, 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;
        }
    }
    //包含该数据的页面
    has_page_addr = (void __user *)
        (((uintptr_t)buffer->user_data + buffer_size) & PAGE_MASK);
    //user_data结尾对应的页面
    end_page_addr =
        (void __user *)PAGE_ALIGN((uintptr_t)buffer->user_data + size);
    if (end_page_addr > has_page_addr)
        end_page_addr = has_page_addr;
    ret = binder_update_page_range(alloc, 1, (void __user *)
        PAGE_ALIGN((uintptr_t)buffer->user_data), end_page_addr);
    buffer->free = 0;
    buffer->allow_user_free = 0;
    //插入allocated_buffers红黑树中
    binder_insert_allocated_buffer_locked(alloc, buffer);
    buffer->data_size = data_size;
    buffer->offsets_size = offsets_size;
    return buffer;
}

这个方法还是很长的,总体来说,干了这么几件事:

  • 1、数据对齐
  • 2、寻找binder_alloc中有没有空闲的节点,如果有并且足够大,那么就可以用这个
  • 3、计算我们内存对应的页面
  • 4、执行binder_update_page_range方法
  • 5、将使用的Buffer插入到allocated_buffers

中间的这个binder_update_page_range方法,看起来比较可疑,我们跳进去看下:

2.2.1 binder_update_page_range

static int binder_update_page_range(struct binder_alloc *alloc, int allocate,
                    void __user *start, void __user *end)
{
    void __user *page_addr;
    unsigned long user_page_addr;
    struct binder_lru_page *page;
    struct vm_area_struct *vma = NULL;
    struct mm_struct *mm = NULL;
    bool need_mm = false;
    //是否需要申请页面,我们这里肯定是需要的,所以need_mm为true
    for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
        page = &alloc->pages[(page_addr - alloc->buffer) / PAGE_SIZE];
        if (!page->page_ptr) {
            need_mm = true;
            break;
        }
    }

    if (mm) {
        mmap_read_lock(mm);
        vma = alloc->vma;
    }
    
    //判断物理Page是否存在
    for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
        int ret;
        bool on_lru;
        size_t index;
        index = (page_addr - alloc->buffer) / PAGE_SIZE;
        page = &alloc->pages[index];
        if (page->page_ptr) {
            //页面存在
            continue;
        }
        //申请物理Page
        page->page_ptr = alloc_page(GFP_KERNEL |
                        __GFP_HIGHMEM |
                        __GFP_ZERO);
        page->alloc = alloc;
        INIT_LIST_HEAD(&page->lru);
        user_page_addr = (uintptr_t)page_addr;
        //将page插入到vma中
        ret = vm_insert_page(vma, user_page_addr, page[0].page_ptr);
    }
    if (mm) {
        mmap_read_unlock(mm);
        mmput(mm);
    }
    return 0;
}

哦,原来binder_update_page_range是用来申请物理页面的,按需申请Page并将其插入vma中,且被binder_alloc所持有,那么binder_alloc是何许人也?

2.2.2 binder_alloc是什么?

图2.1 - binder_alloc基础结构

通过上图可以看到,binder_alloc持有vmabinder_buffer的空闲列表以及在使用中的列表,还有物理页面,它持有了申请内存相关的数据。

2.2.3 binder_buffer是什么?

图2.2 - binder_buffer基础结构

binder_buffer持有当次通信所需的数据地址以及数据大小,并且被binder_alloc持有。

2.2.4 小结

到这里binder_alloc_new_buf就分析的差不多了,最重要的是申请binder_buffer,以及申请物理Page。

接下来我们看binder_alloc_copy_user_to_buffer

2.3 数据拷贝 - binder_alloc_copy_user_to_buffer

unsigned long
binder_alloc_copy_user_to_buffer(struct binder_alloc *alloc,
                 struct binder_buffer *buffer,
                 binder_size_t buffer_offset,
                 const void __user *from,
                 size_t bytes)
{
    //检查Buffer
    if (!check_buffer(alloc, buffer, buffer_offset, bytes))
        return bytes;
    while (bytes) {
        unsigned long size;
        unsigned long ret;
        struct page *page;
        pgoff_t pgoff;
        void *kptr;
        //从binder_alloc中获得Page
        page = binder_alloc_get_page(alloc, buffer,
                         buffer_offset, &pgoff);
        size = min_t(size_t, bytes, PAGE_SIZE - pgoff);
        //将该Page映射到Kernel地址空间
        kptr = kmap(page) + pgoff;
        //从用户空间拷贝数据到kptr
        ret = copy_from_user(kptr, from, size);
        //释放page
        kunmap(page);
        if (ret)
            return bytes - size + ret;
        bytes -= size;
        from += size;
        buffer_offset += size;
    }
    return 0;
}

这个方法可以说是重中之重,

  • 1、首先是拿到我们之前申请到的物理页面
  • 2、将该Page映射到Kernel的内存地址空间之中,得到地址kptr,这样kptr这个地址也就指向了我们申请到的Page中,也就是说,kptr和用户空间实际上指向同一个物理页面,如果修改kptr的数据,也就是在修改用户空间的数据
  • 3、执行数据拷贝
  • 4、释放页面

我们回顾一下,刚才说kptr和用户空间指向的同一个物理页面,而且只执行了一次从写入端用户空间到内核态的数据拷贝,就完成了数据的传递,这就是“一次拷贝”的真正实现。

三、总结

讲到这里其实就比较清楚了,“一次拷贝”的核心正是Binder驱动与接收进程同时映射到同一个物理页面,发送进程在发生调用时,只需要将其拷贝给内核层的地址中,就完成了数据的传递。

图3.1 - 数据传递示意图

四、疑问

既然Binder驱动和Server进程可以通过内存映射的形式映射到同一个页面,那么我们能不能把Client端的也给映射过去呢?这样甚至都不需要拷贝。

其实这种做法就跟共享内存一样了,存在一些问题:

  • 1、存在一定的安全问题,多个客户端都可以访问该内容
  • 2、数据同步很难处理,同时存在两个甚至多个进程对这段内存作出读写,很容易就出现数据同步问题
  • 3、内存不好管理

所以IPC要想做到快速且安全,至少是需要发生一次拷贝的。