美文网首页
[Android]从一个简单的AIDL实现看binder原理(七

[Android]从一个简单的AIDL实现看binder原理(七

作者: dafasoft | 来源:发表于2020-11-25 13:49 被阅读0次

    参考链接:

    从一个简单的AIDL实现看binder原理(一)简单的AIDL实现
    从一个简单的AIDL实现看binder原理(二)bindService的调用过程
    从一个简单的AIDL实现看binder原理(三)bindService调用过程中Binder的传递
    从一个简单的AIDL实现看binder原理(四)bindService调用过程中Binder的写入
    从一个简单的AIDL实现看binder原理(五)bindService调用过程中Binder的转换
    从一个简单的AIDL实现看binder原理(六)Android系统中Binder的运行

    我们都知道,Binder能作为Android系统的主要IPC工具有一个原因是它的效率更高,而效率更高的体现主要是在IPC过程中只会出现一次内存拷贝即可将A进程用户空间的数据拷贝到B进程,这里主要涉及到两个方面:

    1.Binder的mmap函数,会将内核空间直接与用户空间对应,用户空间可以直接访问内核空间的数据
    2.A进程的数据会被直接拷贝到B进程的内核空间(一次拷贝)

    #define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))
      
      ProcessState::ProcessState()
          : mDriverFD(open_driver())
          , mVMStart(MAP_FAILED)
          , mManagesContexts(false)
          , mBinderContextCheckFunc(NULL)
          , mBinderContextUserData(NULL)
          , mThreadPoolStarted(false)
          , mThreadPoolSeq(1){
         if (mDriverFD >= 0) {
        ....
              // mmap the binder, providing a chunk of virtual address space to receive transactions.
              mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
         ...
       
          }
      }
    

    mmap函数属于系统调用,mmap会从当前进程中获取用户态可用的虚拟地址空间(vm_area_struct *vma),并在mmap_region中真正获取vma,然后调用file->f_op->mmap(file, vma),进入驱动处理,之后就会在内存中分配一块连续的虚拟地址空间,并预先分配好页表、已使用的与未使用的标识、初始地址、与用户空间的偏移等等,通过这一步之后,就能把Binder在内核空间的数据直接通过指针地址映射到用户空间,供进程在用户空间使用,这是一次拷贝的基础,一次拷贝在内核中的标识如下:

    struct binder_proc {
        struct hlist_node proc_node;
        // 四棵比较重要的树 
        struct rb_root threads;
        struct rb_root nodes;
        struct rb_root refs_by_desc;
        struct rb_root refs_by_node;
        int pid;
        struct vm_area_struct *vma; //虚拟地址空间,用户控件传过来
        struct mm_struct *vma_vm_mm;
        struct task_struct *tsk;
        struct files_struct *files;
        struct hlist_node deferred_work_node;
        int deferred_work;
        void *buffer; //初始地址
        ptrdiff_t user_buffer_offset; //这里是偏移
        
        struct list_head buffers;//这个列表连接所有的内存块,以地址的大小为顺序,各内存块首尾相连
        struct rb_root free_buffers;//连接所有的已建立映射的虚拟内存块,以内存的大小为index组织在以该节点为根的红黑树下
        struct rb_root allocated_buffers;//连接所有已经分配的虚拟内存块,以内存块的开始地址为index组织在以该节点为根的红黑树下
        
        }
    

    上面只是在APP启动的时候开启的地址映射,但并未涉及到数据的拷贝,下面看数据的拷贝操作。当数据从用户空间拷贝到内核空间的时候,是直从当前进程的用户空间接拷贝到目标进程的内核空间,这个过程是在请求端线程中处理的,操作对象是目标进程的内核空间。看如下代码:

    static void binder_transaction(struct binder_proc *proc,
                       struct binder_thread *thread,
                       struct binder_transaction_data *tr, int reply){
                       ...
            在通过进行binder事物的传递时,如果一个binder事物(用struct binder_transaction结构体表示)需要使用到内存,
            就会调用binder_alloc_buf函数分配此次binder事物需要的内存空间。
            需要注意的是:这里是从目标进程的binder内存空间分配所需的内存
            //从target进程的binder内存空间分配所需的内存大小,这也是一次拷贝,完成通信的关键,直接拷贝到目标进程的内核空间
            //由于用户空间跟内核空间仅仅存在一个偏移地址,所以也算拷贝到用户空间
            t->buffer = binder_alloc_buf(target_proc, tr->data_size,
                tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
            t->buffer->allow_user_free = 0;
            t->buffer->debug_id = t->debug_id;
            //该binder_buffer对应的事务    
            t->buffer->transaction = t;
            //该事物对应的目标binder实体 ,因为目标进程中可能不仅仅有一个Binder实体
            t->buffer->target_node = target_node;
            trace_binder_transaction_alloc_buf(t->buffer);
            if (target_node)
                binder_inc_node(target_node, 1, 0, NULL);
            // 计算出存放flat_binder_object结构体偏移数组的起始地址,4字节对齐。
            offp = (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));
               // struct flat_binder_object是binder在进程之间传输的表示方式 //
               // 这里就是完成binder通讯单边时候在用户进程同内核buffer之间的一次拷贝动作 //
              // 这里的数据拷贝,其实是拷贝到目标进程中去,因为t本身就是在目标进程的内核空间中分配的,
            if (copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)) {
                binder_user_error("binder: %d:%d got transaction with invalid "
                    "data ptr\n", proc->pid, thread->pid);
                return_error = BR_FAILED_REPLY;
                goto err_copy_data_failed;
            }
    

    可以看到binder_alloc_buf(target_proc, tr->data_size,tr->offsets_size, !reply && (t->flags & TF_ONE_WAY))函数在申请内存的时候,是从target_proc进程空间中去申请的,这样在做数据拷贝的时候copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)),就会直接拷贝target_proc的内核空间,而由于Binder内核空间的数据能直接映射到用户空间,这里就不在需要拷贝到用户空间。这就是一次拷贝的原理。内核空间的数据映射到用户空间其实就是添加一个偏移地址,并且将数据的首地址、数据的大小都复制到一个用户空间的Parcel结构体,具体可以参考Parcel.cpp的Parcel::ipcSetDataReference函数。


    image.png

    Binder传输数据的大小限制

    虽然APP开发时候,Binder对程序员几乎不可见,但是作为Android的数据运输系统,Binder的影响是全面性的,所以有时候如果不了解Binder的一些限制,在出现问题的时候往往是没有任何头绪,比如在Activity之间传输BitMap的时候,如果Bitmap过大,就会引起问题,比如崩溃等,这其实就跟Binder传输数据大小的限制有关系,在上面的一次拷贝中分析过,mmap函数会为Binder数据传递映射一块连续的虚拟地址,这块虚拟内存空间其实是有大小限制的,不同的进程可能还不一样。

    普通的由Zygote孵化而来的用户进程,所映射的Binder内存大小是不到1M的,准确说是 110241024) - (4096 *2) :这个限制定义在ProcessState类中,如果传输说句超过这个大小,系统就会报错,因为Binder本身就是为了进程间频繁而灵活的通信所设计的,并不是为了拷贝大数据而使用的:

    #define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))
    

    而在内核中,其实也有个限制,是4M,不过由于APP中已经限制了不到1M,这里的限制似乎也没多大用途:

    static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
    {
        int ret;
        struct vm_struct *area;
        struct binder_proc *proc = filp->private_data;
        const char *failure_string;
        struct binder_buffer *buffer;
        //限制不能超过4M
        if ((vma->vm_end - vma->vm_start) > SZ_4M)
            vma->vm_end = vma->vm_start + SZ_4M;
        。。。
        }
    

    相关文章

      网友评论

          本文标题:[Android]从一个简单的AIDL实现看binder原理(七

          本文链接:https://www.haomeiwen.com/subject/tvqniktx.html