共计 6834 个字符,预计需要花费 18 分钟才能阅读完成。
前言
家喻户晓,Binder 是 Android 零碎中最次要的过程间通信套件,更具体一点,很多文章称之为 Binder 驱动,那为什么说它是一个驱动呢,驱动又是何物,让咱们自底向上,从内核中的 Binder 来一步步揭开它的面纱。本文重点在帮忙读者对于 Binder 零碎有一个简略的理解,所以写得比拟抽象,后续文章会详细分析。
Binder 到底是什么
Android 零碎内核是 Linux,每个过程有本人的虚拟地址空间,在 32 位零碎下最大是 4GB,其中 3GB 为用户空间,1GB 为内核空间;每个过程用户空间绝对独立,而内核空间是一样的,能够共享,如下图
Linux 驱动运行在内核空间,广义上讲是零碎用于管制硬件的两头程序,然而归根结底它只是一个程序一段代码,所以具体实现并不一定要和硬件无关。Binder 就是将本人注册为一个 misc 类型的驱动 ,不波及硬件操作,同时 本身运行于内核中,所以能够当作不同过程间的桥梁实现 IPC 性能。
Linux 最大的特点就是所有皆文件,驱动也不例外,所有驱动都会被挂载在文件系统 dev 目录下,Binder 对应的目录是 /dev/binder,注册驱动时将 open release mmap 等零碎调用注册到 Binder 本人的函数,这样的话在用户空间就能够通过零碎调用以拜访文件的形式应用 Binder。上面来粗略看一下相干代码。
device\_initcall 函数用于注册驱动,由零碎调用
binder\_init 中调用 misc\_register 注册一个名为 binder 的 misc 驱动,同时指定函数映射,将 binder\_open 映射到零碎调用 open,这样就能够通过 open("/dev/binder")
来调用 binder\_open 函数了
// 驱动函数映射
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,
};
// 注册驱动参数构造体
static struct miscdevice binder_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
// 驱动名称
.name = "binder",
.fops = &binder_fops
};
static int binder_open(struct inode *nodp, struct file *filp){......}
static int binder_mmap(struct file *filp, struct vm_area_struct *vma){......}
static int __init binder_init(void)
{
int ret;
// 创立名为 binder 的单线程的工作队列
binder_deferred_workqueue = create_singlethread_workqueue("binder");
if (!binder_deferred_workqueue)
return -ENOMEM;
......
// 注册驱动,misc 设施其实也就是非凡的字符设施
ret = misc_register(&binder_miscdev);
......
return ret;
}
// 驱动注册函数
device_initcall(binder_init);
Binder 的简略通信过程
一个过程如何通过 binder 和另一个过程通信?最简略的流程如下
- 接收端过程开启一个专门的线程,通过零碎调用在 binder 驱动(内核)中先注册此过程(创立保留一个 bidner\_proc),驱动为接收端过程创立一个工作队列(biner\_proc.todo)
- 接收端线程开始有限循环,通过零碎调用不停拜访 binder 驱动,如果该过程对应的工作队列有工作则返回解决,否则阻塞该线程直到有新工作入队
- 发送端也通过零碎调用拜访,找到指标过程,将工作丢到指标过程的队列中,而后唤醒指标过程中休眠的线程解决该工作,即实现通信
在 Binder 驱动中以 binder\_proc 构造体代表一个过程,binder\_thread 代表一个线程,binder\_proc.todo 即为 过程须要解决的来自其余过程的工作队列。
struct binder_proc {
// 存储所有 binder_proc 的链表
struct hlist_node proc_node;
// binder_thread 红黑树
struct rb_root threads;
// binder_proc 过程内的 binder 实体组成的红黑树
struct rb_root nodes;
......
}
Binder 的一次拷贝
家喻户晓 Binder 的劣势在于一次拷贝效率高,泛滥博客曾经说烂了,那么什么是一次拷贝,如何实现,产生在哪里,这里尽量简略地解释一下。
下面曾经说过,不同过程通过在内核中的 Binder 驱动来进行通信,然而用户空间和内核空间是隔离开的,无奈相互拜访,他们之间传递数据须要借助 copy\_from\_user 和 copy\_to\_user 两个零碎调用,把用户 / 内核空间内存中的数据拷贝到内核 / 用户空间的内存中,这样的话,如果两个过程须要进行一次单向通信则须要进行两次拷贝,如下图。
Binder 单次通信只须要进行一次拷贝,因为它应用了内存映射,将一块物理内存(若干个物理页)别离映射到接收端用户空间和内核空间,达到用户空间和内核空间共享数据的目标。
发送端要向接收端发送数据时,内核间接通过 copy\_from\_user 将数据拷贝到内核空间映射区,此时因为共享物理内存,接管过程的内存映射区也就能拿到该数据了,如下图。
代码实现局部
用户空间通过 mmap 零碎调用,调用到 Binder 驱动中 binder\_mmap 函数进行内存映射,这部分代码比拟难读,感兴趣的能够看一下。
drivers/android/binder.c
binder_mmap
创立 binder\_buffer,记录过程内存映射相干信息(用户空间映射地址,内核空间映射地址等),binder_buffer.data
中寄存的就是接收端过程和内核共享的物理内存块的指针
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;
// 每一次 Binder 传输数据时,都会先从 Binder 内存缓存区中调配一个 binder_buffer 来存储传输数据
struct binder_buffer *buffer;
if (proc->tsk != current)
return -EINVAL;
// 保障内存映射大小不超过 4M
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
......
// 采纳 IOREMAP 形式,调配一个间断的内核虚拟空间,与用户过程虚拟空间大小统一
// 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;
}
// 指向内核虚拟空间的地址
proc->buffer = area->addr;
// 用户虚拟空间起始地址 - 内核虚拟空间起始地址
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
......
// 调配物理页的指针数组,数组大小为 vma 的等效 page 个数
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;
// 调配物理页面,同时映射到内核空间和过程空间,先调配 1 个物理页
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 = proc->buffer;
// buffer 插入链表
INIT_LIST_HEAD(&proc->buffers);
list_add(&buffer->entry, &proc->buffers);
buffer->free = 1;
binder_insert_free_buffer(proc, buffer);
// oneway 异步可用大小为总空间的一半
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;
}
binder_update_page_range
函数为映射地址调配物理页,这里先调配一个物理页(4KB),而后将这个物理页同时映射到用户空间地址和内存空间地址
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 page **page;
// 内存构造体
struct mm_struct *mm;
if (end <= start)
return 0;
......
// 循环调配所有物理页,并别离建设用户空间和内核空间对该物理页的映射
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
int ret;
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;
}
// 物理内存映射到内核虚拟空间
ret = map_kernel_range_noflush((unsigned long)page_addr,
PAGE_SIZE, PAGE_KERNEL, page);
flush_cache_vmap((unsigned long)page_addr,
// 用户空间地址 = 内核地址 + 偏移
user_page_addr =
(uintptr_t)page_addr + proc->user_buffer_offset;
// 物理空间映射到用户虚拟空间
ret = vm_insert_page(vma, user_page_addr, page[0]);
}
}
binder_mmap
函数中调用 binder_update_page_range
只为映射区调配了一个物理页的空间,在 Binder 开始通信时,会再通过 binder_alloc_buf
函数调配更多物理页,这是后话了。
Binder 套件架构
内核层的 Binder 驱动曾经提供了 IPC 性能,不过还须要在 framework native 层提供一些 对于驱动层的调用封装,使 framework 开发者更易于应用,由此封装出了 native Binder;同时,因为 framework native 层是 c /c++ 语言实现,对于利用开发者,须要更加不便的 Java 层的封装,衍生出 Java Binder;最初在此之上,为了缩小反复代码的编写和标准接口,在 Java Binder 的根底上又封装出了 AIDL。通过层层封装,在使用者应用 AIDL 时对于 Binder 基本上是无感知的。
这里贴一张架构图。
Native 层
BpBinder 代表服务端 Binder 的一个代理,外部有一个成员 mHandle 就是服务端 Binder 在驱动层的句柄,客户端通过调用 BpBinder::transact
传入该句柄,通过驱动层和服务端 BBinder 产生会话,最初服务端会调用到BBinder::onTransact
。在这里两者之间通过约定好的 code 来标识会话内容。
后面提到过,须要用 Binder 进行通信的过程都须要在驱动中先注册该过程,并且每次通信时须要一个线程死循环读写 binder 驱动。驱动层中一个过程对应一个 binder\_proc,一个线程对应 binder\_thread;而在 framework native 层中,过程对应一个 ProcessState,线程对应 IPCThreadState,BpBinder::transact
发动通信最终也是通过 IPCThreadState.transact
调用驱动进行。
实际上 Android 中每个利用过程都关上了 Binder 驱动(在驱动中注册),Zygote 过程在 fork 出利用过程后,调用 app\_main.cpp 中 onZygoteInit 函数初始化,此函数中就创立了该过程的 ProcessState 实例,关上 Binder 驱动而后调配映射区,驱动中也创立并保留一个该过程的 binder\_proc 实例。这里借一张图来形容。
Java 层
Java 层是对 native 层相干类的封装,BBinder 对应 Binder,BpBinder 对应 BinderProxy,java 层最初还是会调用到 native 层对应函数
AIDL
AIDL 生成的代码对于 Binder 进行了进一步封装,< 接口 >.Stub
对应服务端 Binder,< 接口 >.Stub.Proxy
标识客户端,外部持有一个 mRemote 实例(BinderProxy),aidl 依据定义的接口办法生成若干个 TRANSACTION_< 函数名 >
code 常量,两端 Binder 通过这些 code 标识解析参数,调用相应接口办法。换言之 AIDL 就是对BinderProxy.transact
和Binder.onTransact
进行了封装,使用者不用再本人定义每次通信的 code 以及参数解析。
后记
本篇文章次要为不理解 Binder 体系的读者提供一个抽象的意识,接下来的文章会从 AIDL 近程服务开始层层向下剖析整个 IPC 过程,所以如果想要更深一步理解 Binder,本文作为前置常识也比拟重要。
相干视频举荐:
【2021 最新版】Android studio 装置教程 +Android(安卓)零基础教程视频(适宜 Android 0 根底,Android 初学入门)含音视频_哔哩哔哩_bilibili
Android 面试重点问题解析——无所不能的 Binder 底层原理解析!_哔哩哔哩_bilibili
【Android 进阶教程】——Framework 面试必问的 Handler 源码解析_哔哩哔哩_bilibili
本文转自 https://juejin.cn/post/6987595923543031821,如有侵权,请分割删除。