再学安卓 - binder之驱动函数open和mmap

25 阅读19分钟

12_0.png

本文分析基于Android14(aosp分支android-14.0.0_r28)(内核分支common-android14-6.1)

前言

在我们之前的binder探索中,已遇到过很多通过syscall调用驱动函数的情况,比如:open、ioctl、mmap等。但这些只是它们在用户空间的封装,而binder_xxx是它们在内核中的名字,比如:binder_open、binder_mmap、binder_ioctl等。本篇我们就来分析其中几个重点函数,通过它们来了解binder驱动在内核中所做的操作。

// common/drivers/android/binder.c

const struct file_operations binder_fops = {
	.owner = THIS_MODULE,
	.poll = binder_poll,
	.unlocked_ioctl = binder_ioctl,
	.compat_ioctl = compat_ptr_ioctl,
	.mmap = binder_mmap,
	.open = binder_open,
	.flush = binder_flush,
	.release = binder_release,
};

内核结构体(struct)

内核中定义了众多的结构体(struct)来表示系统中的抽象概念或数据集,这些struct的作用就是通过结构化的方式,描述这些抽象概念和数据集,因此它们也经常被称为xx描述符。而binder驱动运行过程中会涉及以下struct,因此我们先简单了解一下,方便后续学习驱动函数。

进程结构体

task_struct,进程描述符,内核描述一个进程的结构体,即进程相关的所有数据和信息都保存在这个结构体的字段中。因此,该结构体极其庞大且复杂,我们取其中入门级的字段作简要介绍。另外,内核中提供一个全局宏current,它是一个指向当前运行进程的task_struct的指针。有了它,内核开发者就可以随时使用当前进程相关的各种数据和信息。

虽然我们将task_struct称为进程描述符,但实际上内核也会用task_struct结构体来存储线程相关数据和信息。从内核的角度看,线程和进程有区别,但不大。

// common/include/linux/sched.h

struct task_struct {
	// 进程/线程ID
	// 主线程中:pid = tgid; 子线程中:pid ≠ tgid; 主线程和子线程的tgid相等
	pid_t				pid;
	// 线程组ID
	pid_t				tgid;
	
	// 指向当前进程的主线程结构体,即线程组中的领头线程,通常也是当前进程创建的第一个线程
	struct task_struct	*group_leader;
	
	// 指向代表用户空间虚拟内存的结构的指针
	// 进程内所有线程的mm指针都相等,不同进程则mm指针不一样。这就是我们常说的内存空间,进程间隔离,进程内共享。
	struct mm_struct	*mm;
	
	// 与进程关联的文件系统信息
	struct fs_struct	*fs;
	// 指向结构体files_struct,此结构管理着进程打开的所有文件描述符
	// 进程打开的文件和内存一样,也是进程间隔离,进程内共享。
	struct files_struct	*files;
	
	// 进程名,TASK_COMM_LEN = 16,由于字符数组最后一位为终止符Null,因此最大长度为15字节
	char   comm[TASK_COMM_LEN];
};

内存结构体

在学习相关结构体之前,我们需要先了解一些操作系统内存相关的基础知识:寻址空间、用户虚拟内存空间、内核虚拟内存空间及其组织结构。

在32位设备中,通常使用int整型来存放一个内存地址,因此我们可以获得最多2^32个地址(2的32次方)。而每个地址能存放一个字节(byte)的数据,因此逻辑上我们能使用的内存空间大小为:2^32 x 1 (byte) = 4,294,967,296 (byte) = 4,194,304 (KB) = 4096 (MB) = 4(GB)。这个4GB的空间就被叫做寻址空间,32位设备的寻址空间大小就为4GB。

当然,我们这里谈到的空间是逻辑上的概念,通常也叫做虚拟内存空间,它并不是物理内存(现在手机或电脑的物理内存基本都比4GB大)。系统通过页表映射将虚拟内存地址跟物理内存地址关联起来,进程只面向虚拟内存工作,而系统通过MMU(Memory Management Unit)将虚拟地址转换为物理地址。

进程运行所需的虚拟内存空间被分为了2部分,用户空间(3GB)和内核空间(1GB)。用户空间存放进程相关数据,而内核空间仅在进程处于内核态时才能被访问,存储的是内核运行时所需的数据结构。值得注意的是,内核空间是系统中所有进程共用的空间,换句话说就是所有进程处于内核态时面对的都是同样一块内存空间。由此可见,内核空间是相当宝贵的。

