Android Binder 机制原理

Binder是Android系统进程间通信(IPC)方式之一。Linux已经拥有的进程间通信手段包括(Internet Process Connection):Pipe、Signal、Trace、Socket、Message、Share Memory 和Semaphore

本文将详细介绍Binder作为Android主要的IPC方式的优势。

一、引言

基于Client-Server的通信方式广泛用于从互联网和数据库访问到嵌入式手持设备内部通信等各个领域。智能手机平台特别是Android系统中,为了向应用开发者提供丰富多样的功能,这种通信方式更是无处不在,诸如媒体播放,视频音频的捕获,到各种让手机更智能的传感器,加速度,温度,光亮度等,都有不同的server负责管理,应用程序只需要做为Client与这些server建立连接便可以使用这些服务,花很少的时间节能开发出令人炫目的功能,Client-Server方式的广泛采用 是对进程间通信机制是一个挑战,目前linux支持的IPC包括传统的管道,System V IPC ,即消息队列,内存共享,信号量,以及socket中只有Socket支持client-server的通信方式。当然也可以在这些底层机制上架设一套协议来实现Client-Server通信,但这样增加了系统的复杂性,在手机上这种条件复杂,资源稀缺的环境下可靠性也难以保证。

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

表1 各种IPC方式数据拷贝次数

还有一点是出于安全性的考虑。终端用户不希望从网上下载的程序在不知情的情况下偷窥的隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等,传统的IPC没有任何安全措施,完全依赖上层协议来确保。首选传统的IPC的接收方法无法获得对方进程可靠的UID和PID,PID是用户进程ID,从而无法鉴别对方的身份。Android为每一个安装好的应用程序分配了自己的UID,故进程的UUID是鉴别进程身份的重要标志。使用传统的IPC只能由用户在数据包里面填入UUID和PPID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接口入点是开放的,无法建立私有通道。

比如命名管道的名称,systemV的键值,socket的ip地址或者是文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接受对方地址获得连接。

基于以上原因,Android需要建立一套新的IPC机制来满足对通信方式,传输性能和安全性的要求。这就是Binder。

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

二、面向对象的BinderIPC

Binder 使用的Client-Server通信方式:一个进程作为Server提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务。多个进程作为Client向Server发起服务请求。获取所需服务。要想实现Client-Server通信必须实现以下两点:

  • 一是Server必须有确定的访问接入点或者说地址来接受Client的请求,并且Client可以通过某种途径获取Server地址;
  • 二是制定Command-Reply协议来传输数据。例如在网络通信中Server的访问接入点就是Server的主机的IP和端口号,传输协议为TCP协议。
  • 对Binder而言,Binder可以看成是Server提供的某个实现特定服务的访问接入点,Client通过这个地址想Server发送请求来使用该服务;
  • 对Client而言,Binder可以看成是通向Server的管道入口,要想和某个Server通信必须先建立这个管道并获得管道入口。

与其他的IPC不同,Binder使用了面向对象的思想来描述接入点的Binder及其在Client中的入口:Binder是一个实体位于Server中的对象,该对象提供了一套方法用于实现对服务的请求,就像类的成员函数。遍布于Client中的入口可以看成指向这个Binder对象的指针,一旦获得了这个指针,就可以调用该对象的方法访问Server.

  • 在Client看来,通过Binder指针调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别。尽管前者的实体位于Server端中,而后者位于本地的内存中,指针是C++的术语,更通常的说法是引用。即Client通过Binder的引用访问Server。而软件领域另一个术语句柄,也可以来表述Binder在Client中的存在方式
  • 从通信角度来看,Client中的Binder也可以看做是Server Binder的代理。

在本地代表远端Server为Client提供服务。

本文中会使用引用和句柄这两个术语。

  • 面向对象思想的引入,将进程间通信转换为通过对某个Binder对象的引用调用该对象的方法,而其特别之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。最诱人的是,这个引用和java里应用一样既可以是强类型,也可以是弱类型。而且可以从一个进程传给其他的进程,让大家都能访问同一个Server。就想将一个对象引用复制给另一个引用一样。Binder模糊了进程边界。淡化了进程间通信的过程。整个系统仿佛运营于同一个面向对象的程序之中。形形色色的Binder对象以及星罗棋布的引用仿佛粘接各个应用程序的胶水,这也是Binder在英文里的原意。
  • 当然面向对象针只是对应用程序而言,对于Binder驱动和内核其他模块一样使用C语言实现,没有类和对象的概念,Binder驱动为面向对象的进程间通信提供底层技术支持。

