Binder进程间通讯(五)----内核虚拟地址空间管理

    上一篇笔记分析到了Binder驱动在调用mmap建立内存映射的过程。可是在分配物理内存的时候,咱们只分配了一个页面。当咱们须要更多物理内存的时候,Binder驱动才回去继续分配物理内存。node

    另外,物理内存的分配是以页为单位的,可是在进程中使用内存是却并非以页为单位,因此binder驱动程序使用结构体binder_buffer来描述进程对内存的使用,表示一块内存。linux

分配内核缓冲区

    当一个进程使用BC_TRANSACTION 或者BC_REPLY向binder驱动发送命令请求和另外一个进程通讯时须要向另外一个进程传递数据(经过结构体binder_transaction_data结构体),binder驱动程序就会将这些数据从用户空间拷贝到内核空间(描述不许确的,应该是从client进程的用户空间拷贝到service进程的内核空间,由于同一个binder_proc的内核空间和用户空间映射的是同一片物理内存,理论上并不须要拷贝的),而后再传递给目标进程。这时候,binder驱动须要在目标进程的内核虚拟地址空间中分配一块地址来存放这些数据。算法

    因此这里涉及到一个关键步骤:分配内核虚拟地址空间,这是经过binder_alloc_buf实现的数据结构

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{
	.......
}

    参数就颇有讲究,须要理解一下,proc表示的是目标进程。第二第三个参数就是binder_transaction_data数据结构中的data_size和offsets_size(binder结构体说明),最后一个参数表示请求的内核虚拟地址空间用于同步事物仍是异步事物。异步

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{
	struct rb_node *n = proc->free_buffers.rb_node;
	struct binder_buffer *buffer;
	size_t buffer_size;
	struct rb_node *best_fit = NULL;
	void *has_page_addr;
	void *end_page_addr;
	size_t size;

	if (proc->vma == NULL) {
		pr_err("%d: binder_alloc_buf, no vma\n",
		       proc->pid);
		return NULL;
	}

    //按void指针大小对齐,size表示要分配的虚拟地址空间的大小
	size = ALIGN(data_size, sizeof(void *)) +
		ALIGN(offsets_size, sizeof(void *));
    //溢出检测
	if (size < data_size || size < offsets_size) {
		binder_user_error("%d: got transaction with invalid size %zd-%zd\n",
				proc->pid, data_size, offsets_size);
		return NULL;
	}
    //若是使用异步请求,那么还须要检测须要使用的内核虚拟地址空间大小 是否大于 剩余可用于异步操做的地址大小
    //(咱们在binder_mmap的时候有看到,初始设置为总虚拟地址空间大小的一半)
	if (is_async &&
	    proc->free_async_space < size + sizeof(struct binder_buffer)) {
		binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
			     "%d: binder_alloc_buf size %zd failed, no async space left\n",
			      proc->pid, size);
		return NULL;
	}

	......
}

    须要注意在咱们计算须要的内核虚拟地址空间总大小的时候,还须要加上一个binder_buffer结构体的大小。上文有提到,咱们会使用binder_buffer来组织内存中的数据。async

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{

	.....
    //使用最佳匹配算法,在空闲的内核虚拟地址空间树种(proc->buffer_free中)寻找有没有适合大小的空间可用
	while (n) {
		buffer = rb_entry(n, struct binder_buffer, rb_node);
		BUG_ON(!buffer->free);
        //至关于binder_buffer.data的长度
		buffer_size = binder_buffer_size(proc, 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;
		}
	}
    //没有找到,则直接报错退出
	if (best_fit == NULL) {
		pr_err("%d: binder_alloc_buf size %zd failed, no address space\n",
			proc->pid, size);
		return NULL;
	}
    //没有找到大小恰好合适的空间大小,可是找到了一块较大的地址空间,而后计算地址空间大小,并保存在buffer_size变量中
	if (n == NULL) {
		buffer = rb_entry(best_fit, struct binder_buffer, rb_node);
		buffer_size = binder_buffer_size(proc, buffer);
	}

	......
}

    以上一段代码的主要做用是寻找空闲的内核虚拟地址空间。函数

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{
	.......

    //计算buffer(上面找到的空闲内核虚拟地址空间起始地址)结束地址所在的页 的起始地址
	has_page_addr =
		(void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK);

	if (n == NULL) {
		if (size + sizeof(struct binder_buffer) + 4 >= buffer_size)
			buffer_size = size; /* no room for other buffers */
        // 表示找到的buffer大小相比 须要的buffer大小 大了挺多,直接用浪费裁剪一下。
		else
			buffer_size = size + sizeof(struct binder_buffer);
	}
    
	end_page_addr =
		(void *)PAGE_ALIGN((uintptr_t)buffer->data + buffer_size);
	if (end_page_addr > has_page_addr)
		end_page_addr = has_page_addr;
	if (binder_update_page_range(proc, 1,
	    (void *)PAGE_ALIGN((uintptr_t)buffer->data), end_page_addr, NULL))
		return NULL;

	......
}

    上面这段代码应该说是这个函数,甚至是binder对于内核空间分配和创建物理内存映射最关键的几局代码了。并且更主要的是理解很是困难!!我花了大概一个下午才弄通其中的逻辑。ui

    在理解以前,先给出一个结论,这在理解这段代码的时候很是关键。spa

    咱们在上文找到了一个binder_buffer类型的对象buffer,表示一个未被使用的虚拟地址空间段,用图来表示是这样的.net

                       

    buffer由两部分组成,binder_buffer结构体,以及后面的data数据空间(建议回顾一下binder_buffer的结构)。

    根据寻找buffer的代码,咱们分两种状况来讨论:

