Binder - 6、Binder中的一次拷贝

一、前言

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

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

二、源码分析

2.1 入口-binder_transaction

Kernel\drivers\android\binder.c

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

c++ 复制代码
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方法:

c++ 复制代码
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

c++ 复制代码
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

c++ 复制代码
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要想做到快速且安全,至少是需要发生一次拷贝的。

相关推荐
HerayChen8 分钟前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野9 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing112311 分钟前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
小黄人软件36 分钟前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
dj15402252031 小时前
group_concat配置影响程序出bug
android·bug
周全全1 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
- 羊羊不超越 -2 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
wk灬丨3 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸3 小时前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android