三、Binder通信模型

Binder框架定义了四个角色:Server、Client、ServiceManager,以及Binder Driven。

其中Server,Client和SMgr运行与用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似,Server是服务器,Client是客户终端,SMgr是域名服务器(DNS),驱动是路由器。

3.1 Binder驱动

和路由器一样,Binder驱动虽然默默无闻,确实通信的核心。尽管名字叫驱动,实际上和硬件设备没有任何关系,知识实现方式和设备驱动程序是一样的。它工作于内核态,驱动负责进程之间Binder通信的建立。Binder在进程之间传递,Binder引用计数管理,数据包在进程之间的传递和交互等一些列底层支持。

3.2 ServiceManager与实名Binder

和DNS类似,SMgr的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder实名叫Binder,就先每个网站除了IP地址外还有自己的网址。Server创建了Binder实体,为其取一个字符形式的可读易记的名字,将这个Binder连名字同数据包以数据包的形式通过Binder驱动发送给SMgr,通知SMgr注册一个名字叫做test的Binder,它位于某个Server中,驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgr。SMgr接受数据包后,从中取出名字和引用填入一张查找表中。

SMgr是一个进程,Server是另一个进程,Server想SMgr注册Binder必然会涉及到进程间的通信。当前实现的是进程间通信却又要用到进程间通信,这就好像是先有鸡还是先有蛋。Binder的实现比较巧妙,预先创造一只鸡来孵蛋,SMgr和其他进程同样采用Binder通信,SMgr是Server端,有自己的Binder对象,其他进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。SMgr提供的Binder比较特殊,它没有名字也不需要注册,当前一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成为SMgr的时候,Binder驱动会自动为它创建Binder实体,这就是那只预先造好的鸡。其次这个Binder的应用在所有Client中都固定为0,而无需通过其他的手段获得,也就是说,一个Server若要想SMgr注册自己的Binder就必须通过0这个引用号和SMgr的Binder通信,类比网络通信,0号引用就好比域名服务器和地址。你必须先手动或配置好,要注意这里说的Client是相对SMgr而言的。一个应用程序可能是个提供服务的Server,但是对SMgr来说,它仍然是一个Client。

3.3 Client获得实名的Binder引用

Server向SMgr注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了,Client也保留的0号引用向SMgr请求访问某个Binder:我申请获得名字叫张三的Binder引用。SMgr收到这个链接请求,从请求的数据包里获得Binder名字,在查找里找到该名字对应的条目,从条目中取出Binder的引用。将该引用作为回复发送起请求的Client。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就像Java里一个对象存在多个引用一样。而且类似的这些指向Binder实体就不会被释放掉,通过以上的过程可以看出,SMgr像个火车的代售点,手机了所有的火车的车票,可以通过它购买到生做各趟火车的票,得到某个Binder的引用。

3.4匿名Binder

并不是所有的Binder都需要注册给SMgr广而告之的。Server端可以通过已经建立的Binder连接创建的Binder实体传给Client,当然这条建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder的连接必须是通过实名Binder实现,由于这个Binder没有下你给SMgr注册名字,所以是一个匿名Binder。Client将会受到这个匿名的Binder引用,通过这个引用向位于Server中的实体发送请求,匿名Binder为通信双方建立一条秘密通道,只要Server没有吧匿名的Binder发给别的进程,别的进程就无法通过穷举或者是猜测等任何方式获得该Binder的引用。向该Binder发送请求。

四、Binder内存映射和接受缓存区管理。