状况 1 

    首先是has_page_addr的计算,buffer->data+buffer_size的结果是这个buffer的结束地址。若是不知道为何……本身再去看看代码吧,buffer_size表示的并非整个buffer的长度,而是buffer第二部分data的长度。而后获取这个地址所在的页的起始位置,就以下图(虚线表示分页的线)

                        

    size + sizeof(struct binder_bufffer)+4 > buffer_size  变换一下 :

                                buffer_size - size < sizeof(struct binder_bufffer) + 4   

   表示去掉实际须要的data区域以后,剩下的空间不够分配一个新的binder_buffer内存块的。这种状况下,让buffer_size = size;    

    复杂的来了,end_page_addr的计算,首先buffer_data + buffer_size,获得一个地址,咱们暂且将这个地址命名为 “z地址”! 

                    

    接下去就要讨论end_page_addr和 has_page_addr的关系

                    

                        

    点状虚线表示页分割地址,这两种状况 z地址 和 buffer结束地址 处于同一页,那么end_page_addr >has_pager_addr,而后手动调整成 end_page_addr = has_pager_addr.

                    

                    

    上面这种状况,直接has_page_addr = end_page_addr,以后的处理就和以前同样了。

    思考:会出现end_page_addr < has_page_addr吗?不会,由于若是要出现这种状况,那么剩余空间必定须要超过1页大小,这就不符合咱们对状况1的定义。

    最后调用binder_update_page_range方法来进行物理内存分配(这个函数在binder_mmap创建映射关系的时候讲解过)。关键是传入的 start 和 end两个参数。其实地址是buffer->data按页对齐(将起始地址按页对齐很好理解,由于物理内存的分配是按页分配的,因此每一个页面的起始地址和结束地址确定是按页对齐的),结束地址是end_page_addr。会出现两种状况:

    start < end ,这个时候binder_update_page_range 调用和之前同样,分配一页或者多页(size很大,跨了多页)的内存。

    对齐后start = end,这个时候binder_update_page_range 并不会分配内存,而是直接返回了。那就很奇怪了,这种状况咱们不须要分配物理内存吗?

    仔细看上面几幅图,咱们发现,第一图size占了三页,可是咱们最后只分配了一页,中间那页,左边不须要分配吗?

    思考,咱们在mmap的时候分配了第一页的内存,而且建立了一个binder_buffer,若是这时候我找到的buffer就是这个binder_buffer呢?那么第一页已经被分配,因此左边不须要分配。

    若是这个buffer不是处于开头,那么说明前面的buffer必定在被使用(未被使用的话,这两个buffer会合并的,在释放物理内存的地方能够找到依据),因此前面那也必定是已经被分配了,因此左边不用再次分配。

    左边为何不分配的缘由找到了,再来思考右边:

    若是buffer处于整个buffers的末尾,咱们须要知道linux规定,内核地址空间的最后必需要留一页当作内存保护,也就是说,buffer的末尾地址不可能和z地址在同一个页中,因此不存在右侧有空间未分配的状况

    若是不是处于buffer末尾,那么右侧的buffer必定是已经被使用了的,不然会合并。既然已经被使用,那么右侧的物理内存确定已经被分配了,因此没问题。

    再如图三,size仅仅在一个页面内,可是这个页面也没有分配内存,因此它的内存页必定被分配了吗?是的,若是左侧相邻的binder_buffer和他在同一页,那么这一页已经被分配,若是不是在同一页,那么说明个buffer的binder_buffer结构体跨越了两个页,这种状况下这一页的物理内存早就被分配好了(见下文)。

    PS : 咱们会发现多了一块剩余空间,咱们既没有使用它,也没有给他分配物理内存。这表示,不必定全部binder_buffer都是相邻的,可能中间还会隔着一个大小比较小的空闲区域。

