[Android]从一个简单的AIDL实现看binder原理(七)Binder的一次拷贝和内存限制

参考链接:

从一个简单的AIDL实现看binder原理(一)简单的AIDL实现
从一个简单的AIDL实现看binder原理(二)bindService的调用过程
从一个简单的AIDL实现看binder原理(三)bindService调用过程中Binder的传递
从一个简单的AIDL实现看binder原理(四)bindService调用过程中Binder的写入
从一个简单的AIDL实现看binder原理(五)bindService调用过程中Binder的转换
从一个简单的AIDL实现看binder原理(六)Android系统中Binder的运行
【[Android]从一个简单的AIDL实现看binder原理(七)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函数。

[Android]从一个简单的AIDL实现看binder原理(七)Binder的一次拷贝和内存限制
文章图片
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; 。。。 }

    推荐阅读