以下为进程的虚拟内存布局概况

12_1.png

内核主要通过2个结构体来管理一个进程的用户空间虚拟内存:

  • mm_struct(内存描述符):包括内存映射、页表统计、区域划分、同步保护等数据
  • vm_area_struct(虚拟内存区域描述符),简称vma,代表一块连续的虚拟内存空间。进程中的代码段、数据段、堆、栈、内存映射等都使用此结构来代表。

mm_struct(内存描述符)

// common/include/linux/mm_types.h

struct mm_struct {
	// 用于管理进程所有虚拟内存区域(vma)的数据结构Maple Tree
	// Maple Tree于Linux 6.1版本出现,用于代替之前的rbtree
	struct maple_tree mm_mt;

	// 从用户虚拟内存空间中获取一个未被映射区间的起始地址
	// 注:这里设置的是默认函数,实际do_mmap时可能会根据映射类型执行指定的get_unmapped_area函数
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);

	// 内存映射区的基地址(默认从高地址向低地址增长)
	unsigned long mmap_base;

	// 进程用户空间边界地址(32位系统为0xC0000000)
	unsigned long task_size;

	// 指向Page Global Directory(顶级页表项)
	pgd_t * pgd;

	// 内存映射操作同步锁(比如:mmap、munmap)
	// 写操作需独占锁,读操作可共享
	struct rw_semaphore mmap_lock;

	// start_code, end_code:Code段的起始和结束地址(对应可执行文件的.text段)
	// start_data, end_data:Data段的起始和结束地址(对应.data和.bss段)
	unsigned long start_code, end_code, start_data, end_data;
	// start_brk:堆的起始地址
	// brk:当前堆顶地址
	// start_stack:用户空间栈的起始地址(向下增长)
	unsigned long start_brk, brk, start_stack;
	// arg_start, arg_end:命令行参数(argv)起始和结束地址
	// env_start, env_end:环境变量(envp)起始和结束地址
	unsigned long arg_start, arg_end, env_start, env_end;

	// 指向与进程相关的可执行文件
	struct file __rcu *exe_file;
};

vm_area_struct(虚拟内存区域描述符)

// common/include/linux/mm_types.h

/*
 * This struct describes a virtual memory area. There is one of these
 * per VM-area/task. A VM area is any part of the process virtual memory
 * space that has a special rule for the page-fault handlers (ie a shared
 * library, the executable area etc).
 */
struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */

	// vma的起始和结束虚拟地址
	// 左闭右开区间 [vm_start, vm_end)
	unsigned long vm_start;
	unsigned long vm_end;

	// vma所属的内存描述符(mm_struct)
	struct mm_struct *vm_mm;
	// vma行为属性和访问权限,通过位记录各个标识的值
	// 若想修改标识位,使用vm_flags_{init|reset|set|clear|mod}函数操作
	// 常见的取值及含义请参考附录
	const vm_flags_t vm_flags;
	// vma操作函数表指针,vm_operations_struct存放vma的生命周期hook函数、自定义操作函数等
	// 比如:open、close是vma加入或从进程移除时调用的函数,fault是产生缺页异常时调用的函数
	const struct vm_operations_struct *vm_ops;
	// 若当前为文件映射,则标识映射内容在文件中的起始偏移量,单位是PageSize
	// 比如:vm_pgoff为2,PageSize是4KB,那么偏移量为4096 * 2 = 8192字节
	// 若当前为匿名映射,即无文件关联,那么vm_pgoff = 0
	unsigned long vm_pgoff;
	// 若当前为文件映射,则指向映射的文件对象
	// 若当前为匿名映射,则为NULL
	struct file * vm_file;
};

我们可以通过系统命令查看当前进程的vma情况:cat /proc/<pid>/maps。

12_2.png

其中的每一行都代表一个vma,每一列的含义如下:

  • vm_start, vm_end:区域起始、结束地址

  • vm_flags权限标志:并不包含所有标志位,仅展示以下4位 读权限:r(可读)、-(不可读) 写权限:w(可写)、-(不可写) 执行权限:x(可执行)、-(不可执行) 共享属性:p(私有映射,写时复制)、s(共享映射)

  • 偏移量:16进制偏移量(单位:字节),假设此列数值为offset,那么它与vm_pgoff的换算关系为:offset / page_size = vm_pgoff 比如:若page_size=4KB,那么第四行vma的vm_pgoff = 0x0000b000 / 0x1000 = 0xB = 11(十进制)

  • 设备号:主设备号和次设备号(major:minor),映射文件所在设备的标识符。 若是匿名映射,则为00:00。

  • 文件inode编号(十进制):若是匿名映射,则为0。

  • 文件路径: 若是文件映射,则显示文件路径。 若是匿名映射,则显示[anon:xxxxxxx]。 以及其他特殊名称,比如:[stack]表示用户态栈,[vvar]表示内核与用户共享的只读数据等等。

binder_open

Android系统中所有binder通信进程都需要先通过此函数打开binder设备文件,才能继续后续的操作。而binder_open是在ProcessState实例初始化时调用的。

// common/drivers/android/binder.c

static int binder_open(struct inode *nodp, struct file *filp) {
	// binder_proc_wrap是binder_proc的封装结构体
	// 除了包含binder_proc,还包含dbitmap用于管理Binder资源
	struct binder_proc_wrap *proc_wrap;
	// 从驱动角度看,binder_proc就代表一个binder通信进程
	struct binder_proc *proc, *itr;
	... ...

	// 为binder_proc_wrap申请空间,并将proc_wrap中的指针字段赋给proc变量
	// 但此时proc还没有内容,接下来的代码将会初始化binder_proc中的各个变量
	proc_wrap = kzalloc(sizeof(*proc_wrap), GFP_KERNEL);
	if (proc_wrap == NULL)
		return -ENOMEM;
	proc = &proc_wrap->proc;

	... ...
	// current为内核的全局宏,它是包含当前进程信息的结构体
	// 其中group_leader是当前进程所属线程组的主线程结构体(task_struct)指针
	// 根据binder的上下文情景,下面这行代码其实就是将主线程结构体指针保存到binder_proc->tsk中
	proc->tsk = current->group_leader;
	... ...
	// 初始化待处理的Binder工作项双向链表
	INIT_LIST_HEAD(&proc->todo);
	// 初始化进程冻结时保存被暂停事务的链表
	init_waitqueue_head(&proc->freeze_wait);
	// 设置binder_proc的进程调度策略
	if (binder_supported_policy(current->policy)) {
		proc->default_priority.sched_policy = current->policy;
		proc->default_priority.prio = current->normal_prio;
	} else {
		proc->default_priority.sched_policy = SCHED_NORMAL;
		proc->default_priority.prio = NICE_TO_PRIO(0);
	}
	// 从inode中获取binder设备结构体指针
	if (is_binderfs_device(nodp)) {
		// nodp->i_private中存放的是binder_device指针
		// 它是在binderfs_binder_device_create函数中放入i_private中的
		// binderfs_binder_device_create我们在上一篇文章中提到过
		// 它的主要作用就是创建bidner_device和inode并初始化
		binder_dev = nodp->i_private;
		info = nodp->i_sb->s_fs_info;
		binder_binderfs_dir_entry_proc = info->proc_log_dir;
	} else {
		binder_dev = container_of(filp->private_data,
					  struct binder_device, miscdev);
	}

	// 初始化binder_proc->binder_alloc
	// binder_alloc是代表和管理binder数据缓冲区的结构体
	// 但此时的binder_alloc并未分配虚拟空间,空间的分配需要等到mmap调用时
	binder_alloc_init(&proc->alloc);
	... ...
	proc->pid = current->group_leader->pid;
	... ...
	// 初始化存放休眠binder线程的双向链表
	INIT_LIST_HEAD(&proc->waiting_threads);
	// 将创建好的binder_proc放入file->pricate_data中,后续会取出使用
	filp->private_data = proc;
	... ...
	// 遍历全局的Binder进程列表,标记当前进程是否已注册
	hlist_for_each_entry(itr, &binder_procs, proc_node) {
		if (itr->pid == proc->pid) {
			existing_pid = true;
			break;
		}
	}
	// 将此进程proc_node添加到全局binder进程哈希链表中
	hlist_add_head(&proc->proc_node, &binder_procs);
	... ...
	// 若是新注册的进程,在debugfs中创建进程文件,文件名为pid
	// binder_debugfs_dir_entry_proc即为上一篇中binder_init函数创建的binder/proc目录
	if (binder_debugfs_dir_entry_proc && !existing_pid) {
		... ...
		proc->debugfs_entry = debugfs_create_file(strbuf, 0444,
			binder_debugfs_dir_entry_proc,
			(void *)(unsigned long)proc->pid,
			&proc_fops);
	}
	// 若是新注册的进程,在binderfs中创建进程文件,文件名为pid
	if (binder_binderfs_dir_entry_proc && !existing_pid) {
		... ...
		binderfs_entry = binderfs_create_file(binder_binderfs_dir_entry_proc,
			strbuf, &proc_fops, (void *)(unsigned long)proc->pid);
		... ...
	}
	return 0;
}