状况2

    has_page_addr的计算和状况1相同。

    关键剩余的空间足够分配下一个binder_buffer,因此须要进行分割。

                

    ps:上图中buffer_size仍是最开始的buffer_size,不是裁剪后的buffer_size,这个变量的复用真的让代码变得很不清晰,万恶!

    end_page_addr的大小和状况1不一样,不只仅包含了实际须要数据的大小size,并且还包含了一个额外空间,这个额外空间大小是binder_buffer结构体的大小。

    而后和上面状况1同样分配物理内存。咱们为何要为额外空间分配内存?想一想,这个额外空间是什么?他必定会成为下一个binder_buffer的binder_buffer的结构体的内存区域,咱们提早为他所在页分配了物理内存。    

    上面这段文字可能并非很容易理解,事实上若是不本身思考一些时间,也不可能理解,思考,结合图片才能带来有效的成果。

    

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{
	....
    // buffer已经被从新使用,因此从原来空闲列表中去除
	rb_erase(best_fit, &proc->free_buffers);
	buffer->free = 0;
    //插入buffer列表中
	binder_insert_allocated_buffer(proc, buffer);
    //进行了切割才会出现这个状况
	if (buffer_size != size) {
        //将不使用的内核虚拟地址空间新建一个binder_buffer对象
		struct binder_buffer *new_buffer = (void *)buffer->data + size;
        //插入到buffer列表中
		list_add(&new_buffer->entry, &buffer->entry);
		new_buffer->free = 1;
        //同时插入到free_buffer列表中
		binder_insert_free_buffer(proc, new_buffer);
	}
	binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
		     "%d: binder_alloc_buf size %zd got %p\n",
		      proc->pid, size, buffer);
    //设置buffer的属性
	buffer->data_size = data_size;
	buffer->offsets_size = offsets_size;
	buffer->async_transaction = is_async;
    //若是是异步操做,还要将可用于异步操做的空间上减去此次用掉的空间
	if (is_async) {
		proc->free_async_space -= size + sizeof(struct binder_buffer);
		binder_debug(BINDER_DEBUG_BUFFER_ALLOC_ASYNC,
			     "%d: binder_alloc_buf size %zd async free %zd\n",
			      proc->pid, size, proc->free_async_space);
	}

	return buffer;
}

    最后剩下的代码就相对容易理解不少了。

 

释放内核缓冲区

    当进程处理完Binder驱动发给他的BR_TRANSACTION或者BR_REPLY以后,就会使用BC_FREE_BUFFER来通知Binder驱动程序释放相应的内核缓冲区,以避免浪费空间。

    释放内存经过函数binder_free_buf实现

static void binder_free_buf(struct binder_proc *proc,
			    struct binder_buffer *buffer)
{
	size_t size, buffer_size;
    //
	buffer_size = binder_buffer_size(proc, buffer);

	size = ALIGN(buffer->data_size, sizeof(void *)) +
		ALIGN(buffer->offsets_size, sizeof(void *));

	......
    //若是是异步事物,二话不说,先把异步事物空间让出来再说!
	if (buffer->async_transaction) {
		proc->free_async_space += size + sizeof(struct binder_buffer);

		binder_debug(BINDER_DEBUG_BUFFER_ALLOC_ASYNC,
			     "%d: binder_free_buf size %zd async free %zd\n",
			      proc->pid, size, proc->free_async_space);
	}
    //释放当前buffer数据区所占用的虚拟地址空间所对应的物理内存。
	binder_update_page_range(proc, 0,
		(void *)PAGE_ALIGN((uintptr_t)buffer->data),
		(void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK),
		NULL);
    //将他从正在使用的buffer列表中删除
	rb_erase(&buffer->rb_node, &proc->allocated_buffers);
	buffer->free = 1;
    //查找该buffer的下一个binder_buffer是否为空buffer,若是是,那么须要删除下一个buffer(和当前buffer合并)
	if (!list_is_last(&buffer->entry, &proc->buffers)) {
		struct binder_buffer *next = list_entry(buffer->entry.next,
						struct binder_buffer, entry);

		if (next->free) {
			rb_erase(&next->rb_node, &proc->free_buffers);
			binder_delete_free_buffer(proc, next);
		}
	}
    //查找该buffer的上一个binder_buffer是否为空,若是是,那么删除当前buffer(合并到上一个buffer)
	if (proc->buffers.next != &buffer->entry) {
		struct binder_buffer *prev = list_entry(buffer->entry.prev,
						struct binder_buffer, entry);

		if (prev->free) {
			binder_delete_free_buffer(proc, buffer);
			rb_erase(&prev->rb_node, &proc->free_buffers);
			buffer = prev;
		}
	}
    //将合并后的buffer插入到空buffer列表中
	binder_insert_free_buffer(proc, buffer);
}

    关于删除物理内存的地方,若是想要详细追究,仍是可使用和分配相同的方式去理解。

