Binder 的实现是比较复杂的,想要完全弄明白是怎么一回事,并不是一件容易的事情。
这里面牵涉到好几个层次,每一层都有一些模块和机制需要理解。
这部分内容预计会分为三篇文章来讲解。
本文是第一篇,会对整个 Binder 机制做一个架构性的讲解,然后会将大部分精力用来讲解 Binder 机制中最核心的部分:Binder 驱动的实现。
Binder 源自 Be Inc 公司开发的 OpenBinder 框架,后来该框架转移的 Palm Inc,由 Dianne Hackborn 主导开发。OpenBinder 的内核部分已经合入 Linux Kernel 3.19。
Android Binder 是在 OpneBinder 上的定制实现。原先的 OpenBinder 框架现在已经不再继续开发,可以说 Android 上的 Binder 让原先的 OpneBinder 得到了重生。
Binder 是 Android 系统中大量使用的 IPC(Inter-process communication,进程间通讯)机制。无论是应用程序对系统服务的请求,还是应用程序自身提供对外服务,都需要使用到 Binder。
因此,Binder 机制在 Android 系统中的地位非常重要,可以说, 理解 Binder 是理解 Android 系统的绝对必要前提。
在 Unix/Linux 环境下,传统的 IPC 机制包括:
等。
由于篇幅所限,本文不会对这些 IPC 机制做讲解,有兴趣的读者可以参阅《UNIX 网络编程 卷 2:进程间通信》。
Android 系统中对于传统的 IPC 使用较少(但也有使用,例如:在请求 Zygote fork 进程的时候使用的是 Socket IPC),大部分场景下使用的 IPC 都是 Binder。
Binder 相较于传统 IPC 来说更适合于 Android 系统,具体原因的包括如下三点:
Binder 整体架构如下所示:
从图中可以看出,Binder 的实现分为这么几层:
驱动层位于 Linux 内核中,它提供了最底层的数据传递,对象标识,线程管理,调用过程控制等功能。 驱动层是整个 Binder 机制的核心 。
Framework 层以驱动层为基础,提供了应用开发的基础设施。
Framework 层既包含了 C++ 部分的实现,也包含了 Java 部分的实现。为了能将 C++ 的实现复用到 Java 端,中间通过 JNI 进行衔接。
开发者可以在 Framework 之上利用 Binder 提供的机制来进行具体的业务逻辑开发。其实不仅仅是第三方开发者,Android 系统中本身也包含了很多系统服务都是基于 Binder 框架开发的。
既然是 "进程间" 通讯就至少牵涉到两个进程,Binder 框架是典型的 C/S 架构。在下文中中,我们把服务的请求方称之为 Client,服务的实现方称之为 Server。
Client 对于 Server 的请求会经由 Binder 框架由上至下传递到内核的 Binder 驱动中,请求中包含了 Client 将要调用的命令和参数。请求到了 Binder 驱动之后,在确定了服务的提供方之后,会再从下至上将请求传递给具体的服务。整个调用过程如下图所示:
对网络协议有所了解的读者会发现,这个数据的传递过程和网络协议是如此的相似。
前面已经提到,使用 Binder 框架的既包括系统服务,也包括第三方应用。因此,在同一时刻,系统中会有大量的 Server 同时存在。那么,Client 在请求 Server 的时候,是如果确定请求发送给哪一个 Server 的呢?
这个问题,就和我们现实生活中如何找到一个公司 / 商场,如何确定一个人 / 一辆车一样,解决的方法就是:每个目标对象都需要一个唯一的标识。并且,需要有一个组织来管理这个唯一的标识。
而 Binder 框架中负责管理这个标识的就是 ServiceManager。ServiceManager 对于 Binder Server 的管理就好比车管所对于车牌号码的的管理,派出所对于身份证号码的管理:每个公开对外提供服务的 Server 都需要注册到 ServiceManager 中(通过 addService),注册的时候需要指定一个唯一的 id(这个 id 其实就是一个字符串)。
Client 要对 Server 发出请求,就必须知道服务端的 id。Client 需要先根据 Server 的 id 通过 ServerManager 拿到 Server 的标示(通过 getService),然后通过这个标示与 Server 进行通信。
整个过程如下图所示:
如果上面这些介绍已经让你一头雾水,请不要过分担心,下面会详细讲解这其中的细节。
下文会以自下而上的方式来讲解 Binder 框架。自下而上未必是最好的方法,每个人的思考方式不一样,如果你更喜欢自上而下的理解,你也按这样的顺序来阅读。
对于大部分人来说,我们可能需要反复的查阅才能完全理解。
源码路径(这部分代码不在 AOSP 中,而是位于 Linux 内核代码中):
- /kernel/drivers/android/binder.c
- /kernel/include/uapi/linux/android/binder.h
或者
- /kernel/drivers/staging/android/binder.c
- /kernel/drivers/staging/android/uapi/binder.h
Binder 机制的实现中,最核心的就是 Binder 驱动。 Binder 是一个 miscellaneous 类型的驱动,其本身不对应任何硬件,所有的操作都在软件层。
函数负责 Binder 驱动的初始化工作,该函数中大部分代码是在通过
- binder_init
和
- debugfs_create_dir
函数创建 debugfs 对应的文件。 如果内核在编译时打开了 debugfs,则通过
- debugfs_create_file
连上设备之后,可以在设备的这个路径找到 debugfs 对应的文件:
- adb shell
。Binder 驱动中创建的 debug 文件如下所示:
- /sys/kernel/debug
- # ls -l /sys/kernel/debug/binder/
- total 0
- -r--r--r-- 1 root root 0 1970-01-01 00:00 failed_transaction_log
- drwxr-xr-x 2 root root 0 1970-05-09 01:19 proc
- -r--r--r-- 1 root root 0 1970-01-01 00:00 state
- -r--r--r-- 1 root root 0 1970-01-01 00:00 stats
- -r--r--r-- 1 root root 0 1970-01-01 00:00 transaction_log
- -r--r--r-- 1 root root 0 1970-01-01 00:00 transactions
这些文件其实都在内存中的,实时的反应了当前 Binder 的使用情况,在实际的开发过程中,这些信息可以帮忙分析问题。例如,可以通过查看
目录来确定哪些进程正在使用 Binder,通过查看
- /sys/kernel/debug/binder/proc
和
- transaction_log
文件来确定 Binder 通信的数据。
- transactions
函数中最主要的工作其实下面这行:
- binder_init
ret = misc_register(&binder_miscdev);
该行代码真正向内核中注册了 Binder 设备。
的定义如下:
- binder_miscdev
- static struct miscdevice binder_miscdev = {
- .minor = MISC_DYNAMIC_MINOR,
- .name = "binder",
- .fops = &binder_fops
- };
这里指定了 Binder 设备的名称是 "binder"。这样,在用户空间便可以通过对 / dev/binder 文件进行操作来使用 Binder。
binder_miscdev 同时也指定了该设备的 fops。fops 是另外一个结构体,这个结构中包含了一系列的函数指针,其定义如下:
- static const struct file_operations binder_fops = {
- .owner = THIS_MODULE,
- .poll = binder_poll,
- .unlocked_ioctl = binder_ioctl,
- .compat_ioctl = binder_ioctl,
- .mmap = binder_mmap,
- .open = binder_open,
- .flush = binder_flush,
- .release = binder_release,
- };
这里除了
之外,每一个字段都是一个函数指针,这些函数指针对应了用户空间在使用 Binder 设备时的操作。例如:
- owner
对应了
- binder_poll
系统调用的处理,
- poll
对应了
- binder_mmap
系统调用的处理,其他类同。
- mmap
这其中,有三个函数尤为重要,它们是:
,
- binder_open
和
- binder_mmap
。 这是因为,需要使用 Binder 的进程,几乎总是先通过
- binder_ioctl
打开 Binder 设备,然后通过
- binder_open
进行内存映射。
- binder_mmap
在这之后,通过
来进行实际的操作。Client 对于 Server 端的请求,以及 Server 对于 Client 请求结果的返回,都是通过 ioctl 完成的。
- binder_ioctl
这里提到的流程如下图所示:
Binder 驱动中包含了很多的结构体。为了便于下文讲解,这里我们先对这些结构体做一些介绍。
驱动中的结构体可以分为两类:
一类是与用户空间共用的,这些结构体在 Binder 通信协议过程中会用到。因此,这些结构体定义在
中,包括:
- binder.h
结构体名称 | 说明 |
---|---|
flat_binder_object | 描述在 Binder IPC 中传递的对象,见下文 |
binder_write_read | 存储一次读写操作的数据 |
binder_version | 存储 Binder 的版本号 |
transaction_flags | 描述事务的 flag,例如是否是异步请求,是否支持 fd |
binder_transaction_data | 存储一次事务的数据 |
binder_ptr_cookie | 包含了一个指针和一个 cookie |
binder_handle_cookie | 包含了一个句柄和一个 cookie |
binder_pri_desc | 暂未用到 |
binder_pri_ptr_cookie | 暂未用到 |
这其中,
和
- binder_write_read
这两个结构体最为重要,它们存储了 IPC 调用过程中的数据。关于这一点,我们在下文中会讲解。
- binder_transaction_data
Binder 驱动中,还有一类结构体是仅仅 Binder 驱动内部实现过程中需要的,它们定义在
中,包括:
- binder.c
结构体名称 | 说明 |
---|---|
binder_node | 描述 Binder 实体节点,即:对应了一个 Server |
binder_ref | 描述对于 Binder 实体的引用 |
binder_buffer | 描述 Binder 通信过程中存储数据的 Buffer |
binder_proc | 描述使用 Binder 的进程 |
binder_thread | 描述使用 Binder 的线程 |
binder_work | 描述通信过程中的一项任务 |
binder_transaction | 描述一次事务的相关信息 |
binder_deferred_state | 描述延迟任务 |
binder_ref_death | 描述 Binder 实体死亡的信息 |
binder_transaction_log | debugfs 日志 |
binder_transaction_log_entry | debugfs 日志条目 |
这里需要读者关注的结构体已经用加粗做了标注。
Binder 协议可以分为控制协议和驱动协议两类。
控制协议是进程通过 ioctl("/dev/binder") 与 Binder 设备进行通讯的协议,该协议包含以下几种命令:
命令 | 说明 | 参数类型 |
---|---|---|
BINDER_WRITE_READ | 读写操作,最常用的命令。IPC 过程就是通过这个命令进行数据传递 | binder_write_read |
BINDER_SET_MAX_THREADS | 设置进程支持的最大线程数量 | size_t |
BINDER_SET_CONTEXT_MGR | 设置自身为 ServiceManager | 无 |
BINDER_THREAD_EXIT | 通知驱动 Binder 线程退出 | 无 |
BINDER_VERSION | 获取 Binder 驱动的版本号 | binder_version |
BINDER_SET_IDLE_PRIORITY | 暂未用到 | - |
BINDER_SET_IDLE_TIMEOUT | 暂未用到 | - |
Binder 的驱动协议描述了对于 Binder 驱动的具体使用过程。驱动协议又可以分为两类:
,描述了 进程发送给 Binder 驱动的命令
- binder_driver_command_protocol
,描述了 Binder 驱动发送给进程的命令
- binder_driver_return_protocol
共包含 17 个命令,分别是:
- binder_driver_command_protocol
命令 | 说明 | 参数类型 |
---|---|---|
BC_TRANSACTION | Binder 事务,即:Client 对于 Server 的请求 | binder_transaction_data |
BC_REPLY | 事务的应答,即:Server 对于 Client 的回复 | binder_transaction_data |
BC_FREE_BUFFER | 通知驱动释放 Buffer | binder_uintptr_t |
BC_ACQUIRE | 强引用计数 + 1 | __u32 |
BC_RELEASE | 强引用计数 - 1 | __u32 |
BC_INCREFS | 弱引用计数 + 1 | __u32 |
BC_DECREFS | 弱引用计数 - 1 | __u32 |
BC_ACQUIRE_DONE | BR_ACQUIRE 的回复 | binder_ptr_cookie |
BC_INCREFS_DONE | BR_INCREFS 的回复 | binder_ptr_cookie |
BC_ENTER_LOOPER | 通知驱动主线程 ready | void |
BC_REGISTER_LOOPER | 通知驱动子线程 ready | void |
BC_EXIT_LOOPER | 通知驱动线程已经退出 | void |
BC_REQUEST_DEATH_NOTIFICATION | 请求接收死亡通知 | binder_handle_cookie |
BC_CLEAR_DEATH_NOTIFICATION | 去除接收死亡通知 | binder_handle_cookie |
BC_DEAD_BINDER_DONE | 已经处理完死亡通知 | binder_uintptr_t |
BC_ATTEMPT_ACQUIRE | 暂未实现 | - |
BC_ACQUIRE_RESULT | 暂未实现 | - |
共包含 18 中返回类型,分别是:
- binder_driver_return_protocol
返回类型 | 说明 | 参数类型 |
---|---|---|
BR_OK | 操作完成 | void |
BR_NOOP | 操作完成 | void |
BR_ERROR | 发生错误 | __s32 |
BR_TRANSACTION | 通知进程收到一次 Binder 请求(Server 端) | binder_transaction_data |
BR_REPLY | 通知进程收到 Binder 请求的回复(Client) | binder_transaction_data |
BR_TRANSACTION_COMPLETE | 驱动对于接受请求的确认回复 | void |
BR_FAILED_REPLY | 告知发送方通信目标不存在 | void |
BR_SPAWN_LOOPER | 通知 Binder 进程创建一个新的线程 | void |
BR_ACQUIRE | 强引用计数 + 1 请求 | binder_ptr_cookie |
BR_RELEASE | 强引用计数 - 1 请求 | binder_ptr_cookie |
BR_INCREFS | 弱引用计数 + 1 请求 | binder_ptr_cookie |
BR_DECREFS | 若引用计数 - 1 请求 | binder_ptr_cookie |
BR_DEAD_BINDER | 发送死亡通知 | binder_uintptr_t |
BR_CLEAR_DEATH_NOTIFICATION_DONE | 清理死亡通知完成 | binder_uintptr_t |
BR_DEAD_REPLY | 告知发送方对方已经死亡 | void |
BR_ACQUIRE_RESULT | 暂未实现 | - |
BR_ATTEMPT_ACQUIRE | 暂未实现 | - |
BR_FINISHED | 暂未实现 | - |
单独看上面的协议可能很难理解,这里我们以一次 Binder 请求过程来详细看一下 Binder 协议是如何通信的,就比较好理解了。
这幅图的说明如下:
这里再补充说明一下,通过上面的 Binder 协议的说明中我们看到,Binder 协议的通信过程中,不仅仅是发送请求和接受数据这些命令。同时包括了对于引用计数的管理和对于死亡通知的管理(告知一方,通讯的另外一方已经死亡)等功能。
这些功能的通信过程和上面这幅图是类似的:一方发送 BC_XXX,然后由驱动控制通信过程,接着发送对应的 BR_XXX 命令给通信的另外一方。因为这种相似性,对于这些内容就不再赘述了。
在有了上面这些背景知识介绍之后,我们就可以进入到 Binder 驱动的内部实现中来一探究竟了。
PS:上面介绍的这些结构体和协议,因为内容较多,初次看完记不住是很正常的,在下文详细讲解的时候,回过头来对照这些表格来理解是比较有帮助的。
任何进程在使用 Binder 之前,都需要先通过
打开 Binder 设备。上文已经提到,用户空间的
- open("/dev/binder")
系统调用对应了驱动中的
- open
函数。在这个函数,Binder 驱动会为调用的进程做一些初始化工作。
- binder_open
函数代码如下所示:
- binder_open
- static int binder_open(struct inode *nodp, struct file *filp)
- {
- struct binder_proc *proc;
- // 创建进程对应的binder_proc对象
- proc = kzalloc(sizeof(*proc), GFP_KERNEL);
- if (proc == NULL)
- return -ENOMEM;
- get_task_struct(current);
- proc->tsk = current;
- // 初始化binder_proc
- INIT_LIST_HEAD(&proc->todo);
- init_waitqueue_head(&proc->wait);
- proc->default_priority = task_nice(current);
- // 锁保护
- binder_lock(__func__);
- binder_stats_created(BINDER_STAT_PROC);
- // 添加到全局列表binder_procs中
- hlist_add_head(&proc->proc_node, &binder_procs);
- proc->pid = current->group_leader->pid;
- INIT_LIST_HEAD(&proc->delivered_death);
- filp->private_data = proc;
- binder_unlock(__func__);
- return 0;
- }
在 Binder 驱动中,通过
记录了所有使用 Binder 的进程。每个初次打开 Binder 设备的进程都会被添加到这个列表中的。
- binder_procs
另外,请读者回顾一下上文介绍的 Binder 驱动中的几个关键结构体:
在实现过程中,为了便于查找,这些结构体互相之间都留有字段存储关联的结构。
下面这幅图描述了这里说到的这些内容:
在打开 Binder 设备之后,进程还会通过 mmap 进行内存映射。mmap 的作用有如下两个:
函数对应了 mmap 系统调用的处理,这个函数也是 Binder 驱动的精华所在(这里说的 binder_mmap 函数也包括其内部调用的 binder_update_page_range 函数,见下文)。
- binder_mmap
前文我们说到,使用 Binder 机制,数据只需要经历一次拷贝就可以了,其原理就在这个函数中。
这个函数中,会申请一块物理内存,然后在用户空间和内核空间同时对应到这块内存上。在这之后,当有 Client 要发送数据给 Server 的时候, 只需一次,将 Client 发送过来的数据拷贝到 Server 端的内核空间指定的内存地址即可 ,由于这个内存地址在服务端已经同时映射到用户空间,因此无需再做一次复制,Server 即可直接访问,整个过程如下图所示:
- binder_mmap
这幅图的说明如下: 1. Server 在启动之后,调用对 / dev/binder 设备调用 mmap 2. 内核中的 binder_mmap 函数进行对应的处理:申请一块物理内存,然后在用户空间和内核空间同时进行映射 3. Client 通过 BINDER_WRITE_READ 命令发送请求,这个请求将先到驱动中,同时需要将数据从 Client 进程的用户空间拷贝到内核空间 4. 驱动通过 BR_TRANSACTION 通知 Server 有人发出请求,Server 进行处理。由于这块内存也在用户空间进行了映射,因此 Server 进程的代码可以直接访问
了解原理之后,我们再来看一下 Binder 驱动的相关源码。这段代码有两个函数:
函数对应了 mmap 的系统调用的处理
- binder_mmap
函数真正实现了内存分配和地址映射
- binder_update_page_range
- 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;
- ...
- // 在内核空间获取一块地址范围
- 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 = area->addr;
- // 记录内核空间与用户空间的地址偏移
- proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
- mutex_unlock(&binder_mmap_lock);
- ...
- 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 assumes preemption is disabled */
- preempt_disable();
- // 通过下面这个函数真正完成内存的申请和地址的映射
- // 初次使用,先申请一个PAGE_SIZE大小的内存
- ret = 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;
- ...
- for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
- int ret;
- struct page **page_array_ptr;
- 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? */;
- page_array_ptr = page;
- // 在内核空间进行内存映射
- ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr);
- 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_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);
- }
- preempt_disable();
- return 0;
- ...
在开发过程中,我们可以通过 procfs 看到进程映射的这块内存空间:
进入到终端
- adb shell
来确定进程号,例如是 1889
- ps | grep system_server
过滤出这块内存的地址
- cat /proc/[pid]/maps | grep "/dev/binder"
在我的 Nexus 6P 上,控制台输出如下:
- angler:/ # ps | grep system_server
- system 1889 526 2353404 140016 SyS_epoll_ 72972eeaf4 S system_server
- angler:/ # cat /proc/1889/maps | grep "/dev/binder"
- 7294761000-729485f000 r--p 00000000 00:0c 12593 /dev/binder
PS:grep 是通过通配符进行匹配过滤的命令,"|" 是 Unix 上的管道命令。即将前一个命令的输出给下一个命令作为输入。如果这里我们不加 "| grep xxx",那么将看到前一个命令的完整输出。
上文中,我们看到 binder_mmap 的时候,会申请一个 PAGE_SIZE(通常是 4K) 的内存。而实际使用过程中,一个 PAGE_SIZE 的大小通常是不够的。
在驱动中,会根据实际的使用情况进行内存的分配。有内存的分配,当然也需要内存的释放。这里我们就来看看 Binder 驱动中是如何进行内存的管理的。
首先,我们还是从一次 IPC 请求说起。
当一个 Client 想要对 Server 发出请求时,它首先将请求发送到 Binder 设备上,由 Binder 驱动根据请求的信息找到对应的目标节点,然后将请求数据传递过去。
进程通过 ioctl 系统调用来发出请求:
- ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)
PS:这行代码来自于 Framework 层的
类。在后文中,我们将看到,
- IPCThreadState
类专门负责与驱动进行通信 。
- IPCThreadState
这里的
对应了打开 Binder 设备时的 fd。
- mProcess->mDriverFD
对应了具体要做的操作码,这个操作码将由 Binder 驱动解析。
- BINDER_WRITE_READ
存储了请求数据,其类型是
- bwr
。
- binder_write_read
其实是一个相对外层的数据结构,其内部会包含一个
- binder_write_read
结构的数据。
- binder_transaction_data
包含了发出请求者的标识,请求的目标对象以及请求所需要的参数。它们的关系如下图所示:
- binder_transaction_data
binder_ioctl 函数对应了 ioctl 系统调用的处理。这个函数的逻辑比较简单,就是根据 ioctl 的命令来确定进一步处理的逻辑,具体如下:
这其中,最关键的就是 binder_thread_write 方法。当 Client 请求 Server 的时候,便会发送一个 BINDER_WRITE_READ 命令,同时框架会将将实际的数据包装好。此时,binder_transaction_data 中的 code 将是 BC_TRANSACTION,由此便会调用到 binder_transaction 方法,这个方法是对一次 Binder 事务的处理,这其中会调用 binder_alloc_buf 函数为此次事务申请一个缓存。这里提到到调用关系如下:
binder_update_page_range 这个函数在上文中,我们已经看到过了。其作用就是:进行内存分配并且完成内存的映射。而 binder_alloc_buf 函数,正如其名称那样的:完成缓存的分配。
在驱动中,通过 binder_buffer 结构体描述缓存。一次 Binder 事务就会对应一个 binder_buffer,其结构如下所示:
- struct binder_buffer {
- struct list_head entry;
- struct rb_node rb_node;
- 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;
- uint8_t data[0];
- };
而在 binder_proc(描述了使用 Binder 的进程)中,包含了几个字段用来管理进程在 Binder IPC 过程中缓存,如下:
- struct binder_proc {
- ...
- struct list_head buffers; // 进程拥有的buffer列表
- struct rb_root free_buffers; // 空闲buffer列表
- struct rb_root allocated_buffers; // 已使用的buffer列表
- size_t free_async_space; // 剩余的异步调用的空间
- size_t buffer_size; // 缓存的上限
- ...
- };
进程在 mmap 时,会设定支持的总缓存大小的上限(下文会讲到)。而进程每当收到 BC_TRANSACTION,就会判断已使用缓存加本次申请的和有没有超过上限。如果没有,就考虑进行内存的分配。
进程的空闲缓存记录在 binder_proc 的 free_buffers 中,这是一个以红黑树形式存储的结构。每次尝试分配缓存的时候,会从这里面按大小顺序进行查找,找到最接近需要的一块缓存。查找的逻辑如下:
- while (n) {
- buffer = rb_entry(n, struct binder_buffer, rb_node);
- BUG_ON(!buffer->free);
- 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;
- }
- }
找到之后,还需要对 binder_proc 中的字段进行相应的更新:
- rb_erase(best_fit, &proc->free_buffers);
- buffer->free = 0;
- binder_insert_allocated_buffer(proc, buffer);
- if (buffer_size != size) {
- struct binder_buffer *new_buffer = (void *)buffer->data + size;
- list_add(&new_buffer->entry, &buffer->entry);
- new_buffer->free = 1;
- 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->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);
- }
下面我们再来看看内存的释放。
命令是通知驱动进行内存的释放,
- BC_FREE_BUFFER
函数是真正实现的逻辑,这个函数与 binder_alloc_buf 是刚好对应的。在这个函数中,所做的事情包括:
- binder_free_buf
Binder 机制淡化了进程的边界,使得跨越进程也能够调用到指定服务的方法,其原因是因为 Binder 机制在底层处理了在进程间的 "对象" 传递。
在 Binder 驱动中,并不是真的将对象在进程间来回序列化,而是通过特定的标识来进行对象的传递。Binder 驱动中,通过
来描述需要跨越进程传递的对象。其定义如下:
- flat_binder_object
- struct flat_binder_object {
- __u32 type;
- __u32 flags;
- union {
- binder_uintptr_t binder;
- /* local object */
- __u32 handle;
- /* remote object */
- };
- binder_uintptr_t cookie;
- };
这其中,type 有如下 5 种类型。
- enum {
- BINDER_TYPE_BINDER = B_PACK_CHARS('s', 'b', '*', B_TYPE_LARGE),
- BINDER_TYPE_WEAK_BINDER = B_PACK_CHARS('w', 'b', '*', B_TYPE_LARGE),
- BINDER_TYPE_HANDLE = B_PACK_CHARS('s', 'h', '*', B_TYPE_LARGE),
- BINDER_TYPE_WEAK_HANDLE = B_PACK_CHARS('w', 'h', '*', B_TYPE_LARGE),
- BINDER_TYPE_FD = B_PACK_CHARS('f', 'd', '*', B_TYPE_LARGE),
- };
当对象传递到 Binder 驱动中的时候,由驱动来进行翻译和解释,然后传递到接收的进程。
例如当 Server 把 Binder 实体传递给 Client 时,在发送数据流中,flat_binder_object 中的 type 是 BINDER_TYPE_BINDER,同时 binder 字段指向 Server 进程用户空间地址。但这个地址对于 Client 进程是没有意义的(Linux 中,每个进程的地址空间是互相隔离的),驱动必须对数据流中的 flat_binder_object 做相应的翻译:将 type 该成 BINDER_TYPE_HANDLE;为这个 Binder 在接收进程中创建位于内核中的引用并将引用号填入 handle 中。对于发生数据流中引用类型的 Binder 也要做同样转换。经过处理后接收进程从数据流中取得的 Binder 引用才是有效的,才可以将其填入数据包 binder_transaction_data 的 target.handle 域,向 Binder 实体发送请求。
由于每个请求和请求的返回都会经历内核的翻译,因此这个过程从进程的角度来看是完全透明的。进程完全不用感知这个过程,就好像对象真的在进程间来回传递一样。
上文多次提到,Binder 本身是 C/S 架构。由 Server 提供服务,被 Client 使用。既然是 C/S 架构,就可能存在多个 Client 会同时访问 Server 的情况。 在这种情况下,如果 Server 只有一个线程处理响应,就会导致客户端的请求可能需要排队而导致响应过慢的现象发生。解决这个问题的方法就是引入多线程。
来源: