Binder技术学习总结

Posted by lizubing1992 on 02-12,2019

1. Binder的优势

1.1内存拷贝次数只有一次,而且易于控制

1.2 出于安全性考虑

传统的IPC通信没有任何安全措施,完全依赖上层的通信协议,无法获取到对方可靠的UID/PID,无法鉴别身份,而且接入点是开放的,只要知道就可以进行通信,无法建立私有通道

Binder是基于Client-Server通信模式,传输只要一次拷贝,发送方需要添加UID/PID身份,既支持实名Binder也支持匿名的Binder,安全性高

2.通信协议

对Binder而言,Binder可以看成Server提供的实现某个特定服务的访问接入点, Client通过这个‘地址’向Server发送请求来使用该服务;对Client而言,Binder可以看成是通向Server的管道入口,要想和某个Server通信首先必须建立这个管道并获得管道入口。可以用 引用或者句柄来描述client中的Binder的存在

3.通信模型

Binder框架定义了四个角色:Server,Client,ServiceManager以及Binder驱动。Server,Client,ServiceManager运行在用户空间,驱动运行在内核空间,和互联网类似:Server服务器,Client客户端,ServiceManager域名服务器,驱动为路由器。

3.1 Binder驱动

提供open(),mmap(),poll(),ioctl()等标准的文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问它。

驱动主要是负责进程间Binder通信的建立,Binder在进程之间的传递,Binder引用计数的管理,数据包在进程之间的传递和交互等一系列底层支持。

接口协议主要功能通过ioctl()接口实现的,没有提供read(),write()接口,因为ioctl()灵活,能够一次调用实现先读后写以满足同步的交互,代码位于driver/misc/binder.c中

3.2 ServiceManager 与实名的Binder

ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder称为实名Binder。ServiceManager收到数据包之后,取出对应的名字和引用填入一张查找表总。

ServiceManager是一个进程,Server也是一个进程,两者通信采用的是Binder,ServiceManager是一个Server,各个提供服务的Server是Client的。预设好了0号就是引用地址

3.3 Client获得实名Binder的引用

Client和ServiceManager之间也是进程通信,使用也是保留的0号引用向ServiceManager请求访问某个Binder。ServiceManager接收到这个连接请求,在查找表中根据名字找到对应的条目,取出对应的Binder的引用,把该引用作为回复发送给Client

3.4 匿名Binder

Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,而对应的连接也是通过实名Binder实现的,由于没有向ServiceManager注册名字,是匿名的Binder.

4. Binder协议

Binder协议基本数据结构是(命令+数据),使用ioctl(fd,cmd,arg)函数实现交互

比较常见的命令是BINDER_WRITE_READ,改命令包含读和写部分,异步命令 read_size 置0,读数据 write_size 置0,发送数据和同步等待两者写上

4.1 BINDER_WRITE_READ写操作

写操作的数据格式也是(命令+数据)

比较常见的是EC_TRANSACTION/BC_REPLY 命令对,Binder请求和应答数据就是通过这对命令发送给接收方的,其承载的数据包在结构体struct binder_transaction_data中定义的,如果flag域为TF_ONE_WAY位为1则为异步交互,表示Client端发送请求交互即结束,Server端不再返回BC_REPLY数据包,否则Server会返回BC_REPLY数据包,等待其接收才算完成一次交互

4.2 BINDER_WRITE_REASD:从Binder读出数据

结构同样是(命令+数据)

和写数据一样的,最重要的消息是BR_TRANSATION或者BR_REPLY,表明收到一个格式为binder_transation_data的请求数据包(BR_TRANSACTION)或者返回数据包(BR_REPLY)

4.3 struct binder_transactin_data : 收发的数据结构

offset_size 和data_offsets主要是用于存放binder在数据中的偏移量

使用数组data.offsets存放用户数据中的每个Binder相对的data.buffer的偏移量,用offsets_size表示这个数组的大小。驱动在发送数据包时会根据data.offsets和offsets_size 将散落在data,buffer中的Binder找出来,并一一为它们创建相关的数据结构

而对于接收方,真正的用户数据存放在data.buffer所指向的缓存区中的,如果存在多个也是通过data.offsets和offsets_size指出每个Binder的位置和总个数

5.Binder的表述

Binder分布在系统的部分

