Android学习笔记1-2--通信2--Binder

来源:互联网 发布:nba赌球软件 编辑:程序博客网 时间:2024/06/15 01:17

IPC简介

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

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

在安全性上考虑,Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。

传统IPC没有任何安全措施,完全依赖上层协议来确保。传统IPC的接收方无法获得对方进程可靠的UID/PID(使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠导致容易被恶意程序利用),从而无法鉴别对方身份。
传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,system V的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于CS通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。

基于CS的通信方式广泛应用于从互联网和数据库访问到嵌入式手持设备内部通信等各个领域。在Android系统中诸如媒体播放、视音频频捕获以及各种让手机更智能的传感器(加速度,方位,温度,光亮度等)都由不同的Server负责管理,应用程序只需做为Client与这些Server建立连接便可以使用这些服务,花很少的时间和精力就能开发出令人眩目的功能。

Binder

要想实现CS通信据必须实现以下两点:

一是server必须有确定的访问接入点或者说地址来接受Client的请求,并且Client可以通过某种途径获知Server的地址;
二是制定Command-Reply协议来传输数据。例如在网络通信中Server的访问接入点就是Server主机的IP地址+端口号,传输协议为TCP协议。

与其它IPC不同,Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口:Binder是一个Server中的成员对象,该对象提供了一套方法用以实现对服务的请求。遍布于client中的入口可以看成指向这个binder对象的引用。从通信的角度看,Client中的Binder也可以看作是Server Binder的代理,在本地代表远端Server为Client提供服务。

面向对象思想的引入将进程间通信转化为通过对Binder对象的引用来调用其方法。独特之处在于Binder对象是个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。

Binder模型

Binder本质上只是一种底层通信方式,和具体服务没有关系。

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

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

Binder驱动

Binder驱动的代码位于linux目录的drivers/staging/android/binder.c中。尽管命名叫驱动但实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样:

它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。
驱动负责进程之间Binder通信的建立、Binder在进程之间的传递、Binder引用计数管理、数据包在进程之间的传递和交互等一系列底层支持。
驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现但不提供read、write接口,因为ioctl()灵活方便且能够一次调用实现先写后读以满足同步交互。

驱动是Binder通信的核心,系统中所有的Binder实体和Binder引用都登记在驱动中(驱动需要记录Binder实体和Binder引用之间一对多的关系)。每个Binder(不论实体或者引用)的隶属进程都有一棵红黑树用于存放创建好的节点,以Binder在用户空间的指针作为索引(引用通过对应实体在内核中的地址)。驱动里的Binder是什么时候创建的呢?首先为了实现实名Binder的注册,系统必须创建第一只鸡(为SMgr创建的)用于注册实名Binder的Binder实体,负责实名Binder注册过程中的进程间通信。然后驱动将所有进程中的0号引用都预留给该Binder实体,无须特殊操作即可以使用0号引用来注册实名Binder。接下来随着应用程序不断地注册实名Binder且不断向SMgr索要Binder的引用,不断将Binder从一个进程传递给另一个进程,越来越多的Binder以传输结构flat_binder_object的形式穿越驱动做跨进程的迁徙。由于binder_transaction_data中data.offset数组的存在,所有流经驱动的Binder都逃不过驱动的眼睛。

当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成SMgr时Binder驱动会自动为它创建Binder实体(就是那只预先造好的鸡),其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。类比网络通信,0号引用就好比域名服务器的地址,你必须预先手工或动态配置好。

无论是Binder实体还是Binder引用都从属与某个进程,所以该结构不能透明地在进程之间传输,必须经过驱动翻译。Binder将对这些穿越进程边界的Binder做如下操作(检查传输结构的type域):

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

ServiceManager

SMgr和其它进程同样采用Binder通信。SMgr是Server端(有自己的Binder成员对象,这个Binder比较特殊,它没有名字也不需要注册),Client(其它进程都是Client)需要通过这个SMgr的Binder引用来实现Binder的注册、查询、获取。也就是说一个Server若要向SMgr注册自己Binder需通过0这个引用号和SMgr的Binder通信。

