Binder进程间通信(四)----映射设备文件

走远了吗. 提交于 2020-02-29 11:11:11

    进程打开了/dev/binder设备之后,还需要使用mmap方法将这个设备文件映射到进程的内存之中,然后才能使用Binder进程间通信。(关于mmap是一种实现ipc的重要机制,http://www.cnblogs.com/huxiao-tee/p/4660352.html )

    /dev/binder是一个虚拟设备,我们映射这个东西主要是为了为进程分配内核缓冲区,以便传输进程间通信数据。我们在初始化驱动程序的时候知道,当调用mmap映射/dev/binder时,实际上会调用的方法是binder_mmap

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;

	if (proc->tsk != current)
		return -EINVAL;

	if ((vma->vm_end - vma->vm_start) > SZ_4M)
		vma->vm_end = vma->vm_start + SZ_4M;

	binder_debug(BINDER_DEBUG_OPEN_CLOSE,
		     "binder_mmap: %d %lx-%lx (%ld K) vma %lx pagep %lx\n",
		     proc->pid, vma->vm_start, vma->vm_end,
		     (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags,
		     (unsigned long)pgprot_val(vma->vm_page_prot));

	if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
		ret = -EPERM;
		failure_string = "bad vm_flags";
		goto err_bad_arg;
	}
	vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;

	mutex_lock(&binder_mmap_lock);
	if (proc->buffer) {
		ret = -EBUSY;
		failure_string = "already mapped";
		goto err_already_mapped;
	}

	.....
}

    PS : 内存映射信息放在vma参数中,注意,这里的vma的数据类型是struct vm_area_struct,它表示的是一块连续的虚拟地址空间区域,在函数变量声明的地方,我们还看到有一个类似的结构体struct vm_struct,这个数据结构也是表示一块连续的虚拟地址空间区域,那么,这两者的区别是什么呢?在Linux中,struct vm_area_struct表示的虚拟地址是给用户空间使用的,而struct vm_struct表示的虚拟地址是给内核使用的,它们对应的物理页面都可以是不连续的。struct vm_area_struct表示的地址空间范围是0~3G,而struct vm_struct表示的地址空间范围是(3G + 896M + 8M) ~ 4G。struct vm_struct表示的地址空间范围为什么不是3G~4G呢?原来,3G ~ (3G + 896M)范围的地址是用来映射连续的物理页面的,这个范围的虚拟地址和对应的实际物理地址有着简单的对应关系,即对应0~896M的物理地址空间,而(3G + 896M) ~ (3G + 896M + 8M)是安全保护区域(例如,所有指向这8M地址空间的指针都是非法的),因此struct vm_struct使用(3G + 896M + 8M) ~ 4G地址空间来映射非连续的物理页面。

    这一段代码相当于都是准备工作,    proc的赋值我们在上一篇有过介绍,filp指向文件结构体,其中的private_data是在调用binder_open的时候binder驱动程序赋值的。

    上面说了vma是用户空间的虚拟地址空间,vm_end和vm_start制定了地址区域,然后判断是否大于4m,如果大于则截取。说明Binder驱动程序最多分配4M的虚拟地址空间用于传输进程间通信数据。

    binder要求用户空间的虚拟地址空间不可写,不可拷贝,通过vm_flags来设置。然后检测一下binder_proc是否已经分配了内核空间,如果已经分配了直接返回。

    到此准备工作算是结束了,接下去是真正的分配空间操作

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
	........
    //在内核中分配一段指定大小的虚拟地址空间
	area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
	if (area == NULL) {
		ret = -ENOMEM;
		failure_string = "get_vm_area";
		goto err_get_vm_area_failed;
	}
    //内核虚拟地址空间起始地址赋值给buffer
	proc->buffer = area->addr;
    //用户空间地址和内核空间地址的offset(用于快速计算地址)
	proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
	mutex_unlock(&binder_mmap_lock);
    
    ............
    //为申请物理空间做准备,创建一个数组,用来存储物理页面,保存在proc->pages中。(只创建了数组,还没真正分配物理内存)
    //每一页虚拟地址空间都对应一个物理页面
	proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
	if (proc->pages == NULL) {
		ret = -ENOMEM;
		failure_string = "alloc page array";
		goto err_alloc_pages_failed;
	}
    //初始化内核虚拟地址空间大小
	proc->buffer_size = vma->vm_end - vma->vm_start;

    //设置用户空间虚拟地址打开和关闭的函数
	vma->vm_ops = &binder_vm_ops;
	vma->vm_private_data = proc;

    //为内核的虚拟地址空间请求物理内存,并进行分配
	if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
		ret = -ENOMEM;
		failure_string = "alloc small buf";
		goto err_alloc_small_buf_failed;
	}

    //使用binder_buffer 结构体来描述内核的虚拟地址空间 ,并且将其添加到proc->buffers链表中
    //步骤a
	buffer = proc->buffer;
	INIT_LIST_HEAD(&proc->buffers);
	list_add(&buffer->entry, &proc->buffers);

    //虚拟内存空间为空,所以添加到free_buffers中。
	buffer->free = 1;
	binder_insert_free_buffer(proc, buffer);
    //将最大可用于异步事物的内核虚拟地址空间设置为总的一半
	proc->free_async_space = proc->buffer_size / 2;
	barrier();
	proc->files = get_files_struct(current);
	proc->vma = vma;
	proc->vma_vm_mm = vma->vm_mm;

	/*pr_info("binder_mmap: %d %lx-%lx maps %p\n",
		 proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/
	return 0;

    ............
}

 PS: 步骤a  首先我们知道proc->buffer 的值是 area->addr,也就是内核的虚拟地址空间的起始位置的地址值,将其保存到buffer指针中。&buffer->entry 代表的也是一个在内核虚拟地址空间地址,将其添加到buffers列表中。当前buffers列表中只有一个数据。

    

差不多就是上面这个意思……

    binder_update_page_range的作用是真正的为内核的虚拟地址空间分配物理内存,代码如下

// 参数是 proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma
static int binder_update_page_range(struct binder_proc *proc, int allocate,
				    void *start, void *end,
				    struct vm_area_struct *vma)
{
	void *page_addr;
	unsigned long user_page_addr;
	struct vm_struct tmp_area;
	struct page **page;
	struct mm_struct *mm;

	....

	if (allocate == 0)
		goto free_range;

	....
    //分配物理内存,
	for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
		int ret;
        //依次获取pages数组中的项
		page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];

		BUG_ON(*page);
        //请求物理页
		*page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
		if (*page == NULL) {
			pr_err("%d: binder_alloc_buf failed for page at %p\n",
				proc->pid, page_addr);
			goto err_alloc_page_failed;
		}
        //将物理页映射到内核虚拟地址空间
		tmp_area.addr = page_addr;
		tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */;
		ret = map_vm_area(&tmp_area, PAGE_KERNEL, page);
		if (ret) {
			pr_err("%d: binder_alloc_buf failed to map page at %p in kernel\n",
			       proc->pid, page_addr);
			goto err_map_kernel_failed;
		}
        //获取内核虚拟地址空间对应的用户虚拟地址空间(通过加上user_buffer_offset)
		user_page_addr =
			(uintptr_t)page_addr + proc->user_buffer_offset;
        //将物理页映射到用户虚拟地址空间
		ret = vm_insert_page(vma, user_page_addr, page[0]);
		if (ret) {
			pr_err("%d: binder_alloc_buf failed to map page at %lx in userspace\n",
			       proc->pid, user_page_addr);
			goto err_vm_insert_page_failed;
		}
		/* vm_insert_page does not seem to increment the refcount */
	}
	if (mm) {
		up_write(&mm->mmap_sem);
		mmput(mm);
	}
	return 0;