static void *buffer_start_page(struct binder_buffer *buffer)
{
	return (void *)((uintptr_t)buffer & PAGE_MASK);
}

static void *buffer_end_page(struct binder_buffer *buffer)
{
	return (void *)(((uintptr_t)(buffer + 1) - 1) & PAGE_MASK);
}

    这里有两个方法,分别用来计算buffer起始位置所在的页的地址和buffer结束位置所在的页的地址。

    这个函数就是上面用来删除空闲buffer的,在调用这个函数以前咱们已经保证了它不是proc得第一个buffer,并且它前面的binder_buffer必定是空的。

static void binder_delete_free_buffer(struct binder_proc *proc,
				      struct binder_buffer *buffer)
{
	struct binder_buffer *prev, *next = NULL;
	int free_page_end = 1;
	int free_page_start = 1;

	BUG_ON(proc->buffers.next == &buffer->entry);
	prev = list_entry(buffer->entry.prev, struct binder_buffer, entry);
	BUG_ON(!prev->free);
    //若是前一个buffer的结束地址和当前buffer的开始地址在同一个页面上(两个buffer不必定相邻的)
	if (buffer_end_page(prev) == buffer_start_page(buffer)) {
		free_page_start = 0;
        //若是前一个buffer的结束地址和当前页面的结束地址在同一个页面上
		if (buffer_end_page(prev) == buffer_end_page(buffer))
			free_page_end = 0;
		binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
			     "%d: merge free, buffer %p share page with %p\n",
			      proc->pid, buffer, prev);
	}
    //若是当前buffer不是最后一个buffer
	if (!list_is_last(&buffer->entry, &proc->buffers)) {
		next = list_entry(buffer->entry.next,
				  struct binder_buffer, entry);
        //若是下一个buffer的起始地址和当前buffer的结束地址在同一个页面上
		if (buffer_start_page(next) == buffer_end_page(buffer)) {
			free_page_end = 0;
            //若是下一个buffer起始地址和当前buffer的起始地址在同一个buffer上
			if (buffer_start_page(next) ==
			    buffer_start_page(buffer))
				free_page_start = 0;
			binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
				     "%d: merge free, buffer %p share page with %p\n",
				      proc->pid, buffer, prev);
		}
	}
    //将当前buffer从buffer列表中删除
	list_del(&buffer->entry);
	if (free_page_start || free_page_end) {
		binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
			     "%d: merge free, buffer %p do not share page%s%s with %p or %p\n",
			     proc->pid, buffer, free_page_start ? "" : " end",
			     free_page_end ? "" : " start", prev, next);
		binder_update_page_range(proc, 0, free_page_start ?
			buffer_start_page(buffer) : buffer_end_page(buffer),
			(free_page_end ? buffer_end_page(buffer) :
			buffer_start_page(buffer)) + PAGE_SIZE, NULL);
	}
}

    这一段在罗老师的书中仍是介绍的比较详细的,说到底这里进行那么多if的判断都是防止内存的错误释放,只有当前一整页的内存都再也不被使用,才回去释放物理内存,不然不会释放物理内存。

    状况虽然不少,可是经过画图和代码仍是比较容易理解的,这里就不分析了。

查询内存缓冲区

    做为一个用户进程,在通知Binder驱动程序表示我要释放内存的时候,带上了一个地址参数,可是这个地址参数确定是用户空间的虚拟地址空间的地址,不多是内核空间的地址,因此,驱动程序还须要经过用户空间地址来寻找内核空间对应的地址,经过函数binder_buffer_lookup

// user_ptr就是用户进程传给binder驱动程序的参数
// 表示用户空间binder_buffer的data所指向的地址。
static struct binder_buffer *binder_buffer_lookup(struct binder_proc *proc,
						  uintptr_t user_ptr)
{
	struct rb_node *n = proc->allocated_buffers.rb_node;
	struct binder_buffer *buffer;
	struct binder_buffer *kern_ptr;
    
    //计算内核缓冲区中对应的binder_buffer的地址
	kern_ptr = (struct binder_buffer *)(user_ptr - proc->user_buffer_offset
		- offsetof(struct binder_buffer, data));

    //循环,经过这个buffer地址,找到对应的buffer节点。
	while (n) {
		buffer = rb_entry(n, struct binder_buffer, rb_node);
		BUG_ON(buffer->free);

		if (kern_ptr < buffer)
			n = n->rb_left;
		else if (kern_ptr > buffer)
			n = n->rb_right;
		else
			return buffer;
	}
	return NULL;
}
相关文章
相关标签/搜索