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

原文地址:https://www.cnblogs.com/liutimo/p/14160883.html