综上,binder_open主要工作就是创建binder_proc,将进程相关的数据保存在其中。并将其添加到全局binder进程哈希链表中,最后在debugfs和binderfs中创建进程文件

binder_mmap

ProcessState类调用binder_open打开binder驱动文件之后,会立即通过binder_mmap尝试分配binder通信所需的虚拟内存空间。

// frameworks/native/libs/binder/ProcessState.cpp

ProcessState::ProcessState(const char* driver) {
	// 打开Binder设备文件 
	base::Result<int> opened = open_driver(driver);

	if (opened.ok()) { 
		// 通过系统调用mmap将用户虚拟空间和内核虚拟空间映射到同一块物理内存上 
		// 该内存区域大小为BINDER_VM_SIZE(1M - (内存页大小 * 2)),即Binder通信传输内容的大小限制 
		// Android 14及之前内存页大小都为4K 
		mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, opened.value(), 0);
		... ...
	}
	... ...
}

通过syscall,最终会调用内核函数do_mmap。

// common/mm/mmap.c

unsigned long do_mmap(struct file *file, unsigned long addr, 
					  unsigned long len, unsigned long prot,
			          unsigned long flags, unsigned long pgoff, 
			          unsigned long *populate, struct list_head *uf) {
	// 从当前全局描述符current中取出mm_struct(内存描述符)
	struct mm_struct *mm = current->mm;
	vm_flags_t vm_flags;

	// 根据prot和flags,进行标志位检查和设置
	... ...
	// 若没有设置固定地址标识MAP_FIXED,且addr小于mmap_min_addr,那么将最终地址确定为页对齐后的地址
	if (!(flags & MAP_FIXED))
		addr = round_hint_to_min(addr);

	// 将映射区域长度进行页对齐
	len = PAGE_ALIGN(len);
	... ...
	// 从当前进程的虚拟空间中,找到符合参数条件的一块空间,并返回它的起始地址
	// get_unmapped_area函数中会根据当前参数,选择相应的函数执行查找逻辑
	// 对于binder中的mmap来说,将使用内存描述符(mm_struct)中的默认函数来查找地址
	addr = get_unmapped_area(file, addr, len, pgoff, flags);
	
	// 检查、合并标志位,并统一设置到变量vm_flags中
	... ...

	// 准备就绪,mmap_region函数负责创建虚拟内存区域
	addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
	... ...
	return addr;
}
// common/mm/mmap.c

unsigned long mmap_region(struct file *file, unsigned long addr,
			  unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
			  struct list_head *uf) {
	... ...
	ret = __mmap_region(file, addr, len, vm_flags, pgoff, uf);
	... ...
	return ret;
}

static unsigned long __mmap_region(struct file *file, unsigned long addr,
		unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
		struct list_head *uf) {
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma = NULL;
	struct vm_area_struct *next, *prev, *merge;
	pgoff_t pglen = len >> PAGE_SHIFT;
	unsigned long charged = 0;
	unsigned long end = addr + len;
	unsigned long merge_start = addr, merge_end = end;
	pgoff_t vm_pgoff;
	int error;
	// 以当前内存描述符中的Maple Tree和映射空间的起始结束地址为参数,创建一个对应的结构体ma_state
	// ma_state是操作Maple tree期间保存操作状态的结构体,简称为mas
	MA_STATE(mas, &mm->mm_mt, addr, end - 1);
	... ...
	// mas_next返回mas指定条件区域的下一个vma区域指针
	next = mas_next(&mas, ULONG_MAX);
	// mas_prev返回mas指定条件区域的上一个vma区域指针
	prev = mas_prev(&mas, 0);
	// 根据next和prev判断,mas指定区域是否有可能与next和prev合并到一起
	... ...
	// 若可以,则调用vma_expand进行合并
	if (vma &&
	    !vma_expand(&mas, vma, merge_start, merge_end, vm_pgoff, next)) {
		khugepaged_enter_vma(vma, vm_flags);
		goto expanded;
	}
	... ...
	// 若不能合并,则为新的vma结构体申请空间,并设置起止地址、页偏移、访问权限等属性
	vma = vm_area_alloc(mm);
	if (!vma) {
		error = -ENOMEM;
		goto unacct_error;
	}

	vma->vm_start = addr;
	vma->vm_end = end;
	vm_flags_init(vma, vm_flags);
	vma->vm_page_prot = vm_get_page_prot(vm_flags);
	vma->vm_pgoff = pgoff;
	... ...
	if (file) {
		// 文件映射,将新的vma与映射文件关联
		vma->vm_file = get_file(file);
		// mmap_file函数中会执行 file->f_op->mmap(file, vma)
		// 调用了文件的mmap操作函数,在当前语境下,就是binder_mmap函数
		error = mmap_file(file, vma);
		... ...
	} else if (vm_flags & VM_SHARED) {
		// 共享内存映射,会映射到默认文件dev/zero
		error = shmem_zero_setup(vma);
		... ...
	} else {
		// 匿名映射,不关联任何文件
		vma_set_anonymous(vma);
	}
	... ...
}