暂且撇开Binder,考虑一下传统的IPC方式中,数据是怎样从发送端到达接收端的呢?通常的做法是,发送方将准备好的数据存放在缓存区中,调用API通过系统调用进入内核中。内核服务器在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中,接受方读取数据时也要提供一块缓存区,内核将数据从缓存区拷贝到接收方提供的缓存区中并唤醒接收线程,完成一次数据发送。这种存储-转发机制有两个缺陷,首先是效率底下,需要做两次拷贝:用户空间—->内核空间–>用户空间。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝,在此过程中,如果使用了高端内存(high memory),这种拷贝需要临时建立/取消页面映射,造成性能损失,其次是接受数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量打的空间或者先调用API接受消息获得消息体大小。再开辟适当的空间接受消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder采用一种全新的策略:由Binder驱动负责管理数据接受缓存,我们注意到Binder驱动实现了mmap()通常用在屋里储存介质的文件系统中上,而像Binder这样没有物理介质,村崔用来通信的字符设备没必要支持mmap(),Binder驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收和缓存空间,先看mmap()是如何使用的: fd = open(“/dev/binder”,O_RDWR); mmap(NULL,MAP_SIZE,PORT_READ,MAP_PRIVATE,fd,0); 这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区,mmap()的返回值是内存映射在用户空间的地址,不过这段空间是驱动管理,用户不必也不能直接访问。映射类型为PORT_READ,只读映射。

节后缓存区映射后皆可以作为缓存池接收和存放数据了,前面说过,接收数据包的结构为binder_transaction_data,但这试试消息头,真正的有效负荷位于data.buffer所指的内存中,这片内存不需要接收方提供。恰恰是来自mmap()映射的这片缓存池,在数据从发送方向接收方拷贝是,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区中复制过来。要注意的是,存放binder_transaction_data结构本身以及表4中所有消息的内存空间还是得由接受者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池重获取目的存储区,一旦缓存池耗竭将产生无法预期的结果。

有分配必然有释放,接受放在处理完数据包之后,就要通知驱动释放data.buffer所指的内存区。在介绍Binder协议时已经提到,这是由命令BC_FREE_BUFFER完成的。

通过上面的介绍可以看到,驱动为接收方分担了最违反所的任务:分配/释放大小不等,难以预测的有效负荷缓存区,而接收方只需要提供缓存在存放大小固定,最大空间可以预测的消息头即可。在效率上,由于mmap()分配的内存是映射在接收方的用户空间里,所有总体效果就相当于对有效符合数据做了一次发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核暂存这个步骤,提升了一倍的性能。顺便在提一点,Linux内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到另一个用户空间,为了实现用户空间的拷贝,mmap()分配的出了映射进了接收方的进程里,还映射进了内核空间,所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的秘密。

五、Binder 接收线程管理

Binder通信实际上是位于不同进程中之间的线程之间的通信。假如进程S是Server端,提供Bidner实体,线程T1从Client进程C1中通过Binder的引用向进程S发送请求。S为了处理这个请求需要启动线程T2,而此时线程T1处于接受返回数据的等待状态。T2处理完请求就会将处理结果返回给T1,T1,被唤醒得到处理结果,在这个过程中,T2仿佛在进程S中的代理,代表T1执行任务,而给T1的感觉就是像穿越到S中执行一段代码有回到了C1,为了使这种穿越更加真实,驱动会将T1的一些属性赋给T2,特别是T1的优先级高,这样T2会使用和T1类似的时间完成任务,很多资料会用线程迁移来形容这种现象,但是这容易让人产生误会。一来线程根本不可能在进程之间跳来跳去,二来T2出了和T1优先级一样,其他没有什么特别之处,包括身份,打开文件,栈大小,信号处理,私有数据等等。

对于Server进程S,可能会有许多Client同事发起请求,为了提高效率往往开辟线程池并发处理收到请求。怎样使用线程池实现并发处理呢?这和具体的IPC机制有关系。拿socket举例,Server端的Socket设置为侦听模式,有一个专门的线程使用该Socket侦听来自Client的连接请求,即阻塞在accept()上。这个Socket就像是一只会蛋的鸡,一旦受到来自Client的请求就会生一个蛋,创建新Socket并从accept()返回。侦听线程从线程池中启动一个工作线程并将刚下的蛋,交给该线程。后续业务处理就由该线程完成,并通过这个与Client实现交互。

