一、Binder概述
Android应用程序是由Activity、Service、Broadcast Receiver和Content Provider四种类型的的组件构成,它们有可能运行在同一个进程中,也有可能运行在不同的进程中,这些进程间的通信就依赖于 Binder IPC 机制。另外,各种系统组件也运行在独立的进程中,如Camera,我们的App经常会有调起Camera的需求。那么,这些运行在不同进程中的应用程序组件和系统组件是怎么通信的呢? 答案就是使用Binder, 那么Binder是什么呢? Binder 是一种进程间通信机制,基于开源的 OpenBinder 实现;OpenBinder 起初由 Be Inc. 开发,后由 Plam Inc. 接手。从字面上来解释 Binder 有胶水、粘合剂的意思,顾名思义就是粘和不同的进程,使之实现通信。
对于Android系统来说,Binder机制十分重要,因为Android系统基本上可以看作是一个基于Binder通信的C/S架构。Binder就像网络一样,把系统的各个部分连接在了一起,实现Android系统中进程间的通信。
二、为什么使用Binder?
Android系统是基于Linux内核开发的。Linux内核提供了丰富的IPC(Inter-Process Communication,进程间通信)机制,如共享内存,socket等。但是,Android系统却没有采用这些传统的进程间的通信机制,而是自己开发了一套新的进程间通信机制---Binder。为什么呢? 下面就来详细的对比一下,传统IPC通信机制和Binder机制的区别。
2.1 内存划分
先来看Linux系统的内存划分,在Linux系统中,内存被操作系统划分成两块,用户空间和内核空间。用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方。为了安全,它们是隔离的,即使用户程序崩溃了,内核也不会受到影响。
2.1.1. 进程隔离
Linux中,进程与进程间的内存是不共享的,A进程无法直接访问B进程的数据。这就是进程隔离,如果A进程和B进程之间要进行数据交互,就需要采用特殊的通信机制:IPC通信。
2.1.2. 用户空间(User Space)/内核空间(Kernel Space)
现在操作系统都是采用的虚拟存储器,对于 32 位系统而言,它的寻址空间(虚拟存储空间)就是 2 的 32 次方,也就是 4GB。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备的权限。为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间(User Space)和内核空间(Kernel Space)。针对 Linux 操作系统而言,将最高的 1GB 字节供内核使用,称为内核空间;较低的 3GB 字节供各进程使用,称为用户空间。
对于64位系统来说,低位:0-47位才是有效的可变地址(寻址空间256T),高位:48-63位全补0或全补1.一般高位全补0对应的地址空间是用户空间,高位全补1对应的是内核空间。
简单的说,内核空间(Kernel)是系统内核运行的空间,用户空间(User Space)是用户程序运行的空间。为了保证安全性,它们之间是隔离的。
2.1.3 用户态和内核态
虽然从逻辑上进行了用户空间和内核空间的划分,但不可避免的用户空间需要访问内核资源,比如文件操作,访问网络等。为了突破隔离限制,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。
Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核站。 当进程在执行用户自己的代码的时候,我们称其处在用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。
系统调用主要通过如下两个函数来实现:
copy_from_user()
copy_to_user()
2.2 传统的IPC传输数据
通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。
传统的IPC传输数据,会经过四步:
- 发送数据
- 通过系统调用copy_from_user()将数据从用户空间copy到内核空间
- 通过系统调用copy_to_user()将数据从内核空间copy到用户空间
- 接收数据
如下图所示:
这样的传输方式有两个问题
- 性能低下,一次数据传递需要经历:内存缓存区 -> 内核缓存区 -> 内存缓存区,需要进行两次拷贝。
- 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,浪费时间又浪费空间。
2.3 Binder传输数据
Binder传输数据和传统的IPC传输数据有什么不同呢? 我们先从Binder原理看起。只有搞清楚了Binder机制是怎么通信的,才能知道,为什么Android系统选择了Binder而不是普通的IPC机制。
2.3.1 动态内核可加载模块
正如前面所说,跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。
2.3.2 内存映射
那么在 Android 系统中用户进程之间是如何通过这个内核模块(Binder 驱动)来实现通信的呢?是通过内存映射来实现的。 Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。 Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。
如下图所示:
对文件进行mmap,会在进程的虚拟内存分配地址空间,创建映射关系。实现这样的映射关系后,就可以采用指针的方式读写操作这一段内存,而系统会自动回写到对应的文件磁盘上。
Binder通过内存映射,减少了数据拷贝的次数,两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。
2.3.3 Binder IPC 通信过程
一次完整的Binder IPC通信过程通常是这样的:
- 首先Binder驱动在内核空间创建一个数据接收缓存区
- 接着在内核空间开辟出一块内核缓存区,建立内核缓存区和内核数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系
- 发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
如下图所示:
看到这里,应该可以理解了,Binder传输只需要拷贝一次数据,性能上优于传统IPC传输数据。
| IPC方式 | 数据拷贝次数 |
|---|---|
| Binder | 1 |
| Socket/管道/消息队列 | 2 |
另外从稳定性和安全性来说,Binder IPC也是明显优于其他IPC的,从稳定性上来说,Binder 基于 C/S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。,而从安全性上来说,传统的 IPC 没有任何安全措施,完全依赖上层协议来确保。首先传统的 IPC 接收方无法获得对方可靠的进程用户ID/进程ID(UID/PID),从而无法鉴别对方身份。Binder 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志。传统的 IPC 只能由用户在数据包中填入 UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标识只有由 IPC 机制在内核中添加。其次传统的 IPC 访问接入点是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。
因此,Android选择了Binder IPC来作为进程间的通信机制。
三、 Binder通信的具体过程
Binder进程间通信机制,是在OpenBinder的基础上实现的,它采用CS通信方式,其中,提供服务的进程称为Server进程,而访问服务的进程称为Client进程。Server进程和Client进程的通信要依靠运行在内核空间的Binder驱动程序来进行。Binder驱动程序想用户空间暴露了一个设备文件/dev/binder,使用应用程序进程可以间接地通过它来建立通信通道。Service组件在启动时,会将自己注册到一个Service Manager组件中,以便Client组件可以通过Service Manager组件找到它。 以上描述的Binder进程间通信机制中涉及了Client、Service、Service Manager和Binder驱动程序四个角色,它们的关系如图所示:
由上图可以看出,Client、Service和Service Manager运行在用户空间,而Binder驱动程序运行在内核空间,其中,Service Manager和Binder驱动程序由系统负责提供,而Client和Service组件由应用程序来实现。Client、Service和Service Manager均是通过系统调用open、mmap和ioctl来访问设备文件dev/binder,从而实现与Binder驱动程序的交互,而交互的目的就是为了能够间接地执行进程间通信。
3.1 Kernal Space中Binder驱动源码分析
3.3.1 Binder设备的初始化过程
Binder设备的初始化过程是在Binder驱动程序的初始化函数binder_init中进行的,它的源代码: kernel\drivers\staging\android\binder.c
// 启动驱动
static int __init binder_init(void)
{
int ret;
char *device_name, *device_names;
struct binder_device *device;
struct hlist_node *tmp;
// 创建单线程的binder队列
binder_deferred_workqueue = create_singlethread_workqueue("binder");
if (!binder_deferred_workqueue)
return -ENOMEM;
binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
if (binder_debugfs_dir_entry_root)
binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",
binder_debugfs_dir_entry_root);
if (binder_debugfs_dir_entry_root) {
debugfs_create_file("state",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_state_fops);
debugfs_create_file("stats",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_stats_fops);
debugfs_create_file("transactions",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_transactions_fops);
debugfs_create_file("transaction_log",
S_IRUGO,
binder_debugfs_dir_entry_root,
&binder_transaction_log,
&binder_transaction_log_fops);
debugfs_create_file("failed_transaction_log",
S_IRUGO,
binder_debugfs_dir_entry_root,
&binder_transaction_log_failed,
&binder_transaction_log_fops);
}
/*
* Copy the module_parameter string, because we don't want to
* tokenize it in-place.
*/
device_names = kzalloc(strlen(binder_devices_param) + 1, GFP_KERNEL);
if (!device_names) {
ret = -ENOMEM;
goto err_alloc_device_names_failed;
}
// 从配置文件中读取binder信息 拷贝到 device_names 读取Kconfig文件中的 ANDROID_BINDER_DEVICES
strcpy(device_names, binder_devices_param);
while ((device_name = strsep(&device_names, ","))) {
// 初始化binder设备 源码在下面
ret = init_binder_device(device_name);
if (ret)
goto err_init_binder_device_failed;
}
return ret;
err_init_binder_device_failed:
hlist_for_each_entry_safe(device, tmp, &binder_devices, hlist) {
misc_deregister(&device->miscdev);
hlist_del(&device->hlist);
kfree(device);
}
err_alloc_device_names_failed:
debugfs_remove_recursive(binder_debugfs_dir_entry_root);
destroy_workqueue(binder_deferred_workqueue);
return ret;
}
static int __init init_binder_device(const char *name)
{
int ret;
struct binder_device *binder_device;
// 开辟内存 拿到Binder设备
binder_device = kzalloc(sizeof(*binder_device), GFP_KERNEL);
if (!binder_device)
return -ENOMEM;
// 对设备进行初始化
binder_device->miscdev.fops = &binder_fops; // 设备的文件操作结构 这是file_operations结构
binder_device->miscdev.minor = MISC_DYNAMIC_MINOR; // 次设备号 动态分配
binder_device->miscdev.name = name; // 设备名 “binder”
binder_device->context.binder_context_mgr_uid = INVALID_UID;
binder_device->context.name = name;
// misc 驱动注册
ret = misc_register(&binder_device->miscdev);
if (ret < 0) {
kfree(binder_device);
return ret;
}
// 把binder设备添加到 链表 中 将hlist节点添加到binder_devices为表头的设备链表
hlist_add_head(&binder_device->hlist, &binder_devices);
return ret;
}
从上面的源码总结,初始化Binder的主要步骤是:
- 首先通过调用device_initcall(binder_init);调用binder_init方法,来初始化Binder驱动。
- 使用binder_device = kzalloc(sizeof(*binder_device), GFP_KERNEL);为Binder设备分配内存
- 初始化设备
- 通过hlist_add_head(&binder_device->hlist, &binder_devices);把Binder设备添加到binder_devices为表头的设备链表
另外:
binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
if (binder_debugfs_dir_entry_root)
binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",
binder_debugfs_dir_entry_root);
if (binder_debugfs_dir_entry_root) {
debugfs_create_file("state",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_state_fops);
debugfs_create_file("stats",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_stats_fops);
debugfs_create_file("transactions",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_transactions_fops);
debugfs_create_file("transaction_log",
S_IRUGO,
binder_debugfs_dir_entry_root,
&binder_transaction_log,
&binder_transaction_log_fops);
debugfs_create_file("failed_transaction_log",
S_IRUGO,
binder_debugfs_dir_entry_root,
&binder_transaction_log_failed,
&binder_transaction_log_fops);
}
这几行代码,在目标设备上创建了一个/proc/binder/proc目录,每一个使用了Binder进程间通信机制的进程在该目录下都对应有一个文件,这些文件是以进程ID来命名的,通过它们就可以读取到各个进程的Binder线程池、Binder实体对象、Binder引用对象以及内核缓冲区等信息。接下来的代码,if语句块又在/proc/binder目录下创建了五个文件state、stats、transactions、transaction_log和failed_transaction_log,通过这五个文件就可以读取到Binder驱动程序的运行状况。例如:各个命令协议(BinderDriverCommandProtocol)和返回协议(BinderDriverReturnProtocol)的请求次数、日志记录信息,以及正在执行进程间通信过程的进程信息等。
ret = misc_register(&binder_device->miscdev);
这句代码调用函数misc_register来创建一个Binder设备。misc_register是用来创建一个misc类型的设备,misc类型的设备是Linux中主设备号为10的驱动设备,它并不是硬件,而是一种字符设备,它的注册和使用比较简单。Binder设备就是使用的misc类型设备。
// native层 调用 驱动层 要经过一个syscall层 就相当于native层和Java层的 JNI层
static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = binder_ioctl,
.mmap = binder_mmap, // 映射关系 左右是native层mmap 映射到右边的驱动层的binder_mmap
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};
上面的代码的意思是: Binder驱动程序在目标设备上创建了一个Binder设备文件/dev/binder,这个设备文件的操作方法列表是由全局变量binder_fops指定的。全局变量binder_fops为Binder设备/dev/binder指定文件打开、内存映射和IO控制函数分别为binder_open、binder_mmap和binder_ioctl。
3.3.2 Binder设备文件的打开过程。
一个进程在使用Binder进程间通信机制之前,首先要调用函数open打开设备文件/dev/binder来获得一个文件描述符,然后才能通过这个文件描述符来和Binder驱动程序交互,继而和其他进程执行Binder进程间通信。 当进程调用函数open打开设备文件dev/binder时,Binder驱动程序中的函数binder_open就会被调用。
static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc;
struct binder_device *binder_dev;
binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n",
current->group_leader->pid, current->pid);
// 初始化一个结构体
proc = kzalloc(sizeof(*proc), GFP_KERNEL);
if (proc == NULL)
return -ENOMEM;
// 下面都是对proc这个结构体进行初始化
// 获取当前线程的任务栈
get_task_struct(current);
// 赋值到proc结构体中
proc->tsk = current;
// todo 目标任务
INIT_LIST_HEAD(&proc->todo);
// wait 等待任务
init_waitqueue_head(&proc->wait);
proc->default_priority = task_nice(current);// 进程优先级
binder_dev = container_of(filp->private_data, struct binder_device,
miscdev);
proc->context = &binder_dev->context;
binder_lock(__func__);
binder_stats_created(BINDER_STAT_PROC);
// 把proc 添加到链表中
hlist_add_head(&proc->proc_node, &binder_procs);
proc->pid = current->group_leader->pid; // 进程组ID
INIT_LIST_HEAD(&proc->delivered_death);
// 把proc 放入 filp 的 private_data 中 后面binder就是去filp 的 private_data中去拿proc
// 当前的进程信息 都是保存在proc中
filp->private_data = proc;
binder_unlock(__func__);
if (binder_debugfs_dir_entry_proc) {
char strbuf[11];
snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);
proc->debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,
binder_debugfs_dir_entry_proc,
(void *)(unsigned long)proc->pid,
&binder_proc_fops);
}
return 0;
}
binder_open的流程大致是:
- 创建binder_proc结构体
- 初始化binder_proc结构体
- 把proc结构体放入filp->private_data中
- 把proc 添加到binder_procs链表中
创建结构体和初始化结构体,在上面的代码注释中已经描述得很清楚了。第四步把proc添加到一个全局hash队列binder_procs中。Binder驱动程序将所有打开了设备文件/dev/binder的进程都加入到全局hash队列binder_procs中,因此,通过遍历这个队列就可以知道系统中有多少个进程在使用Binder进程间通信。
filp->private_data = proc;
这句代码将初始化完成之后的结构体proc保存在参数filp的成员变量private_data中。当进程后面通过调用mmap和ioctl时,Binder驱动程序可以根据这个private_data来获取这个proc。
3.3.3 Binder设备文件的内存映射过程。
进程打开了设备文件dev/binder之后,还必须要调用函数binder_mmap把这个设备文件映射到进程的地址,然后才可以使用Binder进程间通信机制。
/**
struct vm_area_struct *vma 进程的虚拟内存 不能超过4M 应用层最大不能超过1M-8k
*/
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; // 通过filp->private_data 拿到 proc
const char *failure_string;
struct binder_buffer *buffer;
if (proc->tsk != current)
return -EINVAL;
// wma 虚拟内存不能超过4M
if ((vma->vm_end - vma->vm_start) > SZ_4M) // 判断虚拟内存是否超过4M
vma->vm_end = vma->vm_start + SZ_4M;// 如果超过4M 就截断为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;
}
// 获取虚拟内存的大小 : vma->vm_end - vma->vm_start
// 在进程的内核地址空间中分配一段大小为vma->vm_end - vma->vm_start的空间,如果分配成功,就把它的起始地址以及大小保存在proc->buffer和proc->buffer_size中。
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;
}
// 将proc中的buffer指针指向 内核虚拟空间
proc->buffer = area->addr;
/**
** 计算要映射的用户空间起始地址与前面获得的内核空间起始地址的差值,并且保存在proc->user_buffer_offset中。
** Binder驱动程序为进程分配的内核缓冲区有两个地址,其中一个是用户空间地址,由vm_area_struct来描述,进程通过这块地址来访问内核缓冲区的内容。 另一个是内核空间地址,由vm_struct来描述,Binder驱动程序通过内核空间地址来访问这块内核缓冲区的内容。
** 这两块空间是连续的,因此只要知道其中一个的地址,就可以方便的算出另外一个地址
*/
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
mutex_unlock(&binder_mmap_lock);
#ifdef CONFIG_CPU_CACHE_VIPT
if (cache_is_vipt_aliasing()) {
while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {
pr_info("binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
vma->vm_start += PAGE_SIZE;
}
}
#endif
/**
** 分配内核缓冲区
** 为进程要映射的虚拟地址空间vma和area分配物理页面了
** 这里首先创建了一个物理页面结构体指针数组,大小为(vma->vm_end - vma->vm_start) / PAGE_SIZE。
** 并将该数组的地址保存在proc->pages中。
** 在Linux中 一个物理页面的大小为PAGE_SIZE,PAGE_SIZE是一个宏,一般定义为4K。
*/
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;
/**
** 调用binder_update_page_range来为虚拟地址空间area分配一个物理页面,
** 对应的内核地址空间为proc->buffer~proc->buffer + PAGE_SIZE
*/
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;
}
// 物理页面分配完毕之后,就用结构体buffer来描述它
buffer = proc->buffer;
// 将物理页面放入进程结构体proc内核缓冲区列表buffers中
INIT_LIST_HEAD(&proc->buffers);
// 把buffer放入链表
list_add(&buffer->entry, &proc->buffers);
buffer->free = 1;
// 将buffer加入到进程结构体proc的空闲内核缓冲区红黑树free_buffers中
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;
err_alloc_small_buf_failed:
kfree(proc->pages);
proc->pages = NULL;
err_alloc_pages_failed:
mutex_lock(&binder_mmap_lock);
vfree(proc->buffer);
proc->buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
mutex_unlock(&binder_mmap_lock);
err_bad_arg:
pr_err("binder_mmap: %d %lx-%lx %s failed %d\n",
proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
return ret;
}
参数vma指向一个结构体vm_area_struct,变量area指向的是一个结构体vm_struct,这两个结构体都是用来描述一段虚拟内存空间的,首先,这两个结构体描述的虚拟内存空间是连续的。在Linux内核中,一个进程可以占用的虚拟地址空间是4G,其中,0-3G是用户地址空间,由结构体vm_area_struct描述,3G-4G是内核地址空间,由结构体vm_struct描述。 参数filp执行一个打开文件结构体,它的成员变量private_data指向的是一个进程结构体binder_proc,它在binder_open中创建,struct binder_proc *proc = filp->private_data; 这句代码通过filp->private_data 拿到 proc,并保存在变量proc中。vma的成员变量vm_end和vm_start指定了要映射的用户地址空间范围,if语句判断它是否超过4M,如果超过,那么将它截断为4M。从这里可以看出,Binder驱动程序最多可以为进程分配4M内核缓冲区来传输进程间通信数据。 接下来的代码:
// 检查进程要映射的用户地址空间是否可写
if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
ret = -EPERM;
failure_string = "bad vm_flags";
goto err_bad_arg;
}
这些代码是检查进程要映射的用户地址空间是否可写,其中FORBIDDEN_MMAP_FLAGS是个宏,定义如下:
#define FORBIDDEN_MMAP_FLAGS (VM_WRITE)
Binder驱动程序为进程分配的内核缓冲区在用户空间只可以读,不可以写,因此,如果进程指定要映射的用户地址空间可以写,“goto err_bad_arg;”这句代码就出错返回了。
vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
进程指定要映射的用户地址空间除了不可以写之外,也不可以复制,以及禁止设置可能会执行写操作标志位的,因此这句代码就将参数vma的成员变量VM_DONTCOPY位设置位1,并且将VM_MAYWRITE位设置为0。
if (proc->buffer)
{
ret = -EBUSY;
failure_string = "already mapped";
goto err_already_mapped;
}
这里的if判断语句判断进程是否重复调用函数mmap来映射设备文件dev/binder,即结构体proc的成员变量buffer是否已经指向一块内核缓冲区。如果是,“goto err_already_mapped;”就出错返回了。
area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
// 指定这段空间的打开和关闭函数为binder_vma_open和binder_vma_close
vma->vm_ops = &binder_vm_ops;
这句代码调用get_vm_area函数,在进程的内核地址空间中分配一段空间,指定这段空间的打开和关闭函数为binder_vma_open和binder_vma_close。
static struct vm_operations_struct binder_vm_ops = {
.open = binder_vma_open,
.close = binder_vma_close,
.fault = binder_vm_fault,
};
四、内核缓冲区管理
开始的时候,Binder驱动程序只为进程分配了一个页面的物理内存,
Binder通信的具体过程:
- 首先,一个进程使用 BINDER_SET_CONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
- Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
- Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
如下图所示:
到这里,我们就学习完了Binder通信的原理和过程。