本文通过几个典型的binder通信过程来呈现其实现细节。
启动service manager
流程
Service manager进程和binder驱动的交互如下:
在安卓系统启动过程中,init进程会启动service manager进程。service manager会打开/dev/binder设备,一个进程打开binder设备就意味着该进程会使用binder这种IPC机制,这时候,在内核态会相应的构建一个binder proc对象,来管理该进程相关的binder资源(binder ref、binder node、binder thread等)。为了方便binder内存管控,这时候还会映射一段128K的内存地址用于binder通信。之后,service manager会把自己设定为context manager。所谓context manager实际上就是一个“名字服务器”,可以完成service组件名字的解析。随后service manager会通过binder协议(BC_ENTER_LOOPER)告知驱动自己已经准备好接收请求了。最后,service manager会进入读阻塞状态,等待来自其他进程的服务请求。
完成上面的一系列操作之后,内核相关的数据结构如下所示:
由于Service manager也算是一个特殊的service组件,因此在内核态也有一个binder node对象与之对应。service manager和其他的service组件不同的是它没有使用线程池模型,而是一个单线程的进程,因此它在内核态只有一个binder proc和binder thread。整个系统系统只有一个binder context,系统中所有的binder proc都指向这个全局唯一的binder上下文对象。而找到了binder context也就找到了service manager对应的binder node。
binder proc使用了红黑树来管理其所属的binder thread和binder node,不过在Service manager这个场景中,binder proc只管理了一个binder thread和binder node,看起来似乎有些小题大做,不过在其他场景(例如system server)中,binder proc会创建线程池,也可能注册多个service组件。
相关数据结构
在内核态,每一个参与binder通信的进程都会用一个唯一的struct binder_proc对象来表示。struct binder_proc主要成员如下表所示:
成员变量 | 描述 |
---|---|
struct hlist_nodeproc_node | 系统中的所有binder proc挂入binder_procs的链表中,这个成员是挂入全局binder_procs的链表的节点 |
struct rb_root threads | binder进程对应的所有binder thread组成的红黑树,tid作为key |
struct rb_root nodes | 一个binder进程可以注册多个service组件,因此binder proc可以有很多的binder node。Binder proc对应的所有binder node组成一颗红黑树。当然对于service manager而言,它只有一个binder node。 |
struct list_headwaiting_threads | 该binder进程的线程池中等待处理binder work的binder thread链表 |
int pid | 进程ID |
struct task_struct *tsk | 指向该binder进程对应的进程描述符(指向thread group leader对应的task struct) |
struct list_head todo | 需要该binder进程处理的binder work链表 |
int max_threads | 线程池中运行的最大数目 |
struct binder_alloc alloc | 管理binder 内存分配的数据结构 |
struct binder_context*context | 保存binder上下文管理者的信息。通过binder context可以找到service manager对应的bind node。 |
和进程抽象类似,binder proc也是管理binder资源的实体,但是真正执行binder通信的实体是binder thread。struct binder_thread主要成员如下表所示:
成员变量 | 描述 |
---|---|
struct binder_proc *proc | 该binder thread所属的binder proc |
struct rb_node rb_node | 挂入binder proc红黑树的节点 |
struct list_headwaiting_thread_node | 无事可做的时候,binder thread会挂入binder proc的等待队列 |
int pid | Thread id |
struct binder_transaction*transaction_stack | 该binder thread正在处理的transaction |
struct list_head todo | 需要该binder线程处理的binder work链表 |
struct task_struct *task | 该binder thread对应的进程描述符 |
Binder node是用户空间service组件对象的内核态实体对象,struct binder_node主要成员如下表所示:
成员变量 | 描述 |
---|---|
struct rb_node rb_node; | 一个binder proc可能有多个service组件(提供多种服务),属于一个binder proc的binder node会挂入binder proc的红黑树,这个成员是嵌入红黑树的节点。 |
struct binder_proc *proc | 该binder node所属的binder proc |
int debug_id | 唯一标示该node的id,用于调试 |
struct hlist_head refs | 一个service组件可能会有多个client发起服务请求,也就是说每一个client都是对binder node的一次引用,这个成员是就是保存binder ref的哈希表 |
binder_uintptr_t ptrbinder_uintptr_t cookie | 指向用户空间service组件相关的信息 |
u8 sched_policy:2;u8 inherit_rt:1;u8 min_priority; | 这些属性定义了该service组件在处理transaction的时候优先级的设定。 |
bool has_async_transaction | 是否有异步通信需要处理 |
struct list_head async_todo | 异步binder通信的队列 |
client如何找到service manager
流程
为了完成service组件注册,Client需要首先定位service manager组件。在client这个binder process中,我们使用handle作为地址来标记service组件。Service manager比较特殊,对任何一个binder process而言,handle等于0的那个句柄就是指向service manager组件。对内核态binder驱动而言,寻找service manager实际上就是寻找其对应的binder node。下面是一个binder client向service manager请求注册服务的过程示例,我们重点关注binder驱动如何定位service manager:
想要访问service manager的进程需要首先打开binder driver,这时候内核会创建该进程对应的binder proc对象,并建立binder proc和context manager的关系,这样进一步可以找到service manager对应的binder node。随后,client进程会调用mmap映射了(1M-8K)的binder内存空间。之所以映射这么怪异的内存size主要是为了有效的利用虚拟地址空间(VMA之间有4K的gap)。完成上面两步操作之后,client process就可以通过ioctl向service manager发起transaction请求了,同时告知目标对象handle等于0。
实际上这个阶段的主要工作在用户空间,主要是service manager组件代理BpServiceManager以及BpBinder的创建过程。一般的通信过程需要为组件代理对象分配一个句柄,但是service manager访问比较特殊,对于每一个进程,等于0的句柄都保留给了service manager,因此这里就不需要分配句柄这个过程了。
路由过程
在binder C/S通信结构中,binder client中的BpBinder找到binder server中的BBinder的过程需要如下过程:
- binder client用户空间中的service组件代理(BpBinder)用句柄表示要访问的server中的service组件(BBinder)
-
对于每一个句柄,binder client内核空间使用binder ref对象与之对应
- binder ref对象会指向一个binder node对象
- binder node对象对应一个binder server进程的service组件
在我们这个场景中,binder ref是在client第一次通过ioctl和binder驱动交互时候完成的。这时候,binder驱动的binder_ioctl函数中会建立上面路由过程需要的完整的数据对象:
Service manager的路由比较特殊,没有采用binder ref—>binder node的过程。在binder驱动中,看到0号句柄自然就知道是去往service manager的请求。因此,通过binder proc—>binder context—–binder node这条路径就找到了service manager。
注册Service组件
流程
上一节描述了client如何找到service manager的过程,这是整个注册service组件的前半部分,这一节我们补全整个流程。由于client和service manager都完成了open和mmap的过程,双方都准备好,后续可以通过ioctl进行binder transaction的通信过程了,因此下面的流程图主要呈现binder transaction的流程(忽略client/server和binder驱动系统调用的细节):
Service manager是一个service组件管理中心,任何一个service组件都需要向service manager进行注册(add service),以便其他的APP可以通过service manager定位到该service组件(check service)。
数据对象综述
注册服务相关数据结构全图如下:
配合上面的流程,binder驱动会为client和server分别创建对应的各种数据结构对象,具体过程如下:
- 假设我们现在准备注册A服务组件,绑定A服务组件的进程在add service这个场景下是client process,它在用户空间首先会创建了service组件对象,在递交BC_TRANSACTION的时候会携带service组件的信息(把service组件地址信息封装在flat_binder_object数据结构中)。
-
在系统调用接口层面,我们使用ioctl(BINDER_WRITE_READ)来完成具体transaction的递交过程。具体的transaction数据封装在struct binder_write_read对象中,具体如下图所示:
- Binder驱动创建binder_transaction对象来控制完成本次binder transaction。首先要初始化transaction,具体包括:和谁通信(用户空间通过binder_transaction_data的target成员告知binder驱动transaction的target)、为何通信(binder_transaction_data的code)等
-
对于每一个service组件,内核都会创建一个binder node与之对应。用户空间通过flat_binder_object这个数据结构把本次要注册的service组件扁平化,传递给binder驱动。驱动根据这个flat_binder_object创建并初始化了该service组件对应的binder node。由于是注册到service manager,也就是说service manager会有一个对本次注册组件的引用,所以需要在target proc(即service manager)中建立一个binder ref对象(指向这个要注册的binder实体)并分配一个handle。
-
把一个BINDER_WORK_TRANSACTION_COMPLETE类型的binder work挂入client binder thread的todo list,通知client其请求的transaction已经被binder处理完毕,可以进行其他工作了(当然对于同步binder通信,client一般会通过read类型的ioctl进入阻塞态,等待server端的回应)。
-
至此,client端已经完成了所有操作,现在我们开始进入server端的数据流了。Binder驱动会把一个BINDER_WORK_TRANSACTION类型的binder work(内嵌在binder transaction)挂入binder线程的todo list,然后唤醒它起来干活。
-
binder server端会使用ioctl(BINDER_WRITE_READ)进入读阻塞状态,等待client的请求到来。一旦有请求到来,Service manager进程会从binder_thread_read中醒来处理队列上的binder work。所谓处理binder work其实完成client transaction的向上递交过程。具体的transaction数据封装在struct binder_write_read对象中,具体如下图所示:
需要强调的一点是:在步骤2中,flat_binder_object传递的是binder node,而这里传递的是handle(即binder ref,步骤4中创建的)
- 在Service manager进程的用户态,识别了本次transaction的code是add service,那么它会把(service name,handle)数据写入其数据库,完成服务注册。
- 从transaction的角度看,上半场已经完成。现在开始下半场的transaction的处理,即BC_REPLY的处理。和BC_TRANSACTION处理类似,也是通过binder_ioctl —> binder_ioctl_write_read —> binder_thread_write —> binder_transaction这个调用链条进入binder transaction处理流程的。
- 和上半场类似,在这里Binder驱动同样会创建一个binder_transaction对象来控制完成本次BC_REPLY的binder transaction。通过thread->transaction_stack可以找到其对应的BC_TRANSACTION的binder transaction对象,进而找到回应给哪一个binder process和thread。后续的处理和上半场类似,这里就不再赘述了。
相关数据结构
struct transaction主要用来表示binder client和server之间的一次通信,该数据结构的主要成员如下表所示:
成员变量 | 描述 |
---|---|
work | 本次transaction涉及的binder work,它会挂入target proc或者target binder thread的todo list中。 |
from | 发起binder通信的线程 |
to_proc | 处理binder请求的进程 |
to_thread | 处理binder请求的线程 |
buffer | binder通信使用的buffer,当A向B服务请求binder通信的时候,B进程分配buffer,并copy A的数据(user space)到buffer中。这是binder通信唯一一次内存拷贝。 |
code | 本次transaction的操作码。Binder server端根据操作码提供相应的服务 |
flags | 本次transaction的一些属性标记 |
Prioritysaved_priority | 和优先级处理相关的成员 |
BC_TRANSACTION、BC_REPLY、BR_TRANSACTION和BR_REPLY这四个协议码的协议数据是struct binder_transaction_data,该数据结构的主要成员如下表所示:
成员变量 | 描述 |
---|---|
target | 本次transation去向何方?Target有两种形式,一种是本地binder实体,另外一种是表示远端binder实体的句柄。在client向service manager发起transaction的时候,那么target.handle等于0。当该transaction到达service manager的时候,binder实体变成本地对象,因此用Target.ptr和cookie来表示。 |
cookie | 如果transaction的目的地是本地binder实体,那么这个成员保存了binder实体对象的用户空间地址 |
code | Client和service 组件之间的操作码,binder驱动不关心这个码字。 |
flags | 描述transaction特性的flag。例如TF_ONE_WAY说明是同步还是异步binder通信 |
sender_pidsender_euid | 是谁发起transaction?在binder驱动中会根据当前线程设定。 |
data_sizeoffsets_sizedata | 本次transaction的数据缓冲区信息。 |
flat_binder_object主要用来在进程之间传递Binder对象,该数据结构的主要成员如下表所示:
成员变量 | 描述 |
---|---|
hdr | 用来描述Binder对象的类型,目前支持的类型有:binder实体(本地service组件)Binder句柄(远端的service组件)文件描述符……本文主要关注前两种对象类型 |
Binderhandle | 如果flat_binder_object传递的是本地service组件,那么这个联合体中的binder成员有效,指向本地service组件(用户空间对象)的一个弱引用对象的地址。如果flat_binder_object传递的是句柄,那么这个联合体中的handle成员有效,该handle对应的binder ref指向一个binder实体对象。 |
cookie | 如果传递的是binder实体,那么这个成员保存了binder实体对象(service组件)的用户空间地址 |
struct binder_ref主要用来表示一个对Binder实体对象(binder node)的引用,该数据结构的主要成员如下表所示:
成员变量 | 描述 |
---|---|
data | 这个成员最核心的数据是用户空间的句柄 |
rb_node_desc | 挂入binder proc的红黑树(key是描述符,userspace的句柄) |
rb_node_node | 挂入binder proc的红黑树(key是binder node) |
node_entry | 挂入binder node的哈希表 |
proc | 该binder ref属于哪一个binder proc |
node | 该binder ref引用哪一个binder node |
如何和Service组件通信
我们以B进程向A服务组件(位于A进程)发起服务请求为例来说明具体的操作流程。B进程不能直接请求A服务组件的服务,因为B进程唯一获知的信息是A服务组件的名字而已。由于A服务组件已经注册在案,因此service manager已经有(A服务组件名字,句柄)的记录,因此B进程可以通过下面的流程获得A服务组件的信息并建立其代理组件对象:
B进程首先发起BC_TRANSACTION操作,操作码是CHECK_SERVICE,数据是A服务组件的名字。Service manager找到了句柄后将其封装到BC_REPLY中。这里的句柄是service manager进程的句柄,这个句柄并不能直接被B进程直接使用,毕竟(进程,句柄)才对应唯一的binder实体。这里的binder driver有一个很关键的操作:把service manager中句柄A转换成B client进程中的句柄B,并封装在BR_REPLY中。这时候(service manager进程,句柄A)和(B client进程,句柄B)都指向A服务组件对应的bind node对象。
一旦定位了A服务组件,那么可以继续进行如下的流程:
Binder内存操作
逻辑过程
在处理binder transaction的过程中,相关的内存操作如下所示:
配合上面的流程,内存操作的逻辑过程如下:
- 在binder client的用户空间中,发起transaction的一方会构建用户数据缓冲区(包括两部分:实际的数据区和offset区),把想要传递到server端的数据填充到缓冲区并封装在binder_transaction_data数据结构中。
- binder_transaction_data会被copy到内核态,binder驱动会根据它计算出本次需要binder通信的数据量。
- 根据binder通信的数据量在server进程的binder VMA分配数据缓冲区(binder buffer是这个缓冲区的控制数据对象),同时根据需要也会分配对应的物理page并建立地址映射,以便用户空间可以访问这段buffer的数据。
-
建立内核地址空间的映射,把用户空间的binder数据缓冲区拷贝到内核中,然后释放掉该映射。
-
在把binder buffer的数据传递到server用户空间的时候,我们需要一个binder_transaction_data来描述binder通信的缓冲区数据,这个数据对象需要拷贝到用户地址空间,而binder buffer中的数据则不需要拷贝,因为在上面步骤3中已经建立了地址映射,server进程可以直接访问即可。
主要的数据结构
struct binder_alloc用来描述binder进程内存分配器,该数据结构的主要成员如下表所示:
成员变量 | 描述 |
---|---|
vma | binder内存对应的VMA |
vma_vm_mm | binder进程对应的地址空间描述符 |
buffer | 该binder proc能用于binder通信的内存地址。该地址是mmap的用户空间虚拟地址。 |
buffers | 所有的binder buffers(包括空闲的和正在使用的) |
free_buffers | 空闲binder buffers的红黑树,按照size排序 |
allocated_buffers | 已经分配的binder buffers的红黑树,key是buffer address |
free_async_space | 剩余的可用于异步binder通信的内存大小。初始化的时候配置为2M(整个binder内存的一半) |
pages | binder内存区域对应的page们。在reclaim binder内存的时候 |
buffer_size | 通过mmap映射的,用于binder通信的缓冲区大小,即binder alloc管理的整个内存的大小。 |
pid | Binder proc的pid |
struct binder_buffer用来描述一个用于binder通信的缓冲区,该数据结构的主要成员如下表所示:
成员变量 | 描述 |
---|---|
entry | 挂入binder alloc buffer链表(buffers成员)的节点 |
rb_node | 挂入binder alloc红黑树的节点:如果是空闲的buffer,挂入空闲红黑树,如果是已经分配的,挂入已分配红黑树。 |
transaction | Binder缓冲区都是用于某次binder transaction的,这个成员指向对应的transaction。 |
target_node | 该buffer的去向哪一个node(service组件) |
data_sizeoffsets_size | Binder缓冲区的数据区域的大小以及offset区域的大小。 |
user_data | 该binder buffer的用户空间地址 |