可是对于Binder来说,既没有侦听模式也不会下单,怎样管理线程池呢?一种简单的做法是,不管三七二十一,先创建一堆线程,每个线程都用BINDER_WRITE_READ命令读Binder,这些线程会阻塞在驱动为该Binder设置的等待队列上,一旦有来此Client的数据驱动会从队列中唤醒一个线程来处理,这样做简单直观,省去了线程池,但是一开始就创建一堆线程就有点浪费资源,于是Binder协议引入了专门的命令或者消息帮助用户管理线程池,包括:

  • INDER_SET_MAX_THREADS
  • BC_REGISTER_LOOP
  • BC_EXIT_LOOP
  • BR_SPAWN_LOOPER

首先,要管理线程池就要知道池子有多大,应用程序通过INDEX_SET_MAX_THREADS告诉驱动最多可以创建几个线程,以后每个线程都在创建,进入主循环,退出主循环是都分别要使用,BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驱动,以便收集和记录当前线程池的状态,每当驱动接受完数据包返回读Binder线城时,都要检查一下是不是已经没有闲置的线程了。如果是,而且线程总数不会超出线程池的最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息,告诉用户线程即将不够用了,请再启动一些,否则下一个请求可能不及时响应。新线程一启动又会通过BC_XXX_LOOP告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,及时处理请求。

关于工作线程的启动,Binder驱动还做了一点小小的优化。当进程P1的线程T1,向进程P2发送请求的时候,驱动会先查看一下线程T1是否也正处理来自P2某个线程请求但是尚未完成,没有回复的。这种情况通常发生在两个进程都有Binder实体并互相发送请求时。假如驱动在进程P2中发现了这样的线程,比如T2,就会要求T2来处理T1的这次请求。因为T2既然向T1发送了请求尚未得到返回包状态,这时候可以顺便让T2做点事情,总比等在哪里闲着好。而且如果T2不是线程池中的线程还可以为线程池分担部分工作。减少线程的使用率。

六、数据包接受队列与(线程)等待队列管理

通常数据传输的接收端有两个队列,数据包接受队列和线程等待队列,用以缓解供需矛盾。当超市里的进货太多。货物会堆积在仓库里;购物的人太多,会排队等待在收银台,道理是一样的,在驱动中,每个进程都有一个全局的接受队列,也叫to-do队列,存放不是发往特定线程包的数据包,相对应的有一个全局等待队列,所有等待从全局接收队列里的接收数据的线程在该队列里排队,每个线程都有自己的私有to-do队列。存放发送给该线程的数据包,相对应的每个线程都有各自的私有等待队列,专门用于本线程等待接收自己的to-do队列里的数据。虽然名叫队列,其实线程私有等待队列中最多只有一个线程,即他自己。

由于发送时没有特别的标记,驱动怎么判断哪些数据包该送去全局to-do队列,哪些数据包该送入特定线程to-do队列呢?这里有两条规则,

  • 规则1:Client发个Server请求数据包都是提交到Server进程的全局to-do队列,不过有个特例,就是上节谈到的Binder对工作线程启动的优化,经过优化,来自T1的请求不是提交给P2的全局to-do队列,而是送入了T2的私有to-do队列.
  • 规则2:对同步请求的返回数据包(由BC_REPLAY发送的包)都是发送到发起请求的线程的私有to-do队列中,如上面的例子,如果进程P1的线程T1发给进程P2的线程T2的是同步请求,那么T2返回的数据包将送进T1的私有to-do队列而不会提交到P1的全局队列。

数据包进入接受队列的潜规则也就决定了线程进入了等待队列的潜规则,即一个线程只要不接受返回数据包则应该在全局等待中等待新任务,否则就应该在其私有的等待队列中等待Server的返回数据。还是上面的例子,T1在向T2发送同步请求后就必须等待在它的私有等待队列中,而不是在P1的全局等待队列中排队。否则将得不到T2的返回数据包。

这些潜规则是驱动对Binder通信双方施加的限制条件,体现在应用程序上就是同步请求交互过程中的线程一致性:

  • Client端,等待返回包的线程必须是发送请求的线程,而不能有一个线程发送请求包,另一个线程等待接收包,否则将收不到返回包;
  • Server端,发送对应返回数据包的线程必须是收到请求数据包的线程。否则返回的数据包将无法送交发送请求的线程。这是因为返回数据包的目的Binder不是用户指定的。而是驱动记录在收到请求数据包的线程里,如果发送返回包的线程里不是收到请求包的线程驱动将无从知晓返回包送往何处。

接下来探讨一下Binder驱动是如何递交同步交互和异步交互的。我们知道,同步交互和异步交互的区别是同步交互的请求端在发出请求数据包后交互结束,对于这两种交互的请求数据包,驱动可以不管三七二十一,统统丢到接收端的to-do队列中一个个处理,但驱动并没有这样做,而是对异步交互做了限流,令其为同步交互让路,具体做法是:

  • 对于某个Binder实体,只要有一个异步交互没有处理完毕,例如正在被某个线程处理或还在任意一条to-do队列中排队,那么接下来发给实体的异步交互包将不再投递到to-do队列中,二十阻塞在驱动为实体开辟的异步交互接受队列(Binder节点的async_todo域)中,但这期间同步交互依旧不收限制直接进入to-do队列获得处理。一直到该异步交互处理完毕,下一个异步交互方可以脱离异步交互队列进入to-do队列中。之所以要这么做是因为同步交互的请求端需要等待返回包,必须迅速处理完毕以免影响请求端的响应速度,而异步交互属于发射后不管,稍微延迟一点不会阻塞其他线程,所以用专门队列将过多的异步交互暂存起来,以免突发大量异步交互挤占Server端的处理能力,或者好近线程池力的线程,进而阻塞同步交互。

原文发布于微信公众号 - 数据库SQL(SQLdba)

原文发表时间:2018-03-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Code_iOS

iOS 网络编程(二)TCP协议小结

两个应用程序通过T C P连接交换8 bit字节构成的字节流。T C P不在字节流中插入记录标识符

471
来自专栏分布式系统进阶

记一次Kafka集群的故障恢复Kafka源码分析-汇总

723
来自专栏JAVA烂猪皮

大型分布式网站架构:缓存在分布式系统中的应用

缓存是分布式系统中的重要组件,主要解决高并发,大数据场景下,热点数据访问的性能问题。提供高性能的数据快速访问。

802
来自专栏扎心了老铁

使用beanstalkd实现定制化持续集成过程中pipeline

持续集成是一种项目管理和流程模型,依赖于团队中各个角色的配合。各个角色的意识和配合不是一朝一夕能练就的,我们的工作只是提供一种方案和能力,这就是持续集成能力的服...

2997
来自专栏程序猿

面试问题之mysql修改哪些配置文件可以进行优化?

配置文件中具体修改的内容是什么呢?要是面试官问你,你该怎么回答?你想下,你坐在一间屋子里。 服务器的mysql性能优化,有两个大...

2667
来自专栏即时通讯技术

网络编程懒人入门(三):快速理解TCP协议一篇就够1、前言2、系列文章3、参考资料4、TCP 协议的作用5、TCP 数据包的大小6、TCP 数据包的编号(SEQ) 7、TCP 数据包的组装 8、慢启动

本系列文章的前两篇《网络编程懒人入门(一):快速理解网络通信协议(上篇)》、《网络编程懒人入门(二):快速理解网络通信协议(下篇)》快速介绍了网络基本通信协议及...

853
来自专栏北京马哥教育

LINUX上MYSQL优化三板斧

云豆贴心提醒,本文阅读时间7分钟 现在MySQL运行的大部分环境都是在Linux上的,如何在Linux操作系统上根据MySQL进行优化,我们这里给出一些通用简...

2577
来自专栏白老大的专栏

腾讯云 Redis 集群版配置管理揭秘 ( 上 )

腾讯云 Redis(CRS)集群版已经有数千用户,售出数十 T 容量,那么 CRS 是如何做配置管理的呢?通用的集群系统都需要做配置管理分发,成员健康度检查,希...

8481
来自专栏MYSQL轻松学

MySQL系列优化(一)

MYSQL优化是一个非常大的课题,这篇文章主要介绍了跟MYSQL相关的4个方面,如果想深入研究可以查下相关资料。 ---- 一、服务器级别优化 二、操作系统级别...

2735
来自专栏北京马哥教育

数据库基础知识:数据库中的约束和三大范式

? 一.数据库中的范式: 范式, 英文名称是 Normal Form,它是英国人 E.F.Codd(关系数据库的老祖宗)在上个世纪70年代提出关系数据库模型后...

2717

扫描关注云+社区