//回收物理内存
free_range:
	for (page_addr = end - PAGE_SIZE; page_addr >= start;
	     page_addr -= PAGE_SIZE) {
		page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
		if (vma)
			zap_page_range(vma, (uintptr_t)page_addr +
				proc->user_buffer_offset, PAGE_SIZE, NULL);
err_vm_insert_page_failed:
        //解除物理映射
		unmap_kernel_range((unsigned long)page_addr, PAGE_SIZE);
err_map_kernel_failed:
        //释放物理页
		__free_page(*page);
		*page = NULL;
err_alloc_page_failed:
		;
	}
err_no_vma:
	if (mm) {
		up_write(&mm->mmap_sem);
		mmput(mm);
	}
	return -ENOMEM;

}

    这个方法在分配物理内存页和回收物理内存页的时候都会用到,使用第二个参数来控制,0表示释放物理内存,1表示分配物理内存。需要注意的是我们传入的end,我们看到,这里我们只分配了一个page的物理内存!

总结

    所以binder_mmap的主要作用就是为进程间通信的进程分配内核虚拟地址空间,以及为内核虚拟地址空间,用户虚拟地址空间申请物理内存,并建立映射关系

    上面表示进程的虚拟内存,下面表示物理内存,我们看到我们为用户虚拟地址空间和内核虚拟地址空间映射到了同一块物理内存上,这样在binder驱动修改内核地址空间的时候,用户地址空间对应的数据也做了修改,反之亦然!

    个人觉得上图对于Binder进程间通信机制的理解有非常大的作用,绝对大于全文的代码分析,毕竟我们并不会亲自去写这部分代码,理解代码内容才是关键。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!