Binder
为什么是Binder
具体见QA。
一次完整的 Binder IPC 通信过程通常是这样:
其内存流向是这样的:
- 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
- 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
- 发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
如下图:
- Server创建Binder实例,并调用
ServiceManager.addService(String name, IBinder service)
向ServiceManager注册 - ServiceManager根据传入的服务名与服务实体,在svclist中增加该服务对应的handle和name映射
- Client发起通信,先向ServiceManager查询该服务名,命中后返回
Binder 结构图
要运作Binder,需要4个角色通力合作:
客户端:获取服务端在Binder驱动中对应的引用,然后调用它的transact方法即可向服务端发送消息。
服务端:指Binder实现类所在的进程,该对象一旦创建,内部则会启动一个隐藏线程,会接收客户端发送的数据,然后执行Binder对象中的onTransact()函数。
Binder驱动:当服务端Binder对象被创建时,会在Binder驱动中创建一个mRemote对象。
Service Manager:作用相当于DNS,就想平时我们通过网址,然后DNS帮助我们找到对应的IP地址一样,我们在Binder服务端创建的Binder,会注册到Service Manager,同理,当客户端需要该Binder的时候,也会去Service Manager查找。
所以以上4者的运作基本上是:
- 服务端创建对应Binder实例对象,然后开启隐藏Binder线程,接收来自客户端的请求,同时,将自身的Binder注册到Service Manager,在Binder驱动创建mRemote对象。
- 客户端想和服务端通信,通过Service Manager查找到服务端的Binder,然后Binder驱动将对应的mRemote对象返回
- 至此,整个通信连接建立完毕
在建立完毕通信之后,客户端可以通过获取到的mRemote对象发生消息给远程服务端了,客户端通过调用transact()方法,将要请求的内容发送到服务段,然后挂起自己当前的线程,等待回复,服务端收到数据后在自己的onTransact()方法进行处理,然后将对应的结果返回给客户端,客户端收到数据,重新拉起线程,至此进程间交互数据完毕。
Binder 通信模型
介绍完 Binder IPC 的底层通信原理,接下来我们看看实现层面是如何设计的。
一次完整的进程间通信必然至少包含两个进程,通常我们称通信的双方分别为客户端进程(Client)和服务端进程(Server),由于进程隔离机制的存在,通信双方必然需要借助 Binder 来实现。
5.1 Client/Server/ServiceManager/驱动
前面我们介绍过,Binder 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。
Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之前的关系。
通常我们访问一个网页的步骤是这样的:首先在浏览器输入一个地址,如 http://www.google.com 然后按下回车键。但是并没有办法通过域名地址直接找到我们要访问的服务器,因此需要首先访问 DNS 域名服务器,域名服务器中保存了 http://www.google.com 对应的 ip 地址 10.249.23.13,然后通过这个 ip 地址才能放到到 http://www.google.com 对应的服务器。
Android Binder 设计与实现一文中对 Client、Server、ServiceManager、Binder 驱动有很详细的描述,以下是部分摘录:
Binder 驱动
Binder 驱动就如同路由器一样,是整个通信的核心;驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。ServiceManager 与实名 Binder
ServiceManager 和 DNS 类似,作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用,使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用。注册了名字的 Binder 叫实名 Binder,就像网站一样除了除了有 IP 地址意外还有自己的网址。Server 创建了 Binder,并为它起一个字符形式,可读易记得名字,将这个 Binder 实体连同名字一起以数据包的形式通过 Binder 驱动发送给 ServiceManager ,通知 ServiceManager 注册一个名为“张三”的 Binder,它位于某个 Server 中。驱动为这个穿越进程边界的 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager。ServiceManger 收到数据后从中取出名字和引用填入查找表。细心的读者可能会发现,ServierManager 是一个进程,Server 是另一个进程,Server 向 ServiceManager 中注册 Binder 必然涉及到进程间通信。当前实现进程间通信又要用到进程间通信,这就好像蛋可以孵出鸡的前提却是要先找只鸡下蛋!Binder 的实现比较巧妙,就是预先创造一只鸡来下蛋。ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡)。其次这个 Binder 实体的引用在所有 Client 中都固定为 0 而无需通过其它手段获得。也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信。类比互联网,0 号引用就好比是域名服务器的地址,你必须预先动态或者手工配置好。要注意的是,这里说的 Client 是相对于 ServiceManager 而言的,一个进程或者应用程序可能是提供服务的 Server,但对于 ServiceManager 来说它仍然是个 Client。
Client 获得实名 Binder 的引用
Server 向 ServiceManager 中注册了 Binder 以后, Client 就能通过名字获得 Binder 的引用了。Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder: 我申请访问名字叫张三的 Binder 引用。ServiceManager 收到这个请求后从请求数据包中取出 Binder 名称,在查找表里找到对应的条目,取出对应的 Binder 引用作为回复发送给发起请求的 Client。从面向对象的角度看,Server 中的 Binder 实体现在有两个引用:一个位于 ServiceManager 中,一个位于发起请求的 Client 中。如果接下来有更多的 Client 请求该 Binder,系统中就会有更多的引用指向该 Binder ,就像 Java 中一个对象有多个引用一样。
5.2 Binder 通信过程
至此,我们大致能总结出 Binder 通信过程:
- 首先,一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
- Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点mRemote对象以及 ServiceManager 对mRemote的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
- Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
我们看到整个通信过程都需要 Binder 驱动的接入。下图能更加直观的展现整个通信过程(为了进一步抽象通信过程以及呈现上的方便,下图我们忽略了 Binder 实体及其引用的概念):
5.3 Binder 通信中的代理模式
我们已经解释清楚 Client、Server 借助 Binder 驱动完成跨进程通信的实现机制了,但是还有个问题会让我们困惑。A 进程想要 B 进程中某个对象(object)是如何实现的呢?毕竟它们分属不同的进程,A 进程 没法直接使用 B 进程中的 object。
前面我们介绍过跨进程通信的过程都有 Binder 驱动的参与,因此在数据流经 Binder 驱动的时候驱动会对数据做一层转换。当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把把请求参数交给驱动即可。对于 A 进程来说和直接调用 object 中的方法是一样的。
当 Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了。
5.4 Binder 的完整定义
现在我们可以对 Binder 做个更加全面的定义了:
- 从进程间通信的角度看,Binder 是一种进程间通信的机制;
- 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
- 从 Client 进程的角度看,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理
- 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。
Binder的死亡通知
三种方式来检测远程对象是否存活:
调用远程方法的时候捕获RemoteException(DeadObjectException);
调用IBinder的pingBinder()进行检测;
调用linkToDeath注册IBinder.DeathRecipient接口回调,接口会在Ibinder死亡时回调binderDied();
ServiceManager
链接:https://juejin.cn/post/6855904885976924173
一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager,同时想Binder驱动注册ServiceManager的Binder实体,此时其他相对于SM的Client可以通过0号引用获取SM的Binder引用。
ServiceManager的Binder引用:0号引用
ServiceManager
是安卓中一个重要的类,正如它的名字所表达的意思,用于管理所有的系统服务,维护着系统服务和客户端的binder
通信。
那ServiceManager
和Binder
是什么样的关系呢?
简单点说,ServiceManager
作用是将字符形式的 Binder
名字转化成 Client
中对该 Binder
的引用,使得 Client
能够通过 Binder
的名字获得对 Binder
实体的引用。(这个注册了名字的 Binder
叫实名 Binder
)
但现在就有个问题,Service Manager
是一个进程,Server
是一个进程,Server
向 Service Manager
中注册 Binder
就涉及到进程间通信。但当前进程间通信又要用到进程间通信,这就好像蛋可以孵出鸡的前提却是要先找只鸡下蛋。那如何解决这个没有鸡生蛋的问题呢?
巧妙的实现:
要打破这个问题,就要预先创造一只鸡去生蛋,这只鸡就是ServiceManager
的Binder
实体。ServiceManager
提供的 Binder
比较特殊,它没有名字也不需要注册。当一个进程使用 BINDER_SET_CONTEXT_MGR
命令就会将自己注册成 ServiceManager
时, Binder
驱动会自动为它创建 Binder
实体(这就是那只预先造好的那只鸡)
Server端的注册方式
这个 Binder
实体的引用在所有 Client
中都固定为 0 而无需通过其它手段获得。也就是说,一个 Server
想要向 ServiceManager
注册自己的 Binder
就必须通过这个 0 号引用和 ServiceManager
的 Binder
通信。
整个过程大致是:
当一个Binder Service
创建后,它们就将自己的[名称、Binder句柄]对应关系告知SM
进行备案,完成注册。
Client端获取Service的Binder引用
Server
向 ServiceManager
中注册了 Binder
后, Client
就能通过名字获得 Binder
的引用了。由于 Client
和 SM
通信也需要Binder,所以Client
也是通过这个0号引用去向SM
获取某个Service
的Binder
。
因为
ServiceManager
对于Client
端来说Handle句柄
是固定的,都是0,所以ServiceManager
服务并不需要查询,可以直接使用。
整个过程大致是:
Client
发送数据包向SM
请求某个名字的Binder
引用SM
收到这个请求后,从请求数据包中去找名称,在查找表里找到对应的Binder
的引用- 将找到的
Binder
引用作为回复发送给请求的Client
QA
为什么内核空间里有两个缓存区,“内核缓存区”和“数据接收缓存区”
举个调用WindoManagerService的Binder例子?
为什么是 Binder,具体说说
Client-Server方式的广泛采用对进程间通信(IPC)机制是一个挑战。目前linux支持的IPC包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及socket中只有socket支持Client-Server的通信方式。当然也可以在这些底层机制上架设一套协议来实现Client-Server通信,但这样增加了系统的复杂性,在手机这种条件复杂,资源稀缺的环境下可靠性也难以保证。
另一方面是传输性能。socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。
表 1 各种IPC方式数据拷贝次数
还有一点是出于安全性考虑。Android作为一个开放式,拥有众多开发者的的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,system V的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。
附录-Binder示例
Server
1 | public class GameService extends Service { |
Client
1 | private IBinder mRemote = null; |