Binder知识点

写在前面

本文为universus的Android Bander设计与实现 - 设计篇的一些摘要,强烈建议阅读原文!!

在阅读后,为了方便没时间时可以快速浏览加深记忆,将其中的大部分内容摘要、精简后整理了一下。如果您和我一样对Binder不是很了解,那么看这篇文章不会给您带来太多的帮助,强烈建议阅读原文!!


Binder与传统IPC的对比

通信方式

目前linux支持的IPC包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及socket中只有socket支持Client-Server的通信方式。

传输性能

socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。
消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程
共享内存虽然无需拷贝,但控制复杂,难以使用。

安全性

传统IPC没有任何安全措施,完全依赖上层协议来确保。 (无法向数据包中添加标志身份的信息(UID/PID),IPC节点开放)
首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。
使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。
其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,system V的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

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

面向对象的思想

Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口:

Server

Binder是一个实体位于Server中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。遍布于client中的入口可以看成指向这个binder对象的‘指针’,一旦获得了这个‘指针’就可以调用该对象的方法访问server。

Client

在Client看来,通过Binder‘指针’调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别,尽管前者的实体位于远端Server中,而后者实体位于本地内存中。

Client通过Binder的引用访问Server,软件领域另一个术语‘句柄(handle)’也可以用来表述Binder在Client中的存在方式。

Proxy

从通信的角度看,Client中的Binder也可以看作是Server Binder的‘代理’,在本地代表远端Server为Client提供服务。

Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。
这个引用和java里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给其它进程,让大家都能访问同一Server,就象将一个对象或引用赋值给另一个引用一样。
Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。
形形色色的Binder对象以及星罗棋布的引用仿佛粘接各个应用程序的胶水,这也是Binder在英文里的原意。

在这里插入图片描述
在这里插入图片描述

Binder框架中的四个角色

Binder框架定义了四个角色:Server,Client,ServiceManager(以后简称SMgr)以及Binder驱动。其中Server,Client,SMgr运行于用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似:Server是服务器,Client是客户终端,SMgr是域名服务器(DNS),驱动是路由器。

ServiceManager

SMgr的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。(和DNS类似)
注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址。

Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SMgr,通知SMgr注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgr。SMgr收数据包后,从中取出名字和引用填入一张查找表中。

Binder 协议

命令含义args
BINDER_WRITE_READ该命令向Binder写入或读取数据。参数分为两段:写部分和读部分。如果write_size不为0就先将write_buffer里的数据写入Binder;如果read_size不为0再从Binder中读取数据存入read_buffer中。write_consumed和read_consumed表示操作完成时Binder驱动实际写入或读出的数据个数。struct binder_write_read {
signed long write_size;
signed long write_consumed;
unsigned long write_buffer;
signed long read_size;
signed long read_consumed;
unsigned long read_buffer;
};
BINDER_SET_MAX_THREADS该命令告知Binder驱动接收方(通常是Server端)线程池中最大的线程数。由于Client是并发向Server端发送请求的,Server端必须开辟线程池为这些并发请求提供服务。告知驱动线程池的最大值是为了让驱动发现线程数达到该值时不要再命令接收端启动新的线程。int max_threads;
BINDER_SET_CONTEXT_MGR
将当前进程注册为SMgr。系统中同时只能存在一个SMgr。只要当前的SMgr没有调用close()关闭Binder驱动就不能有别的进程可以成为SMgr。
BINDER_THREAD_EXIT通知Binder驱动当前线程退出了。Binder会为所有参与Binder通信的线程(包括Server线程池中的线程和Client发出请求的线程)建立相应的数据结构。这些线程在退出时必须通知驱动释放相应的数据结构。
BINDER_VERSION获得Binder驱动的版本号。
Binder收发数据包结构:binder_transaction_data
cmd含义arg
BC_TRANSACTION
BC_REPLY
BC_TRANSACTION用于Client向Server发送请求数据;BC_REPLY用于Server向Client发送回复(应答)数据。其后面紧接着一个binder_transaction_data结构体表明要写入的数据。struct binder_transaction_data
BC_ACQUIRE_RESULT
BC_ATTEMPT_ACQUIRE
暂未实现
BC_FREE_BUFFER释放一块映射的内存。Binder接收方通过mmap()映射一块较大的内存空间,Binder驱动基于这片内存采用最佳匹配算法实现接收数据缓存的动态分配和释放,满足并发请求对接收缓存区的需求。应用程序处理完这片数据后必须尽快使用该命令释放缓存区,否则会因为缓存区耗尽而无法接收新数据。指向需要释放的缓存区的指针;该指针位于收到的Binder数据包中
BC_INCREFS
BC_ACQUIRE
BC_RELEASE
BC_DECREFS
这组命令增加或减少Binder的引用计数,用以实现强指针或弱指针的功能。32位Binder引用号
BC_INCREFS_DONE
BC_ACQUIRE_DONE
第一次增加Binder实体引用计数时,驱动向Binder实体所在的进程发送BR_INCREFS, BR_ACQUIRE消息;Binder实体所在的进程处理完毕回馈BC_INCREFS_DONE,BC_ACQUIRE_DONEvoid *ptr:Binder实体在用户空间中的指针
void *cookie:与该实体相关的附加数据
BC_REGISTER_LOOPER
BC_ENTER_LOOPER
BC_EXIT_LOOPER
这组命令同BINDER_SET_MAX_THREADS一道实现Binder驱动对接收方线程池管理。BC_REGISTER_LOOPER通知驱动线程池中一个线程已经创建了;BC_ENTER_LOOPER通知驱动该线程已经进入主循环,可以接收数据;BC_EXIT_LOOPER通知驱动该线程退出主循环,不再接收数据。
BC_REQUEST_DEATH_NOTIFICATION获得Binder引用的进程通过该命令要求驱动在Binder实体销毁得到通知。虽说强指针可以确保只要有引用就不会销毁实体,但这毕竟是个跨进程的引用,谁也无法保证实体由于所在的Server关闭Binder驱动或异常退出而消失,引用者能做的是要求Server在此刻给出通知。uint32 *ptr; 需要得到死亡通知的Binder引用
void **cookie:
与死亡通知相关的信息,驱动会在发出死亡通知时返回给发出请求的进程。
BC_DEAD_BINDER_DONE收到实体死亡通知书的进程在删除引用后用本命令告知驱动。void **cookie
offsets_size和data.offsets

