深入浅出Binder细节

本文通过几个典型的binder通信过程来呈现其实现细节。

启动service manager

流程

Service manager进程和binder驱动的交互如下:

启动service manager

在安卓系统启动过程中,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 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:

client如何找到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的过程需要如下过程:

  1. binder client用户空间中的service组件代理(BpBinder)用句柄表示要访问的server中的service组件(BBinder)

  2. 对于每一个句柄,binder client内核空间使用binder ref对象与之对应

  3. binder ref对象会指向一个binder node对象
  4. binder node对象对应一个binder server进程的service组件

在我们这个场景中,binder ref是在client第一次通过ioctl和binder驱动交互时候完成的。这时候,binder驱动的binder_ioctl函数中会建立上面路由过程需要的完整的数据对象:

client如何找到service manager

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组件

Service manager是一个service组件管理中心,任何一个service组件都需要向service manager进行注册(add service),以便其他的APP可以通过service manager定位到该service组件(check service)。

数据对象综述

注册服务相关数据结构全图如下:

注册Service组件

配合上面的流程,binder驱动会为client和server分别创建对应的各种数据结构对象,具体过程如下:

  1. 假设我们现在准备注册A服务组件,绑定A服务组件的进程在add service这个场景下是client process,它在用户空间首先会创建了service组件对象,在递交BC_TRANSACTION的时候会携带service组件的信息(把service组件地址信息封装在flat_binder_object数据结构中)。

  2. 在系统调用接口层面,我们使用ioctl(BINDER_WRITE_READ)来完成具体transaction的递交过程。具体的transaction数据封装在struct binder_write_read对象中,具体如下图所示:

注册Service组件

  1. Binder驱动创建binder_transaction对象来控制完成本次binder transaction。首先要初始化transaction,具体包括:和谁通信(用户空间通过binder_transaction_data的target成员告知binder驱动transaction的target)、为何通信(binder_transaction_data的code)等

  2. 对于每一个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。

  3. 把一个BINDER_WORK_TRANSACTION_COMPLETE类型的binder work挂入client binder thread的todo list,通知client其请求的transaction已经被binder处理完毕,可以进行其他工作了(当然对于同步binder通信,client一般会通过read类型的ioctl进入阻塞态,等待server端的回应)。

  4. 至此,client端已经完成了所有操作,现在我们开始进入server端的数据流了。Binder驱动会把一个BINDER_WORK_TRANSACTION类型的binder work(内嵌在binder transaction)挂入binder线程的todo list,然后唤醒它起来干活。

  5. binder server端会使用ioctl(BINDER_WRITE_READ)进入读阻塞状态,等待client的请求到来。一旦有请求到来,Service manager进程会从binder_thread_read中醒来处理队列上的binder work。所谓处理binder work其实完成client transaction的向上递交过程。具体的transaction数据封装在struct binder_write_read对象中,具体如下图所示:

注册Service组件

需要强调的一点是:在步骤2中,flat_binder_object传递的是binder node,而这里传递的是handle(即binder ref,步骤4中创建的)

  1. 在Service manager进程的用户态,识别了本次transaction的code是add service,那么它会把(service name,handle)数据写入其数据库,完成服务注册。
  2. 从transaction的角度看,上半场已经完成。现在开始下半场的transaction的处理,即BC_REPLY的处理。和BC_TRANSACTION处理类似,也是通过binder_ioctl —> binder_ioctl_write_read —> binder_thread_write —> binder_transaction这个调用链条进入binder transaction处理流程的。
  3. 和上半场类似,在这里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服务组件的信息并建立其代理组件对象:

如何和Service组件通信

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服务组件,那么可以继续进行如下的流程:

如何和Service组件通信

Binder内存操作

逻辑过程

在处理binder transaction的过程中,相关的内存操作如下所示:

Binder内存操作

配合上面的流程,内存操作的逻辑过程如下:

  1. 在binder client的用户空间中,发起transaction的一方会构建用户数据缓冲区(包括两部分:实际的数据区和offset区),把想要传递到server端的数据填充到缓冲区并封装在binder_transaction_data数据结构中。
  2. binder_transaction_data会被copy到内核态,binder驱动会根据它计算出本次需要binder通信的数据量。
  3. 根据binder通信的数据量在server进程的binder VMA分配数据缓冲区(binder buffer是这个缓冲区的控制数据对象),同时根据需要也会分配对应的物理page并建立地址映射,以便用户空间可以访问这段buffer的数据。

  4. 建立内核地址空间的映射,把用户空间的binder数据缓冲区拷贝到内核中,然后释放掉该映射。

  5. 在把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的用户空间地址
赞(3)
未经允许不得转载:极客笔记 » 深入浅出Binder细节
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址