zoukankan      html  css  js  c++  java
  • Android Binder机制 -- Binder驱动

    Binder驱动分析

    由于时间和精力有限,驱动的分析可能会存在不准确或者不全的地方。。。

    准备从 Binder 内存管理、多线程和引用计数三个方面介绍驱动。

    进而回答之前留下的问题。

    数据结构介绍

    我们即将见到Binder驱动的第一个数据结构!!!!!!!有请,,,,

    struct binder_alloc

    每个进程对应一个binder_alloc,通过它,我们就能管理Binder进程的内存了。。。

    定义

    struct binder_alloc {
    	struct mutex mutex;
    	struct vm_area_struct *vma;
    	struct mm_struct *vma_vm_mm;
    	void *buffer;
    	ptrdiff_t user_buffer_offset;
    	struct list_head buffers;
    	struct rb_root free_buffers;
    	struct rb_root allocated_buffers;
    	size_t free_async_space;
    	struct binder_lru_page *pages;
    	size_t buffer_size;
    	uint32_t buffer_free;
    	int pid;
    	size_t pages_high;
    };
    
    • vma

      表示mmap映射的那一段用户空间的虚拟地址空间。

    • buffer

      mmap的系统调用实现会为进程分配一段虚拟地址(vma),然后会调用到binder_mmap,由binder完成内存映射工作。binder的做法就是将创建一段和vma大小一样的内核虚拟地址和vma关联。

      buffer记录的就是内核虚拟地址的起始地址。

      //为内核分配虚拟地址空间
      area = get_vm_area(vma->vm_end - vma->vm_start, VM_ALLOC);
      alloc->buffer = area->addr; //保存起始地址
      alloc->buffer_size = vma->vm_end - vma->vm_start;  // 我们映射的大小
      
    • user_buffer_offset

      vmavma_vm_mm的固定偏移。

      // 负数??
      alloc->user_buffer_offset = vma->vm_start - (uintptr_t)alloc->buffer;
      
    • pages

      mmap时,先为映射的虚拟地址段分配好页面。

      alloc->pages = kzalloc(sizeof(alloc->pages[0]) *  ((vma->vm_end - vma->vm_start) / PAGE_SIZE),GFP_KERNEL);
      

    虽然建立了虚拟地址的映射,但是还是没有看到为我们mmap的虚拟地址分配物理内存??别急,再来认识第二个数据结构,他能帮助我们更好的理解binder_alloc。她就是struct binder_buffer

    struct binder_buffer

    后面,我们传输数据时,一次传输的数据就对应一个binder_buffer

    定义如下:

    struct binder_buffer {
    	struct list_head entry; /* free and allocated entries by address */
    	struct rb_node rb_node; /* free entry by size or allocated entry */
    				/* by address */
    	unsigned free:1;
    	unsigned allow_user_free:1;
    	unsigned async_transaction:1;
    	unsigned debug_id:29;
    
    	struct binder_transaction *transaction;
    
    	struct binder_node *target_node;
    	size_t data_size;
    	size_t offsets_size;
    	size_t extra_buffers_size;
    	void *data;
    };
    
    • entryrb_node

      回到binder_alloc

      //`binder_alloc`
      struct list_head buffers;	// 当前进程所有的 binder_buffer 链表
      struct rb_root free_buffers;		// 空闲状态的 binder_buffer 红黑树
      struct rb_root allocated_buffers;	//正在使用的 binder_buffer 红黑树
      

      binder_alloc::buffers就是是一个链表,当前进程所有的binder_buffer都会挂载到这个链表(通过entry)。

      此外,binder_buffer一旦创建,就不会被轻易回收内存,所以,他有两种状态,被使用和空闲。根据其状态,binder驱动将其挂载到binder_alloc的两棵红黑树中(通过rb_node)。

    • 其余的先不说,说多了很懵逼。

    我们再回到binder_mmap中,前面Binder驱动已经完成了 内核和用户空间虚拟地址的映射,并且创建了一堆page。接下来,就是先创建一个binder_buffer,然后将其添加到binde_alloc::buffersbinder_alloc::free_buffers中。

    int binder_alloc_mmap_handler(struct binder_alloc *alloc,
    			      struct vm_area_struct *vma) {
        ...
        buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
        buffer->data = alloc->buffer;
        //添加到链表中
        list_add(&buffer->entry, &alloc->buffers);
    	buffer->free = 1; //空闲buffer
        //添加到空闲buffer红黑树中
    	binder_insert_free_buffer(alloc, buffer);
    	alloc->free_async_space = alloc->buffer_size / 2;
        ...
    } 
    

    到这里,mmap就结束了,除了为这些数据结构分配内存,没有看到为mmap的虚拟地址分配物理内存的操作。

    内存管理

    32bit系统

    这一节我们会知道,Binder一次传输的数据最大是多少!Binder的一次内存拷贝发生在什么时候。

    mmap

    我们知道,进程在使用binder时需要用到ProcessState类,其对象在构造的时候就会打开binder确定并执行mmap

    //#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
    mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
    if (mVMStart == MAP_FAILED) {    
        close(mDriverFD);    
        mDriverFD = -1;
    }
    
    1. PROT_READ,只读映射。

      简单的理解,就是 mmap出来的虚拟地址空间是作为 数据接收缓冲区用的

    2. mmap映射的虚拟地址空间大小是1M - 8K

    对于由Zygote孵化的进程,都默认创建了ProcessState对象,所以其传输的最大数据就是1M-8K,当然,这只是理论值。

    为了区分要发送的数据,还有一些辅助数据结构,这些也是要占用大小的。

    此外,还要内存碎片问题。。。。

    那是不是我们自己修改ProcessStatemmap的映射大小就能够传输大数据呢?

    是滴,但是驱动对此也做了限制,最大为4M。

    
    static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
    {
        ...
    	if ((vma->vm_end - vma->vm_start) > SZ_4M)
    		vma->vm_end = vma->vm_start + SZ_4M;
    	...
    }
    

    对于大数据的传输,我们可以使用 Ashmem,然后使用Binder传输 文件描述符实现。

    对于mmap具体做了哪些工作,我们用下面的一张图来总结。

    image-20201218111437710

    1. 在内核中申请一段连续的 虚拟地址空间。将其起始地址保存到binder_alloc->buffer,将地址空间大小保存到binder_alloc->pages

    2. 将用户空间的虚拟地址和内核虚拟地址之间的偏移保存到binder_alloc->use_buffer_offset

      这个偏移是固定的,通过这个偏移,binder驱动能轻易的根据内核虚拟地址计算出 用户空间虚拟地址。反之亦然。

    3. 将内核虚拟地址分段,并将页面信息保存到binder_alloc->pages中。

      这里说明一下,虽然名为pages,但是实际上就是binder驱动 前面拿到的虚拟地址分成了若干段,然后每一段的大小都是一个page的大小。

      一开始 SEG是不会关联page的,用到的时候才会关联page。

    4. 创建一个空的binder_buffer,并将其加入到binder_alloc->free_buffersbinder_alloc->buffers中。

      这个在图中没有体现出来,binder驱动的内存管理更多的就是对binder_buffer的管理。后面会详细介绍。

    binder_buffer

    该结构体用于缓存每次 transaction 时的数据,也就是我们通过IPCThreadState::transact发送的数据(用结构体struct binder_transaction_data表示)。

    image-20201217142712431

    创建binder_buffer

    通过函数binder_alloc_new_buf能够获取到一个满足我们需求的binder_buffer。函数原型如下:

    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);
    

    data_size: 同binder_transaction_data.data_size,对应的是我们实际发送数据(用户空间的有效数据)的大小。

    offsets_size: 同binder_transaction_data.offsets_size,对应的是实际发送数据中binder对象的偏移数组的大小。

    extra_buffers_size: 0

    is_aync: 本次申请的binder_buffer是否用于异步传输。

    基本流程:

    1. 计算需要的buffer大小。就是data_sizedata_offset_size四字节对齐后之和。

      data_offsets_size = ALIGN(data_size, sizeof(void *)) +
      		ALIGN(offsets_size, sizeof(void *));
      
      if (data_offsets_size < data_size || data_offsets_size < offsets_size) {
      	return ERR_PTR(-EINVAL);
      }
      //extra_buffers_size == 0 
      size = data_offsets_size + ALIGN(extra_buffers_size, sizeof(void *));
      if (size < data_offsets_size || size < extra_buffers_size) {
      	return ERR_PTR(-EINVAL);
      }
      if (is_async &&
      	alloc->free_async_space < size + sizeof(struct binder_buffer)) {
      	return ERR_PTR(-ENOSPC);
      }
      
      /* Pad 0-size buffers so they get assigned unique addresses */
      size = max(size, sizeof(void *));
      
    2. binder_alloc->free_buffers中找一个合适的buffer。

      mmap中,我们创建了一个空的binder_buffer,将其加入到了free_buffers中。代码如下:

      buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
      buffer->data = alloc->buffer;
      list_add(&buffer->entry, &alloc->buffers);
      buffer->free = 1;
      binder_insert_free_buffer(alloc, buffer);
      alloc->free_async_space = alloc->buffer_size / 2;
      

      补充一下,在这里可以看到,用于异步传输的缓冲区最大值是我们mmap的size的一半。

      此时的内存关系大概如下:

      image-20201218110235876

      mmap时,创建的第一个binder_buffer,其data指向binder_alloc->buffer,也就是当前进程映射的内核虚拟地址空间的首地址。

      binder驱动计算binder_buffer大小并不会根据其传输的实际数据来计算的,而是根据前后两个binder_buffer的距离来计算的。

      static size_t binder_alloc_buffer_size(struct binder_alloc *alloc,
      				       struct binder_buffer *buffer)
      {
          //如果是buffer_alloc->buffers中的最后一个buffer,其大小就是 最后一个buffer起始地址到 虚拟地址空间末尾。
      	if (list_is_last(&buffer->entry, &alloc->buffers))
      		return (u8 *)alloc->buffer +
      			alloc->buffer_size - (u8 *)buffer->data;
          // 后一个buffer的起始地址减去当前buffer的起始地址。
      	return (u8 *)binder_buffer_next(buffer)->data - (u8 *)buffer->data;
      }
      

      我们假设,我们是第一次创建binder_buffer。我们从binder_alloc->free_buffers中拿到的free_buffer就是mmap中创建的。其大小就是binder_alloc->buffer_size(通常就是1M - 8K)。很明显,这个free_buffer太大了。

    3. 分配物理内存。

      分配物理内存的基本单位是page。 这里不是按照binder_buffer的大小来分配,而是按照实际传输的数据大小来分配。

      分配工作由binder_update_page_range完成。

      // free_buffer的 最后一个 page的首地址
      has_page_addr = (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK);
      //我们实际需要的 数据 对应的 page的首地址
      end_page_addr = (void *)PAGE_ALIGN((uintptr_t)buffer->data + size);
      
      // binder_buffer size > actual size 
      if (end_page_addr > has_page_addr)
      	end_page_addr = has_page_addr;
      ret = binder_update_page_range(alloc, 1, (void *)PAGE_ALIGN((uintptr_t)buffer->data), end_page_addr);
      

      binder_update_page_range的主要工作就是通过alloc_page申请page,然后建立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) {
                  //直接复用之前申请过的 page
      			on_lru = list_lru_del(&binder_alloc_lru, &page->lru);
      			continue;
      		}
              //申请 page
      		page->page_ptr = alloc_page(GFP_KERNEL |
      					    __GFP_HIGHMEM |
      					    __GFP_ZERO);
      		
      		page->alloc = alloc;
      		INIT_LIST_HEAD(&page->lru);
      		// 建立内核虚拟地址和page的映射
      		ret = map_kernel_range_noflush((unsigned long)page_addr,
      					       PAGE_SIZE, PAGE_KERNEL,
      					       &page->page_ptr);
      		flush_cache_vmap((unsigned long)page_addr,
      				(unsigned long)page_addr + PAGE_SIZE);
      		// 建立 用户空间和page的映射
      		user_page_addr =
      			(uintptr_t)page_addr + alloc->user_buffer_offset;
      		ret = vm_insert_page(vma, user_page_addr, page[0].page_ptr);
      		
      		if (index + 1 > alloc->pages_high)
      			alloc->pages_high = index + 1;
      	}
      
    4. 裁剪一个新的free binder_buffer

      既然前面的free_buffer过大,我们就可以将其裁剪为两个,一个够用,另一个就是剩下的全部,然后将其加入到binder_alloc->free_buffer.

      // buffer_size 就是前面提到的`1M - 8K`
      // size 就是我们需要的大小。
      if (buffer_size != size) {
      	struct binder_buffer *new_buffer;
      	new_buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
      	new_buffer->data = (u8 *)buffer->data + size;	//将剩余空间都给这个新的free buffer
      	list_add(&new_buffer->entry, &buffer->entry);
      	new_buffer->free = 1;
      	binder_insert_free_buffer(alloc, new_buffer);
      }
      
    5. 将申请到的binder_buffer加入到binder_alloc->allocated中。

      通过函数binder_insert_allocated_buffer_locked完成。

    归还binder_buffer

    binder_buffer完成了传输任务后,就需要将其归还,以便其他传输任务使用。归还操作对应的函数是binder_free_buf_locked

    归还操作如下:

    1. 归还 申请的 page, 也是通过binder_update_page_range完成。归还操作代码大致如下:

      for (page_addr = end - PAGE_SIZE; page_addr >= start; page_addr -= PAGE_SIZE) {
      	bool ret;
      	size_t index;
      	index = (page_addr - alloc->buffer) / PAGE_SIZE;
      	page = &alloc->pages[index];
      	ret = list_lru_add(&binder_alloc_lru, &page->lru);
      }
      

      如上,将binder_buffer占用的page页添加到binder_alloc_lru中,值得注意的时,归还page并不是将其归还给操作系统,而是归还给binder_alloc,其还是占据实际的物理内存的,这样做的目的是避免频繁的alloc_pagefree_page的调用。

      再来看一下调用代码:

      binder_update_page_range(alloc, 0,
      		(void *)PAGE_ALIGN((uintptr_t)buffer->data),
      		(void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK));
      

      PAGE_ALIGN:如果(uintptr_t)buffer->data的地址不是page的其实地址,其返回值就是下一个page的其实地址。

      (((uintptr_t)buffer->data + buffer_size) & PAGE_MASK),实际上就是向下取整。用图片来解释一下。

      image-20201219194849120

      由于PAGE1PAGE3同时被其他的binder_buffer所引用,我们能回收的只有PAGE2

      图片画的可能有点歧义,橙色部分应该 binder_buffer中数据占用占用的内存对应的page。

    2. 判断是否需要合并binder_buffer

      image-20201217212102309

      如上图所示,curr表示我们当前需要归还的binder_buffer,它的下一个指向next,也是free状态的,所以要将这两个binder_buffer合并。

      同时,它的上一个prev也是free状态,所以也要合并,最终这三个binder_buffer会合并成一个。

      	// 后面还有一个空闲 binder_bufer, 删除它
      	if (!list_is_last(&buffer->entry, &alloc->buffers)) {
      		struct binder_buffer *next = binder_buffer_next(buffer);
      
      		if (next->free) {
      			rb_erase(&next->rb_node, &alloc->free_buffers);
      			// 
      			binder_delete_free_buffer(alloc, next);
      		}
      	}
      	// 前面还有一个空闲 binder_buffer, 删除当前buffer
      	if (alloc->buffers.next != &buffer->entry) {
      		struct binder_buffer *prev = binder_buffer_prev(buffer);
      
      		if (prev->free) {
      			binder_delete_free_buffer(alloc, buffer);
      			rb_erase(&prev->rb_node, &alloc->free_buffers);
      			buffer = prev;
      		}
      	}
      

      合并操作的主要工作由binder_delete_free_buffer完成。按理说只要释放掉binder_buffer的内存就可以了,反正page前面已经被binder_alloc处理了。但有一种特殊情况。就是两个binder_buffer处于同一个page

      static void binder_delete_free_buffer(struct binder_alloc *alloc,
      				      struct binder_buffer *buffer)
      {
      	struct binder_buffer *prev, *next = NULL;
      	bool to_free = true;
      	BUG_ON(alloc->buffers.next == &buffer->entry);
      	prev = binder_buffer_prev(buffer);
      	BUG_ON(!prev->free);
          // buffer->prev 的尾部 和 buffer的首部 在同一个 page中。
      	if (prev_buffer_end_page(prev) == buffer_start_page(buffer)) {
              // 那就不能回收当前page
      		to_free = false;
      	}
      
      	if (!list_is_last(&buffer->entry, &alloc->buffers)) {
      		next = binder_buffer_next(buffer);
              // buffer->next 的首部和 buffer 的尾部 在同一个page中
      		if (buffer_start_page(next) == buffer_start_page(buffer)) {        
        	      // 那也能回收当前page
      			to_free = false;
      		}
      	}
      	// buffer的首刚好是page 的首地址
      	if (PAGE_ALIGNED(buffer->data)) {
              // 也不能回收当前page
      		to_free = false;
      	}
      
      	if (to_free) {
              // 为什么只回收一个page呢?
      		binder_update_page_range(alloc, 0, buffer_start_page(buffer),
       				 buffer_start_page(buffer) + PAGE_SIZE);
      	}
      	list_del(&buffer->entry);
          //释放 binder_buffer占用的内存
      	kfree(buffer);
      }
      

      为什么PAGE_ALIGNED(buffer->data)成立是,不用回收当前的page?为什么后面只回收只用回收一个PAGE呢?回到我们刚刚的图片。

      image-20201219194849120

      我们回收中间的B(binder_buffer)后,PAGE2已经被回收(添加到binder_alloc_lru中)了(此时B已经是空闲 binder_buffer)。由于AC的存在,我们没有合并操作。现在我们要回收A。首先执行的是下面的代码:

      //binder_free_buf_locked
      binder_update_page_range(alloc, 0,
      		(void *)PAGE_ALIGN((uintptr_t)buffer->data),
      		(void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK));
      

      在回收binder_buffer A时,以这段代码而言,PAGE_ALIGN((uintptr_t)buffer->data) 的值是PAGE2的起始地址,(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK)的值是PAGE1的起始地址。所以这段代码不会执行回收操作。

      然后执行的是binder_delete_free_buffer。由于A后面的B是空闲状态,所以要合并AB。合并后的状态如下:

      image-20201219195137063

      在回到binder_delete_free_buffer中,按照该函数的流程走下来,我们只要回收PAGE1就行。这也是为什么binder_delete_free_buffer中的binder_update_page_range只会回收一个页面。

      同理,如果A刚好是在PAGE1的首地址,那么在进入binder_free_buf_locked的第一个binder_update_page_range时,PAGE1就会被回收。所以binder_delete_free_buffer也就不需要回收PAGE

      最后一步两个binder_buffer合并后,多出一个binder_buffer,binder驱动要会后这个数据结构占用的内存。

    总结

    关于Binder驱动的内存管理就介绍到这里,回顾一下重点。

    1. 一次内存拷贝的原理。

      mmap在用户空间和内核空间 各分配了一个连续的虚拟地址空间。当我们创建binder_buffer时,驱动会将这两个虚拟地址(大小就是传输数据的实际大小)映射到相同的物理内存上。就避免了 一次从内核往用户空间拷贝的操作。

    2. binder一次传输最大的size是多少

      • zygote孵化的进程理论上最大是1M-8K(有ProcessState限制),对于异步传输是1M-8K的一半

      • 对于一般的native进程,最大是4M,由Binder驱动限制。

      • 由前面的分析可以知道,当前能够传输的数据最大size取决于binder_alloc->free_buffers中最大的空闲buffer的size.

    数据传输原理

    之前已经介绍过,数据从进程A到进程B的 native 层的实现,这一章节主要就是介绍Binder驱动是如何传输数据的。

    不同于TCP的流式传输,Binder更像UDP的报文传输,binder驱动需要明确知道传输数据的size(从前面的内存管理一节也可以看出这一点)。

    首先看一下数据传输的基本调用流程(BC_TRANSACTION):

    ioctl(fd, BINDER_WRITE_READ, &bwr);
    ---------进入binder驱动----------
    binder_ioctl
        binder_ioctl_write_read
        	binder_thread_write
        		binder_transaction //(分析重点)
        	binder_thread_read //(分析重点)	 
    

    前面我们虽然提到了一次内存拷贝的本质,但是这个一次内存拷贝发生在什么时候???? 还有 传输 binder 对象和文件描述符的时候,binder驱动做了什么事情??

    大多数情况下,binder数据传输都是类似于C/S模式,有客户端发起请求,服务端响应客户端的请求。接下来我们也以着重分析请求和响应两个过程。

    数据从Client到Service

    当我们通过mRemote()->transaction发送数据时,最终执行的是ioctl(fd, BINDER_WRITE_READ, &bwr)。该调用会导致当前进程进入内核态。最终调用到binder驱动的binder_ioctl函数。

    数据从Service到Client

  • 相关阅读:
    Javascript面向对象(三):非构造函数的继承
    Javascript面向对象(二):构造函数的继承
    Javascript 面向对象(一):封装
    .NET面试题系列[12]
    .NET面试题系列[11]
    .NET面试题系列[10]
    .NET面试题系列[9]
    .NET面试题系列[8]
    .NET面试题系列[7]
    .NET面试题系列[6]
  • 原文地址:https://www.cnblogs.com/liutimo/p/14160883.html
Copyright © 2011-2022 走看看