跨进程传递的Binder混杂在应用程序发送的数据包里,数据格式由用户定义,如果不把它们一一标记出来告知驱动,驱动将无法从数据中将它们提取出来。于是就使用数组data.offsets存放用户数据中每个Binder相对data.buffer的偏移量,用offsets_size表示这个数组的大小。驱动在发送数据包时会根据data.offsets和offset_size将散落于data.buffer中的Binder找出来并一一为它们创建相关的数据结构。

对于接收方来说,该结构只相当于一个定长的消息头,真正的用户数据存放在data.buffer所指向的缓存区中。如果发送方在数据中内嵌了一个或多个Binder,接收到的数据包中同样会用data.offsets和offset_size指出每个Binder的位置和总个数。

在这里插入图片描述

Binder 的表述

Binder本质上只是一种底层通信方式,和具体服务没有关系。
为了提供具体服务,Server必须提供一套接口函数以便Client通过远程访问使用各种服务。
这时通常采用Proxy设计模式:将接口函数定义在一个抽象类中,Server和Client都会以该抽象类为基类实现所有接口函数,所不同的是Server端是真正的功能实现,而Client端是对这些函数远程调用请求的包装。
如何将Binder和Proxy设计模式结合起来是应用程序实现面向对象Binder通信的根本问题。

Binder 在Server端的表述 – Binder实体

做为Proxy设计模式的基础,首先定义一个抽象接口类封装Server所有功能,其中包含一系列纯虚函数留待Server和Proxy各自实现。
由于这些函数需要跨进程调用,须为其一一编号 (code),从而Server可以根据收到的编号决定调用哪个函数。
其次就要引入Binder了。Server端定义另一个Binder抽象类处理来自Client的Binder请求数据包,其中最重要的成员是虚函数onTransact()。该函数分析收到的数据包,调用相应的接口函数处理请求。

接下来采用继承方式以接口类和Binder抽象类为基类构建Binder在Server中的实体,实现基类里所有的虚函数,包括公共接口函数以及数据包处理函数:onTransact()。这个函数的输入是来自Client的binder_transaction_data结构的数据包。
前面提到,该结构里有个成员code,包含这次请求的接口函数编号。onTransact()将case-by-case地解析code值,从数据包里取出函数参数,调用接口类中相应的,已经实现的公共接口函数。
函数执行完毕,如果需要返回数据就再构建一个binder_transaction_data包将返回数据包填入其中。