SMgr的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。本质是注册时SMgr收数据包后从中取出名字和引用填入一张查找表中,请求时从表中查询。

Server注册实名Binder

Server创建了Binder实体且为其取一个字符形式且可读易记的名字,将这个跨进程的Binder连同名字以数据包的形式通过Binder驱动发送给SMgr,通知SMgr注册一个名叫张三的Binder且它位于某个Server中。注册了名字的Binder叫实名Binder(就象每个网站除了有IP地址外还有自己的网址)。Binder驱动为其创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgr。

Binder在Server端的表述(Binder实体)

做为Proxy设计模式的一部分,首先实现Proxy设计模式的基础

首先定义一个抽象接口封装Server所有功能(其中包含一系列纯虚函数留待Server和Proxy各自实现)。由于这些函数需跨进程调用就需为其逐个编号,从而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。

Client请求实名Binder

Client利用保留的0号引用向SMgr请求访问某个Binder:我申请获得名字叫张三的Binder的引用。SMgr收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。

Binder在Client端的表述(Binder引用)

做为Proxy设计模式的一部分,Client端的Binder同样要继承Server提供的公共接口类并实现公共函数。但他是对远程函数调用的包装(将函数参数打包),通过Binder(Client对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没有向SMgr注册名字就称为匿名Binder(并不是所有Binder都需要注册给SMgr广而告之的)。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder实现。Client将会收到这个匿名Binder的引用,通过这个引用向位于Server中的实体发送请求。匿名Binder为通信双方建立一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用而向该Binder发送请求。

Binder协议

Binder协议基本格式是:命令+数据,使用ioctl(fd, cmd, arg)函数实现交互。命令由参数cmd承载,数据由参数arg承载且随cmd不同而不同。具体的参考这里

Binder驱动进一步学习

Binder接收缓存区管理

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

这种存储-转发机制有两个缺陷:首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝,在此过程中如果使用了高端内存,这种拷贝需要临时建立/取消页面映射,造成性能损失。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间或先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder采用一种全新策略:由Binder驱动负责管理数据接收缓存,Binder驱动实现了mmap(该函数可参考笔记中Prace注入)系统调用。

因为mmap()通常用在有物理存储介质的文件系统上,Binder驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间:Binder的接收方有了一片大小为MAP_SIZE(通过mmap参数指定)的接收缓存区,不过这段空间是由驱动管理,用户不必也不能直接访问(mmap的参数指定为PROT_READ),mmap()的返回值是内存映射在用户空间的地址。

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

接收缓存区映射好后就可以做为缓存池接收和存放数据。接收数据包的结构为binder_transaction_data,但这只是消息头,真正的有效负荷位于data.buffer所指向的内存中。这片内存不需要接收方提供,恰恰是来自mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

要注意的是,存放binder_transaction_data结构本身以及表4中所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。

有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放data.buffer所指向的内存区,这是由命令BC_FREE_BUFFER完成的。

通过上面介绍可以看到,驱动为接收方分担了最为繁琐的任务:分配/释放大小不等且难以预测的有效负荷缓存区,而接收方只需要提供缓存来存放大小固定消息头即可。在效率上,由于mmap()分配的内存是映射在接收方用户空间里的,所有总体效果就相当于对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,提升了一倍的性能。

Binder接收线程管理

Binder通信实际上是位于不同进程中的线程之间的通信。假如进程S是Server端,进程C是Client端。线程TC从C进程中通过Binder的引用向进程S发送请求,此时线程TC处于接收返回数据的等待状态,S为了处理这个请求需要启动线程TS,TS处理完请求就会将处理结果返回给TC。在这过程中,TS仿佛是TC在进程S中的代理(代表TC执行远程任务),而给TC的感觉就像穿越到S中执行一段代码又回到了C。为了使这种穿越更加真实,驱动会将TC的一些属性赋给TS(特别是TC的优先级nice),这样TS会使用和TC类似的时间完成任务。很多资料会用‘线程迁移’来形容这种现象,容易让人产生误解。一来线程根本不可能在进程之间跳来跳去,二来TS除了和TC优先级一样,其它没有相同之处,包括身份,打开文件,栈大小,信号处理,私有数据等。对于进程S,可能会有许多Client同时发起请求,为了提高效率往往开辟线程池并发处理收到的请求。怎样使用线程池实现并发处理呢?

这和具体的IPC机制有关。拿socket举例,Server端的socket设置为侦听模式,有一个专门的线程使用该socket侦听来自Client的连接请求(即阻塞在accept()上)。这个socket就象一只会生蛋的鸡,一旦收到来自Client的请求就会生一个蛋(创建新socket并从accept()返回)。侦听线程从线程池中启动一个工作线程并将刚下的蛋交给该线程。后续业务处理就由该线程完成并通过这个单与Client实现交互。

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

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

数据包接收队列与等待队列管理

通常数据传输的接收端(Binder驱动和泛Client)有两个队列:数据包接收队列和(线程)等待队列,用以缓解供需矛盾。

在驱动中,每个进程有一个全局的接收队列(也叫to-do队列),存放不是发往特定线程的数据包;相应地有一个全局等待队列,所有等待从全局接收队列里收数据的线程在该队列里排队。每个线程有自己私有的to-do队列,存放发送给该线程的数据包;相应的每个线程都有各自私有等待队列,专门用于本线程等待接收自己to-do队列里的数据。虽然名叫队列但其实线程私有等待队列中最多只有一个线程(即它自己)。

由于发送时没有特别标记,驱动怎么判断哪些数据包该送入全局to-do队列,哪些数据包该送入特定线程的to-do队列呢?这里有两条规则。规则1:Client发给Server的请求数据包都提交到Server进程的全局to-do队列。规则2:对同步请求的返回数据包(由BC_REPLY发送的包)都发送到发起请求的线程的私有to-do队列中。数据包进入接收队列的潜规则也就决定了线程进入等待队列的潜规则,即一个线程只要不接收返回数据包则应该在全局等待队列中等待新任务,否则就应该在其私有等待队列中等待Server的返回数据。

这些潜规则是驱动对Binder通信双方施加的限制条件,体现在应用程序上就是同步请求交互过程中的线程一致性:1) Client端,等待返回包的线程必须是发送请求的线程,而不能由一个线程发送请求包,另一个线程等待接收包,否则将收不到返回包;2) Server端,发送对应返回数据包的线程必须是收到请求数据包的线程,否则返回的数据包将无法送交发送请求的线程。