应用程序进程:分别位于Server和Client进程

Binder驱动:分别管理为Server端的Binder实体和Client端的引用

传输数据:由于Binder可以跨进程传递,需要在传输数据中予以表述

5.1 Binder在应用程序中的表述

Binder本质上只是一种底层的通信方式,和具体的服务没啥关系。为了提供具体的服务,Server必须提供一套接口函数以便Client通过远程访问各种服务,这时通常采用Proxy代理设计模式:接口函数定义在抽象类中,Server和Client以抽象类为基类实现所有接口的函数,而Server是真正的实现

5.1.1 Binder 在Server端的表述——Binder实体

采用继承方式以接口类和Binder抽象类为基类构建Binder在Server中的实体,实现所有的虚函数,包括公共接口函数以及数据包处理函数,onTransact() ,这个函数会接收来自Client的binder_transaction_data的数据包。该结构中有一个code包含了这次请求的接口函数编号,onTransaction会case-by-cas解析code值,从数据包中取出函数参数,并调用相应的接口实现函数,执行完毕之后,如果需要返回数据,就要在构建一个binder_transaction_data包返回数据包填入即可

Binder实体要以Binder传输结构flat_binder_object形式发送给其它的进程才能建立Binder通信,而Binder实体指针就存放在改结构的handle句柄域中,驱动根据Binder位置数组从传输数据中获取该Binder的传输结构,为它创建位于内核中的Binder节点,将Binder实体指针记录在该节点中,如果接下来有其他进程向该Binder发送数据,驱动会根据节点中记录的信息,将Binder实体指针填入binder_transaction_data的target.ptr中返回给接收线程。

接收线程从数据包中取出该指针,reinterpret_cast成抽象类并调用onTransact()函数,由于是一个虚函数,不同的Binder有不同的实现,从而调用不同的Binder实体提供onTransact()

5.1.2 Binder在Client端的表述-Binder引用

Client端的Binder同样要继承Server提供的公共接口类并实现公共函数。这仅仅是对远端函数调用的包装:将函数参数打包,通过Binder向Server发送申请并等待返回值。Client端的Binder还要知道Binder实体的相关信息,即对Binder实体的引用。改引用或是由ServerManager转发过来,对实名Binder的引用,或是由另一个进程直接发送过来的,对匿名Binder的引用。

公共接口函数的包装方式:创建一个binder_transaction_data数据包,将其对应的编码填入code域,将调用函数的所需要的参数填入data.buffer指向缓存中,Binder实体的引用填入数据包的target.handlez中。

这里和Serverd的区别是target是一个联合体,包括ptr和handle两个成员,前者用于接收数据包的Server,指向Binder实体对应的内存空间,后者用于作为请求方的Client,存放Binder实体的引用,告知驱动包将路由给哪一个实体

5.2 Binder在传输总的表述

传输过程中Binder是一种结构flat_binder_object表示

Binder在Server端是Binder实体,在CLient端是Binder引用,在传输的过程中,需要Binder驱动把Binder实体转化为对应的引用,这样处理也是为了安全考虑,由Binder驱动生成引用,能确保Server存在,引用是合法的

5.2.1 文件形式的Binder

将文件看成是一个Binder实体,进程打开的文件号看成Binder的引用。

从Binder的角度,linux在内核创建的打开文件描述结构struct file是Binder的实体,打开文件号是该进程的对该实体的引用。我们知道打开文件号是个局限于某个进程的值,跨进程就没有意义了,这个时候就需要Binder驱动做转化。驱动在接收Binder进程控件创建一个新的打开文件号,将它与已有的打开文件描述结构struct file勾连上,从此该Binder实体有多了一个引用

之所以这么做,还是出于安全性考虑

5.3Binder 在驱动中的表述

预留0号引用给ServiceManager进程
管理和处理Binder的数据转化

5.3.1 Binder实体在驱动中的表述

驱动中的Binder实体也叫 “节点”,隶属提供实体的进程,由struct binder_node表示

每个进程都有一颗红黑树用于存放创建好的节点,以Binder在用户控件的指针作为索引。每当在传输数据中侦测到一个代表Binder实体的flat_binder_object ,先以该结构的binder指针作为索引搜索红黑树,如果没有找到就创建一个新的节点添加到数中,对于同一个进程来讲内存地址是唯一的,所以不会混乱。