那么各个Binder实体的onTransact()又是什么时候调用呢?这就需要驱动参与了。
前面说过,Binder实体须要以Binde传输结构flat_binder_object形式发送给其它进程才能建立Binder通信,而Binder实体指针就存放在该结构的handle域中。驱动根据Binder位置数组从传输数据中获取该Binder的传输结构,为它创建位于内核中的Binder节点,将Binder实体指针记录在该节点中。如果接下来有其它进程向该Binder发送数据,驱动会根据节点中记录的信息将Binder实体指针填入binder_transaction_data的target.ptr中返回给接收线程。接收线程从数据包中取出该指针,reinterpret_cast成Binder抽象类并调用onTransact()函数。由于这是个虚函数,不同的Binder实体中有各自的实现,从而可以调用到不同Binder实体提供的onTransact()。

Binder 在Client端的表述 – Binder引用

做为Proxy设计模式的一部分,Client端的Binder同样要继承Server提供的公共接口类并实现公共函数。
但这不是真正的实现,而是对远程函数调用的包装:将函数参数打包,通过Binder向Server发送申请并等待返回值。
为此Client端的Binder还要知道Binder实体的相关信息,即对Binder实体的引用。
该引用 (或是由SMgr转发过来的,对实名Binder的引用) ,(或是由另一个进程直接发送过来的,对匿名Binder的引用)。

由于继承了同样的公共接口类,Client Binder提供了与Server Binder一样的函数原型,使用户感觉不出Server是运行在本地还是远端。
Client Binder中,公共接口函数的包装方式是:创建一个binder_transaction_data数据包,将其对应的编码填入code域,将调用该函数所需的参数填入data.buffer指向的缓存中,并指明数据包的目的地,那就是已经获得的对Binder实体的引用,填入数据包的target.handle中。
注意这里和Server的区别:实际上target域是个联合体,包括ptr和handle两个成员,前者用于接收数据包的Server,指向 Binder实体对应的内存空间;后者用于作为请求方的Client,存放Binder实体的引用,告知驱动数据包将路由给哪个实体。
数据包准备好后,通过驱动接口发送出去。经过BC_TRANSACTION/BC_REPLY回合完成函数的远程调用并得到返回值。

Binder 在传输数据中的表述

Binder可以塞在数据包的有效数据中越进程边界从一个进程传递给另一个进程,这些传输中的Binder用结构flat_binder_object表示

成员含义
unsigned long type表明该Binder的类型,包括以下几种:
BINDER_TYPE_BINDER:表示传递的是Binder实体,并且指向该实体的引用都是强类型;
BINDER_TYPE_WEAK_BINDER:表示传递的是Binder实体,并且指向该实体的引用都是弱类型;
BINDER_TYPE_HANDLE:表示传递的是Binder强类型的引用
BINDER_TYPE_WEAK_HANDLE:表示传递的是Binder弱类型的引用
BINDER_TYPE_FD:表示传递的是文件形式的Binder
unsigned long flags该域只对第一次传递Binder实体时有效,因为此刻驱动需要在内核中创建相应的实体节点,有些参数需要从该域取出:
第0-7位:代码中用FLAT_BINDER_FLAG_PRIORITY_MASK取得,表示处理本实体请求数据包的线程的最低优先级。当一个应用程序提供多个实体时,可以通过该参数调整分配给各个实体的处理能力。
第8位:代码中用FLAT_BINDER_FLAG_ACCEPTS_FDS取得,置1表示该实体可以接收其它进程发过来的文件形式的Binder。由于接收文件形式的Binder会在本进程中自动打开文件,有些Server可以用该标志禁止该功能,以防打开过多文件。
union {
void *binder;
signed long handle;
};
当传递的是Binder实体时使用binder域,指向Binder实体在应用程序中的地址。
当传递的是Binder引用时使用handle域,存放Binder在进程中的引用号。
void *cookie;该域只对Binder实体有效,存放与该Binder有关的附加信息。

无论是Binder实体还是对实体的引用都从属与某个进程,所以该结构不能透明地在进程之间传输,必须经过驱动翻译。
例如当Server把Binder实体传递给Client时,在发送数据流中,flat_binder_object中的type是BINDER_TYPE_BINDER,binder指向Server进程用户空间地址。如果透传给接收端将毫无用处,驱动必须对数据流中的这个Binder做修改:将type该成BINDER_TYPE_HANDLE;为这个Binder在接收进程中创建位于内核中的引用并将引用号填入handle中。对于发生数据流中引用类型的Binder也要做同样转换。
经过处理后接收进程从数据流中取得的Binder引用才是有效的,才可以将其填入数据包binder_transaction_data的target.handle域,向Binder实体发送请求。