接下来探讨一下Binder驱动是如何递交同步交互和异步交互的。同步交互和异步交互的区别是同步交互的请求端(client)在发出请求数据包后须要等待应答端(Server)的返回数据包,而异步交互的发送端发出请求数据包后交互即结束。对于这两种交互的请求数据包,驱动可以不管三七二十一,统统丢到接收端的to-do队列中一个个处理。但驱动并没有这样做,而是对异步交互做了限流,令其为同步交互让路,具体做法是:对于某个Binder实体,只要有一个异步交互没有处理完毕,例如正在被某个线程处理或还在任意一条to-do队列中排队,那么接下来发给该实体的异步交互包将不再投递到to-do队列中,而是阻塞在驱动为该实体开辟的异步交互接收队列(Binder节点的async_todo域)中,但这期间同步交互依旧不受限制直接进入to-do队列获得处理。一直到该异步交互处理完毕下一个异步交互方可以脱离异步交互队列进入to-do队列中。之所以要这么做是因为同步交互的请求端需要等待返回包,必须迅速处理完毕以免影响请求端的响应速度,而异步交互属于‘发射后不管’,稍微延时一点不会阻塞其它线程。所以用专门队列将过多的异步交互暂存起来,以免突发大量异步交互挤占Server端的处理能力或耗尽线程池里的线程,进而阻塞同步交互。


最后放一张还没看完的图:

原创粉丝点击