Binder驱动原理
Binder驱动的核心是维护一个binder_proc类型的链表。里面记录了包括ServiceManager在内的所有Client信息,当Client去请求得到某个Service时,Binder驱动就去binder_proc中查找相应的Service返回给Client,同时增加当前Service的引用个数。
Binder驱动是作为一个特殊字符型设备存在,设备节点为/dev/binder,遵循Linux设备驱动模型。在驱动实现过程中,主要通过binder_ioctl函数与用户空间的进程交换数据。BINDER_WRITE_READ用来读写数据,数据包中有个cmd用于区分不同的请求。
在binder_thread_write函数中调用binder_transaction函数来转发请求并返回结果,而binder_thread_read函数用于读取结果。。当服务进程收到请求时,binder_transaction函数会通过对象的handle找到对象所在进程,如果handle为0,就认为请求的是ServiceManager进程。
整个Binder的流程:
对象的索引和映射
Binder中有两种索引,一是本地进程地址空间的一个地址,另一个是一个抽象的32位句柄(HANDLE),它们之间是互斥的:所有的进程本地对象的索引都是本地进程的一个地址(address, ptr, binder),所有的远程进程的对象的索引都是一个句柄(handle)。对于发送者进程来说,索引就是一个远端对象的一个句柄,当Binder对象数据被发送到远端接收进程时,远端接受进程则会认为索引是一个本地对象地址,因此从第三方的角度来说,尽管名称不同,对于一次Binder调用,两种索引指的是同一个对象,Binder驱动则负责两种索引的映射,这样才能把数据发送给正确的进程。
对于Android的Binder来说,对象的索引和映射是通过binder_node和binder_ref两个核心数据结构来完成的,对于Binder本地对象,对象的Binder地址保存在binder_node->ptr里,对于远程对象,索引就保存在binder_ref->desc里,每一个binder_node都有一个binder_ref对象与之相联系,他们就是是通过ptr和desc来做映射的,如下图:
Binder句柄:
句柄就是个简单的整数值,用来告诉Binder驱动我们想找的目标Binder实体是哪个。但是请注意,句柄只对发起端进程和Binder驱动有意义,A进程的句柄直接拿到B进程,是没什么意义的。也就是说,不同进程中指代相同Binder实体的句柄值可能是不同的。示意图如下:
ServiceManagerService 记录了所有系统service所对应的Binder句柄,它的核心功能就是维护好这些句柄值。后续,当用户进程需要获取某个系统service的代理时,SMS就会在内部按service名查找到合适的句柄值,并“逻辑上”传递给用户进程,于是用户进程会得到一个新的合法句柄值,这个新句柄值可能在数值上和SMS所记录的句柄值不同,然而,它们指代的却是同一个Service实体。句柄的合法性是由Binder驱动保证的,这一点我们不必担心
flat_binder_object就是进程间传递的Binder对象,每一个flat_binder_object对象内都有一个唯一的binder_node对象或者binder_ref对象.他们之间的查找过程如下:
如果发送的flat_binder_object.type=BINDER,
- 在发送进程内查找flat_binder_object.binder对应的binder_node
- 如果找到则执行步骤4,否则执行步骤3
- 在发送进程内创建新的binder_node,binder_node.ptr=flat_binder_object.binder
- 在接收进程内查找binder_node对应的binder_ref,如果找到则执行步骤6,否则执行步骤5
- 在接收进程内创建新的binder_ref,binder_ref.node=binder_node,分配binder_ref.desc值
- 修改flat_binder_object.type=HANDLE(BINDER_TYPE_BINDER->BINER_TYPE_HANDLE;BINDER_TYPE_WEAK_BINDER->BINDER_TYPE_WEAK_HANDLE).
- 修改flat_binder_object.handle=binder_refs.desc
如果发送的flat_binder_object.type=HANDLE,
- 在发送进程内查找flat_binder_object.handle对应的binder_ref,如果找到,执行步骤3,否则执行步骤2
- 设置error为BR_FAILED_REPLY,执行步骤11(这里的设计,防止了Client通过蒙猜的方式进行非法通信
- binder_ref.node.proc是否为接收进程,如果是,则执行步骤4,否则执行步骤7
- 设置flat_binder_obecjt.type=BINDER(BINDER_TYPE_HANDLE->BINDER_TYPE_BINDER; BINDER_TYPE_WEAK_HANDLE->BINDER_TYPE_WEAK_BINDER)
- 设置flat_binder_object.binder=binder_ref.node.binder.
- 设置flat_binder_object.cookies=binder_ref.node.cookies,执行步骤11
- 在接收进程内超找对应的binder_ref,如果未找到,执行步骤8,否则执行步骤10
- 在接收进程内创建新的binder_ref
- 设置binder_ref(接收进程).node=binder_ref(发送进程).node,并分配binder_refs(接收进程).desc
- 设置flat_binder_object.handle=binder_ref(接收进程).desc
- 结束
进程1的BpBinder在发起跨进程调用时,向binder驱动传入了自己记录的句柄值,binder驱动就会在“进程1对应的binder_proc结构”的引用树中查找和句柄值相符的binder_ref节点,一旦找到binder_ref节点,就可以通过该节点的node域找到对应的binder_node节点,这个目标binder_node当然是从属于进程2的binder_proc啦,不过不要紧,因为binder_ref和binder_node都处于binder驱动的地址空间中,所以是可以用指针直接指向的。目标binder_node节点的cookie域,记录的其实是进程2中BBinder的地址,binder驱动只需把这个值反映给应用层,应用层就可以直接拿到BBinder了。
传输机制的大体运作
Binder IPC机制的大体思路是这样的,它将每次“传输并执行特定语义的”工作理解为一个小事务,既然所传输的数据是binder_transaction_data类型的,那么这种事务的类名可以相应地定为binder_transaction。系统中当然会有很多事务啦,那么发向同一个进程或线程的若干事务就必须串行化起来,因此binder驱动为进程节点(binder_proc)和线程节点(binder_thread)都设计了个todo队列。todo队列的职责就是“串行化地组织待处理的事务”。
下图绘制了一个进程节点,以及一个从属于该进程的线程节点,它们各带了两个待处理的事务(binder_transaction):
这样看来,传输动作的基本目标就很明确了,就是想办法把发起端的一个binder_transaction节点,插入到目标端进程或其合适子线程的todo队列去。
可是,该怎么找目标进程和目标线程呢?基本做法是先从发起端的BpBinder开始,找到与其对应的binder_node节点,这个在前文阐述binder_proc的4棵红黑树时已经说过了,这里不再赘述。总之拿到目标binder_node之后,我们就可以通过其proc域,拿到目标进程对应的binder_proc了。如果偷懒的话,我们直接把binder_transaction节点插到这个binder_proc的todo链表去,就算完成传输动作了。当然,binder驱动做了一些更精细的调整。
binder驱动希望能把binder_transaction节点尽量放到目标进程里的某个线程去,这样可以充分利用这个进程中的binder工作线程。比如一个binder线程目前正睡着,它在等待其他某个线程做完某个事情后才会醒来,而那个工作又偏偏需要在当前这个binder_transaction事务处理结束后才能完成,那么我们就可以让那个睡着的线程先去做当前的binder_transaction事务,这就达到充分利用线程的目的了。反正不管怎么说,如果binder驱动可以找到一个合适的线程,它就会把binder_transaction节点插到它的todo队列去。而如果找不到合适的线程,还可以把节点插入目标binder_proc的todo队列。
Binder 相关数据结构
这一部分内容可以用到的时候再看,主要是数据的封装和解析
flat_binder_object
进程间传输的数据被称为Binder对象(Binder Object),它是一个flat_binder_object
,binder驱动接收和返回的数据中,如果有BpBinder或者BBinder,都会转成一个flat_binder_object
对象,然后再进行赋值或者解析,它的定义如下:
1 | /* |
其中type表示类型,flags描述了传输方式,比如同步、异步等。Android定义了五个(三大类)Binder类型,如下:
1 | enum { |
忽略强弱引用的差异,type可以分为三类BINDER、HANDLE、FD。
- type为BINDER类型时,flat_binder_object代表binder_node,flat_binder_object.binder等于相应binder_node.ptr,指向Service用户空间的BBinder。
- type为HANDLE类型时,flat_binder_object代表binder_ref,flat_binder_object.handle等于相应binder_refs.desc,也就是等于Client用户空间BpBinder.handle。
- type为FD类型时,flat_binder_object代表文件Binder,flat_binder_object.handle是文件在进程内的文件号。
传输的数据是一个复用数据联合体,对于BINDER类型,数据就是一个binder本地对象,如果是HANDLE类型,这数据就是一个远程的handle对象。该如何理解本地binder对象和远程handle对象呢?其实它们都指向同一个对象,不过是从不同的角度来说。举例来说,假如A有个对象X,对于A来说,X就是一个本地的binder对象;如果B想访问A的X对象,这对于B来说,X就是一个handle。因此,从根本上来说handle和binder都指向X。本地对象还可以带有额外的数据,保存在cookie中。
binder_node
binder_node
代表一个内核中的binder实体,每一个binder_node都关联到用户态的BBinder对象。Binder实体服务其实有两种,一是通过addService注册到ServiceManager中的服务,比如ActivityManagerService、PackageManagerService、PowerManagerService等,一般都是系统服务;还有一种是通过bindService拉起的一些服务,一般是开发者自己实现的服务。
1 | struct binder_node { |
binder_ref
binder_ref
代表内核中的binder引用,用户态每一个有效的BpBinder都关联到特定的binder_ref。同时binder_ref总是关联到一个binder_node
1 |
|
binder_proc
binder_proc代表了使用binder driver的process,保存了process的相关信息。binder driver会为每一个调用过open函数打开“dev/binder”文件的进程创建一个binder_proc.
1 | //binder_proc的结构 |
binder_procs
binder_procs结构的定义:
1 | static HLIST_HEAD(binder_procs); |
展开后得到
1 | struct hlist_head binder_procs = { .first = NULL }; |
随着后续不断向binder_procs表中添加节点,这个表会不断加长,示意图如下:
binder_thread
binder_thread代表了binder_proc内的线程,保存了线程相关信息。binder driver会为每一个调用过ioctl函数操作“dev/binder”文件的线程创建binder_thread.
1 | struct binder_thread { |
binder_write_read
binder_write_read为BINDER_WRITE_READ指定的数据类型,它的定义如下:
1 | struct binder_write_read { |
binder_transaction_data
binder_transaction_data为写入协议BC_TRANSACTION、BC_REPLY以及读出协议BR_TRANSACTION、BR_REPLY所指定的数据类型,Binder驱动的使用者(e.i. Client、Service、Service Manager)通过binder_transaction_data和Binder driver进行数据交换。
1 | struct binder_transaction_data { |
就像注释中说明的那样,target成员和cookie成员仅在BC_TRANSACTION和BR_TRANSACTION协议中使用。通常,Client使用BC_TRANSACTION协议写入数据时,需要通过target.handle指定数据接收方。而Service读取到BR_TRANSACTION的binder_transaction_data.ptr成员保存了用户空间binder实体的地址(实际上,BBinder的弱引用地址),而cookie成员保存了用户数据(实际上,cookie才真正保存了BBinder的地址)。而使用BC_REPLY写入时,Binder driver忽略这两个参数,而读取到BR_REPLY的 binder_transaction_data的target和cookie成员则恒为空。
最后,也是对于理解binder_transaction_data最重要的一点,binder_transaction_data结构体,并不包含传输的数据,而是通过其ptr.buffer成员保存了数据的内存地址。而ptr.offsets成员则保存了Binder对象(或者说flat_binder_object)在ptr.buffer的偏移量数组的首地址。data_size成员则记录了数据的长度,offsets_size则是编译量数组的长度(以字节为单位,所以,编译量数组实际的长度是offsets_size/4)。
binder 用户接口
binder_open()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc;
. . . . . .
proc = kzalloc(sizeof(*proc), GFP_KERNEL);
get_task_struct(current);
proc->tsk = current;
. . . . . .
hlist_add_head(&proc->proc_node, &binder_procs);
proc->pid = current->group_leader->pid;
. . . . . .
filp->private_data = proc;
. . . . . .
}主要做了以下几件事情:
- 首先,binder驱动分配内存以保存binder_proc数据结构。然后,binder填充binder_proc数据(初始化),增加当前线程/进程的引用计数并赋值给tsk
- 增加BINDER_STAT_PROC的对象计数,并把创建的binder_proc对象添加到全局的binder_procs中,这样任何一个进程就都可以访问到其他进程的binder_proc对象了。
- 把binder_proc对象指针赋值给filp的private_data域中,在后面每次执行binder_ioctl(),都会从filp->private_data域重新读取binder_proc。
binder_mmap()
内存的映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96static int binder_mmap(struct file *filp, struct vm_area_struct *vma) {
int ret;
//需要映射的内核空间地址信息
struct vm_struct *area;
//取出binder_open时保存的binder_proc数据
struct binder_proc *proc = filp->private_data;
struct binder_buffer *buffer;
//保证这块内存最多只有4M
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
//申请一段内存空间给内核进程
area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
//得到映射的内核空间虚拟地址首地址
proc->buffer = area->addr;
//计算用户空间与映射的内核空间的地址偏移量
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
//得到映射地址的页数
proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
//映射空间的大小
proc->buffer_size = vma->vm_end - vma->vm_start;
vma->vm_ops = &binder_vm_ops;
vma->vm_private_data = proc;
//为虚拟地址空间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 = proc->buffer;
INIT_LIST_HEAD(&proc->buffers);
list_add(&buffer->entry, &proc->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;
return 0;
err_alloc_small_buf_failed:
kfree(proc->pages);
proc->pages = NULL;
err_alloc_pages_failed:
vfree(proc->buffer);
proc->buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
err_bad_arg:
return ret;
}
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;
//以页为单位分配物理页面,由于此时的end=start+PAGE_SIZE,因此只会循环一次
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];
//分配物理页面
*page = alloc_page(GFP_KERNEL | __GFP_ZERO);
if (*page == NULL) {
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) {
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) {
goto err_vm_insert_page_failed;
}
}
return 0;
}binder_ioctl()
这个函数是Binder的最核心部分,Binder的功能就是通过ioctl命令来实现的。Binder的ioctl命令共有7个,定义在ioctl.h头文件中:1
2
3
4
5
6
7
8
9
10
11
12
13
14首先是BINDER_SET_IDLE_TIMEOUT 和 BINDER_SET_IDLE_PRIORITY在目前的Binder驱动中没有实现。
BINDER_SET_MAX_THREADS
这个ioctl命令用于设置进程的Biner对象所支持的最大线程数。设置的值保存在binder_proc结构的max_threads成员里。
BINDER_SET_CONTEXT_MGR
从功能上看,只有一个进程/线程能成功设置binder_context_mgr_node对象,这个进程被称为Context Manager(context_mgr)。当然,也只有创建binder_context_mgr_node对象的Binder上下文管理进程/线程才有权限重新设置这个对象。进程的权限(cred->euid)保存在binder_context_mgr_uid对象里。
从接口的角度来说,这是一个进程想要成为一个Context Manager的唯一接口。一个Context Manager进程需要为binder_proc创建一个binder_node类型的节点。节点是通过binder_new_node函数来创建的,我们在后面在详细讲解这个函数。节点创建成功后内核会初始化节点的部分数据(weak_ref和strong_ref)
对于ContextManager对象来说,binder_node是binder_context_mgr_node,这个是全局变量;这个binder对象的索引(handler)固定为0
BINDER_THREAD_EXIT
通过调用binder_free_thread终止并释放binder_thread对象及其binder_transaction事务。
BINDER_VERSION
读取当前Binder驱动支持的协议版本号。BINDER_WRITE_READ
这个ioctl命令是Binder最核心的部分,Android Binder的IPC机制就是通过这个接口来实现的。
引用:
听说你Binder机制学的不错,来面试下这几个问题
Android进程间通信(IPC)机制Binder简要介绍和学习计划
深入分析Android Binder 驱动
红茶一杯话Binder