以上代码是mmap系统调用的简要流程,主要是根据参数要求为申请方找到合适的虚拟内存空间,并创建vma,加入到MapleTree中方便后续管理。在流程最后,调用了f_op->mmap文件自身的mmap函数,执行驱动文件自定义的行为。因此,接下来我们就看看binder驱动拿到内核分配的vma后做了些什么?

// common/drivers/android/binder.c

static int binder_mmap(struct file *filp, struct vm_area_struct *vma) {
	// 将binder_open函数中放入的binder_proc取出
	struct binder_proc *proc = filp->private_data;

	// 检查一下传入的task_struct是不是当前正在执行的进程
	if (proc->tsk != current->group_leader)
		return -EINVAL;

	... ...
	// 为vma添加了VM_DONTCOPY和VM_MIXEDMAP标识
	// VM_DONTCOPY:fork()时不会复制该vma区域
	// VM_MIXEDMAP:该vma区域支持匿名页和文件页的混合映射
	vm_flags_mod(vma, VM_DONTCOPY | VM_MIXEDMAP, VM_MAYWRITE);

	// 设置vma的操作函数表
	vma->vm_ops = &binder_vm_ops;
	// 设置vam与binder_proc的关联
	vma->vm_private_data = proc;

	return binder_alloc_mmap_handler(&proc->alloc, vma);
}

接下来的binder_alloc_mmap_handler函数将着重构建binder_allocbinder_buffer,这是两个重要的结构体,让我们先看看它的定义。

binder_alloc

// common/drivers/android/binder_alloc.h

// 代表Binder数据缓冲区的描述符
struct binder_alloc {
	struct mutex mutex;
	struct vm_area_struct *vma;  // binder数据缓冲区的虚拟空间描述符
	struct mm_struct *mm;  // binder进程的内存描述符
	void __user *buffer;   // binder数据缓冲区虚拟空间起始地址,等于vma->vm_start
	struct list_head buffers;  // 当前进程所有binder_buffer的链表表头,按地址顺序存放
	struct rb_root free_buffers;  // 空闲buffer的红黑树
	struct rb_root allocated_buffers;  // 已分配buffer的红黑树
	size_t free_async_space;  // 异步通信(oneway)空间大小
	struct binder_lru_page *pages;  // 物理页的LRU链表
	size_t buffer_size;  // binder数据缓冲区大小
	int pid;
	size_t pages_high;
	bool oneway_spam_detected;  // 是否开启单向调用(Oneway)滥用检测
};

binder_buffer

// common/drivers/android/binder_alloc.h

// 代表一次Binder通信所需的数据缓冲区
struct binder_buffer {
	struct list_head entry; // 链表节点,插入到binder_alloc的buffers链表中
	// 红黑树节点
	// 若是空闲缓冲区,按大小,插入binder_alloc的free_buffers中,
	// 若是已分配缓冲区,按地址,插入binder_alloc的allocated_buffers中
	struct rb_node rb_node;
	unsigned free:1;  // 1:空闲,可被分配,0:已分配,正在使用中
	unsigned clear_on_free:1;  // 释放缓冲区时是否需要清零
	unsigned allow_user_free:1;  // 是否允许用户空间主动释放该缓冲区
	unsigned async_transaction:1;  // 该缓冲区是否用于异步事务
	unsigned oneway_spam_suspect:1;  // 该缓冲区可能涉及单向调用(Oneway)滥用
	unsigned debug_id:27;  // 调试标识符,用于跟踪缓冲区的分配和释放
	struct binder_transaction *transaction;  //指向与该缓冲区关联的Binder事务
	struct binder_node *target_node;  //指向该缓冲区要发送到的目标Binder节点(server端)
	size_t data_size;  // 缓冲区中有效数据的实际大小
	size_t offsets_size;  // 缓冲区中偏移表(offsets table)的大小
	size_t extra_buffers_size;  // 额外缓冲区大小
	void __user *user_data;  // 指向用户空间的数据地址
	int pid;  // 该缓冲区所属进程ID
};

来看看函数中是怎么构建这两个结构体的。

// common/drivers/android/binder_alloc.c

int binder_alloc_mmap_handler(struct binder_alloc *alloc,
			      struct vm_area_struct *vma) {
	struct binder_buffer *buffer;
	const char *failure_string;
	int ret, i;

	... ...
	// 取vma大小和4M(SZ_4M)中较小的值
	// 也就是说内核只允许最大4M的Binder缓冲区
	// 但实际情况是ProcessState只会申请BINDER_VM_SIZE(1M - (内存页大小 * 2))
	alloc->buffer_size = min_t(unsigned long, vma->vm_end - vma->vm_start,
				   SZ_4M);
	alloc->buffer = (void __user *)vma->vm_start;

	// 为binder_lru_page数组申请空间,数组的每一个元素都是binder_lru_page结构体,对应着一个物理页
	// 完成之后pages指向数组第一个元素,且可以通过pages+下标访问数组其他元素
	alloc->pages = kcalloc(alloc->buffer_size / PAGE_SIZE,
			       sizeof(alloc->pages[0]),
			       GFP_KERNEL);
	... ...
	for (i = 0; i < alloc->buffer_size / PAGE_SIZE; i++) {
		alloc->pages[i].alloc = alloc;
		// 初始化lru为一个空节点,待后续使用
		INIT_LIST_HEAD(&alloc->pages[i].lru);
	}

	// 申请一个binder_buffer的空间
	buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
	... ...
	// 将缓冲区用户空间起始地址赋给binder_buffer
	buffer->user_data = alloc->buffer;
	// 将binder_buffer添加到binder_alloc->buffers链表中
	list_add(&buffer->entry, &alloc->buffers);
	buffer->free = 1;
	// 将binder_buffer添加到binder_alloc->free_buffers红黑树中
	binder_insert_free_buffer(alloc, buffer);
	// 将异步通信空间设置为整个缓冲区的一半
	alloc->free_async_space = alloc->buffer_size / 2;

	// 完成binder_alloc,且已有一个binder_buffer待使用
	binder_alloc_set_vma(alloc, vma);

	return 0;
}

综上,从用户空间调用mmap开始,就开启了Binder初始化数据缓冲区的流程,它大致分为两段:标准流程段和驱动流程段。

  • 标准流程段:通过current->mm中的信息初始化一个用户虚拟内存空间vma。
  • 驱动流程段:通过在file_operations中注册的操作函数mmap执行Binder驱动自身的逻辑,即创建binder_alloc和binder_buffer。

值得注意的是,直到binder_mmap结束我们仍未看到实际的物理内存与任何结构建立关联,即便是binder_alloc中的pages字段,也仅仅是创建了代表物理页的结构体,并没有实际物理页与之对应。这个过程就好像内核给用户空间开了一张支票,在没有兑现之前,只是一个凭据。什么时候兑现?答案是这个缓冲区需要写入数据时。

12_3.png

总结

本篇我们介绍了binder_open和binder_mmap两个驱动函数,要理解它们需要我们具备一定的Linux系统基础知识(进程线程、文件系统、内存管理方面)。总体来说,这两个函数都属于Binder初始化阶段函数。Binder驱动中最重要的核心函数binder_ioctl由于内容较多,我们放到下一篇,敬请期待!

附录

vm_area_struct中vm_flags常见取值及含义

标志值(十六进制)含义
VM_READ0x00000001可读(对应 PROT_READ
VM_WRITE0x00000002可写(对应 PROT_WRITE
VM_EXEC0x00000004可执行(对应 PROT_EXEC
VM_SHARED0x00000008共享映射
VM_PRIVATE0x00000010私有映射
VM_GROWSDOWN0x00000100内存增长方向向下
VM_GROWSUP0x00000200内存增长方向向上
VM_DONTCOPY0x00020000禁止fork()时复制该区域