这样做也是出于安全性考虑:应用程序不能随便猜测一个引用号填入target.handle中就可以向Server请求服务了,因为驱动并没有为你在内核中创建该引用,必定会被驱动拒绝。唯有经过身份认证确认合法后,由‘权威机构’(Binder驱动)亲手授予你的Binder才能使用,因为这时驱动已经在内核中为你使用该Binder做了注册,交给你的引用号是合法的。

文件形式的 Binder

将文件看成Binder实体,进程打开的文件号看成Binder的引用。一个进程可以将它打开文件的文件号传递给另一个进程,从而另一个进程也打开了同一个文件,就象Binder的引用在进程之间传递一样。

一个进程打开一个文件,就获得与该文件绑定的打开文件号。从Binder的角度看,跨进程传递文件号与传递实体的引用,结构上一致。上文讲到传递引用时,驱动会在内核中先生成接收进程的对应引用–>同理,驱动在接收Binder的进程空间创建一个新的打开文件号,将它与已有的打开文件描述结构struct file勾连上,新建的打开文件号覆盖flat_binder_object中原来的文件号交给接收进程。接收进程利用它可以执行read(),write()等文件操作。

传递文件号,而非传递文件名的原因

1.对同一个打开文件共享的层次不同:使用文件Binder打开的文件共享linux VFS中的struct file,struct dentry,struct inode结构,这意味着一个进程使用read()/write()/seek()改变了文件指针,另一个进程的文件指针也会改变;而如果两个进程分别使用同一文件名打开文件则有各自的struct file结构,从而各自独立维护文件指针,互不干扰。
2.一些特殊设备文件要求在struct file一级共享才能使用,例如android的另一个驱动ashmem,它和Binder一样也是misc设备,用以实现进程间的共享内存。一个进程打开的ashmem文件只有通过文件Binder发送到另一个进程才能实现内存共享,这大大提高了内存共享的安全性,道理和Binder增强了IPC的安全性是一样的。

Binder 在驱动中的表述

1.驱动是Binder通信的核心,系统中所有的Binder实体以及每个实体在各个进程中的引用都登记在驱动中;
2.驱动需要记录Binder引用->实体之间多对一的关系;
为引用找到对应的实体;
3.在某个进程中为实体创建或查找到对应的引用;
4.记录Binder的归属地(位于哪个进程中);
5.通过管理Binder的强/弱引用创建/销毁Binder实体等等。

驱动为ServiceManager创建用于注册实名Binder的Binder实体,负责实名Binder注册过程中的进程间通信。驱动将所有进程中的0号引用都预留给该Binder实体,使用0号引用来注册实名Binder。

驱动通过binder_transaction_data中data.offset数组检测流经驱动的Binder。
Binder将对这些穿越进程边界的Binder检查传输结构的type域:

如果是BINDER_TYPE_BINDER或BINDER_TYPE_WEAK_BINDER则创建Binder的实体;
如果是BINDER_TYPE_HANDLE或BINDER_TYPE_WEAK_HANDLE则创建Binder的引用;
如果是BINDER_TYPE_HANDLE则为进程打开文件,无须创建任何数据结构。

Binder 内存映射和接收缓存区管理
传统IPC

发送方将数据放在缓存区,调用API通过系统调用进入内核中。
内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。
接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程。

缺点:

效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。
接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能(开辟尽量大的空间)或(先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体)。

Binder

发送方向接收方发送数据时,驱动根据发送数据包大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制到驱动中的缓存区。
驱动使用mmap()分配的内存映射在接收方的用户空间里,同时映射在了Linux内核中。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,即Binder只需一次拷贝。
接收方在处理完数据包后,通知驱动释放data.buffer所指向的内存区。

在这里插入图片描述

Servier端的Binder线程池

先创建一堆线程,每个线程都用BINDER_WRITE_READ命令读Binder。
这些线程会阻塞在驱动为该Binder设置的等待队列上,一旦有来自Client的数据驱动会从队列中唤醒一个线程来处理。

一开始就创建一堆线程有点浪费资源,于是Binder协议引入了专门命令或消息帮助用户管理线程池:

应用程序通过INDER_SET_MAX_THREADS告诉驱动最多可以创建几个线程。
以后每个线程在创建,进入主循环,退出主循环时都要分别使用BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驱动,以便驱动收集和记录当前线程池的状态。
每当驱动接收完数据包返回读Binder的线程时,检查闲置线程数量。如果数量不足,且线程总数不超出线程池最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息来通知Server创建用户线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值