5.3.2 Binder引用在驱动中的表述

和实体一样,Binder引用也是驱动根据传输数据中的flat_binder_object创建的,隶属于获得该引用的进程,用struce binder_refd结构体表述

就像一个对象有很多指针一样,同一个Binder实体可能有很多引用,不同的是这些引用可能分布在不同进程中,和实体一样,每一个进程使用红黑树存放所有正在使用的引用,而Binder引用可以通过两个键值索引:

对应实体在内核中的地址:创建于内核中binder_node结构的地址,这个地址是唯一的;用户进程中的地址可能会重合,不能作为索引

引用号:引用号是驱动为引用分配的一个32位标识,在一个进程里面是唯一的,和进程打开文件号类似。引用号将返回给应用程序,可以看作Binder引用在用户进程中的句柄,0号句柄保留给ServiceManager,其他的值由驱动动态分布。

6 Binder 内存映射和接收缓存区管理

传统IPC两次数据拷贝:用户空间->内核空间->用户空间这种拷贝需要临时建立/取消页面映射,造成性能损失,其次还不知道需要申请的缓存大小

Binder采用全新策略:由Binder驱动负责管理数据接收和缓存,主要是使用mmap()系统调用,这个对于字符调用是比较特殊,mmap()主要用户物理存储介质的文件系统上,Binder是用来创建数据接收的缓存空间

接收缓存区映射好后就可以作为缓存池和存放数据了,接收数据的结构为binder_transaction_data,这个仅仅是消息头,真正的数据在data.buffer所指向的内存中,这片内存是来至mmap()映射的这片缓存池中。

数据从发送方向接收方拷贝时,驱动会根据发送数据包大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间。将数据从发送缓存区复制过来,

驱动为接收方分担了最为繁琐的任务:分配/释放大小不等。难以预测的有效负荷缓冲区,而接收方只需要提供缓存来存放大小固定,最大空间可以预测的消息头即可。

在效率上,mmap()分配的内存映射是在接收方用户空间里面的,所有总体的效果就相当于对有效负荷数据做了一次发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中的暂存这个步骤,提升了一倍性能。

linux内核实际上没有从一个用户空间到另一个用户空间的直接数据拷贝函数,需要先copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到用户空间,mmap()分配的内存除了映射进接收方进程里,还映射进内核空间,所以调用copy_from_user()将数据拷贝进内核空间,也就相当于拷贝进接收方的用户空间,这就是Binder只需要一次拷贝的秘密

7. Binder接收线程管理

专有命令或消息帮助用户管理线程池

//最多可以创建多少线程
INDER_SET_MAX_THREADS
//创建线程
BC_REGISTER_LOOP
//进入主循环
BC_ENTER_LOOP
//退出主循环
BC_EXIT_LOOP
//告知用户线程即将不够用
BR_SPAWN_LOOPER

等待(堵塞)的过程中可能会去做一些事情

8. 数据包接收队列与(线程)等待队列管理

通常数据传输的接收端有两个队列:数据包接收队列和线程等待队列,用以缓解供需矛盾。就像超市里进货(数据包)太多,货物会积压在仓库;购物的人(线程)太多,会排队等待收银台。

驱动中,每个进程有一个全局的接收队列,to-do队列,存放不是发往特定的数据包;相应的有一个全局等待队列,所有等待从全局接收队列里接收的线程在该队列里排队

相应的每个线程也有自己私有的to-do队列,存放发送该线程的数据包;相应的也有私有的等待队列,专门用于本线程等待接收自己的to-do队列里的数据,其实等待队列只有它自己

规则1:Client发给Server的请求数据包都提交到Server进程的全局to-do队列,特例:Binder对工作线程的优化。T1的请求不是提交给P2的全局to-do队列,而是直接送入T2的私有队列中

规则2:同步请求的返回数据包(BC_REPLY发送的包)都发送到发起请求的线程的私有to-do队列

潜规则:发送和接收的线程必须是同一个,原因是目标Binder是由驱动指定的

异步交互让步同步交互,只要有一条异步交互没有完成,新的异步交互将存入阻塞队列(Binder节点的async_todo域),而同步却不影响,同步必须尽快处理完毕


0评论