正如上一章所说, 跨进程通信是需要内核空间做支持的. 传统的 IPC 机制如 管道, Socket, 都是内核的一部分, 因此通过内核支持来实现进程间通信自然是没问题的.
但是 Binder 并不是 Linux 系统内核的一部分, 那怎么办呢, 这得益于 Linux 的动态内核可加载模块 (Loadable Kernel Module, LKM)的机制
这样 Android 系统就可以通过动态添加一个内核模块运行在内核空间, 用户进程进程之间通过这个内核模块作为桥梁来实现通信.
那么在 Android 系统中用户进程之间是如何通过这个内核模块 (Binder Driver)来实现通信的呢? 显然不是和上一章的传统 IPC 通信一样,进行两次 copy 了, 不然Binder 也不有在性能方面的优势了.
Binder IPC 机制中设计到的内存映射通过 mmap() 来实现, mmap() 是 *** 作系统中一种内存映射的方法.
内存映射能减少数据 copy 的次数, 实现用户空间和内核空间的高效互动. 两个空间各自的修改也能直接反应在映射的内存区域, 从而被对方空间及时感知. 也正因为如此, 内存映射能够提供对进程间通信的支持.
Binder IPC 正是基于内存映射( mmap() ) 来实现的, 但是 mmap() 通常是用在有物理介质的文件系统上的.
比如进程中的用户区域是不能直接和物理设备打交道的, 如果想要把磁盘上的数据读取到进程的用户区域, 需要两次 copy (磁盘 ->内核空间 ->用户空间). 通常在这种场景下 mmap() 就能发挥作用, 通过在物理介质和用户空间之间建立映射, 减少数据的 copy 次数, 用内存读写代替 I/O 读写, 提高文件读取效率.
而 Binder 并不存在物理介质, 因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间映射, 而是用来在内核空间创建数据接收的缓存空间.
一次完整的 Binder IPC 通信过程通常是这样:
这样就完成了一次进程间通信
如下图:
介绍完 Binder IPC 的底层通信原理, 接下来我们看看实现层面是如何设计的
一次完成的进程间通信必然至少包含两个进程, 通常我们称通信的双方分别为客户端进程(Client) 和服务端进程(Server), 由于进程隔离机制的存在, 通信双方必然需要借助 Binder 来实现.
BInder 是基于 C/S 架构. 是由一些列组件组成. 包括 Client, Server, ServiceManager, Binder 驱动.
Binder 驱动就如如同路由器一样, 是整个通信的核心. 驱动负责进程之间 Binder 通信的建立 / 传递, Binder 引用计数管理, 数据包在进程之间的传递和交互等一系列底层支持.
ServiceManager 作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用, 使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用.
注册了名字的 Binder 叫实名 Binder, 就像网站一样除了 IP 地址以外还有自己的网址.
Server 创建了 Binder, 并为它起一个字符形式, 可读易记的名字, 将这个 BInder 实体连同名字一起以数据包的形式通过 Binder 驱动 发送给 ServiceManager, 通知 ServiceManager 注册一个名字为 "张三"的 Binder, 它位于某个 Server 中, 驱动为这个穿越进程边界的 BInder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用, 将名字以及新建的引用打包传给 ServiceManager, ServiceManager 收到数据后从中取出名字和引用填入查找表.
ServiceManager 是一个进程, Server 又是一个另外的进程, Server 向 ServiceManager 中注册 BInder 必然涉及到进程间通信. 当实现进程间通信又要用到进程间通信, 这就好像蛋可以孵出鸡的前提确实要先找只鸡下蛋! Binder 的实现比较巧妙, 就是预先创造一只鸡来下蛋. ServiceManager 和其他进程同样采用 Binder 通信, ServiceManager 是 Server 端, 有自己的 Binder 实体, 其他进程都是 Client, 需要通过这个 Binder 的引用来实现 Binder 的注册, 查询和获取. ServiceManager 提供的 Binder 比较特殊, 它没有名字也不需要注册. 当一个进程使用 BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡). 其实这个 Binder 实体的引用在所有 Client 中都固定为 0 , 而无需通过其他手段获得. 也就是说, 一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信. 这里说的 Client 是相对于 ServiceManager 而言的, 一个进程或者应用程序可能是提供服务的 Server, 但是对于 ServiceManager 来说它仍然是个 Client.
Server 向 ServiceManager 中注册了 Binder 以后, Client 就能通过名字获得 Binder 的引用. Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder. 比如,Client 申请访问名字叫"张三"的 Binder 引用. ServiceManager 收到这个请求后从请求数据包中取出 Binder 名称, 在查找表里找到对应的条目, 取出对应的 Binder 引用, 作为回复发送给发起请求的 Client. 从面相对象的角度看, Server 中的 Binder 实体现在有两个引用: 一个位于 ServiceManager 中, 一个位于发起请求的 Client 中. 如果后面会有更多的 Client 请求该 Binder, 系统中就会有更多的引用指向这个 Binder, 就像 Java 中一个对象有多个引用一样.
我们已经解释清楚 Client, Server 借助 Binder 驱动完成跨进程通信的实现机制了, 但是还有个问题需要弄清楚, 比如 A 进程想要 B 进程中的某个对象(object) 是如何实现的呢, 毕竟它们属于不同的进程, A 进程没办法直接使用 B 进程中的 object.
前面我们说过跨进程通信的过程都有 Binder 驱动的参与, 因此在数据流经 Binder 驱动的时候 Binder 驱动会对数据做一层转换.
我们在 Client端,向 ServiceManager 获取具体的 Server 端的 Binder 引用的时候,会首先进过 Binder 驱动,Binder 驱动它并不会把真正的 Server 的 Binder 引用返回给 Client 端,而是返回一个代理的 java 对象,该对象具有跟 Server 端的 Binder 引用相同的方法签名,这个对象为 ProxyObject,他具有跟 Server 的 Binder 实例一样的方法,只是这些方法并没有 Server 端的能力,这些方法只需要把请求参数交给 Binder 驱动即可. 对于 Client 端来说和直接调用 Server 中的方法是一样的.
了解了上面之后, 我们大致可以推算出 Binder 的通信过程
1. 注册 ServiceManager
2. 注册 Server
3. Client 获取 Server 的 Binder 引用
4. Client 与 Server 通信
Linux传统IPC机制主要有已下几种:管道、消息队列、共享内存Socket等。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。
同时从安全性角度考虑,Android作为一个开放式,拥有众多开发者的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称、system V的键值、socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。
由于传统的IPC机制有这样那样不让人满意的原因。Android自己开发了一套进程间通信机制Binder通信。Binder机制起源于一个简单的想法:将申请服务的请求和对应的响应信息,写入一个所有进程均能够访问的地址空间中。当进程需要使用这些数据时,只需要访问对应的内存地址,以减小内容复制引入的开销。为此,Binder机制利用kernel空间作为共享区域,并由Binder driver来建立起每个进程的内存地址与kernel空间中存储地址的映射。从而使数据只要拷贝一次就可以。同时为发送方添加UID/PID身份,既支持实名Binder也支持匿名Binder,提高安全性。
Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及Binder驱动,其中ServiceManager用于管理系统中的各种服务。如上图所示Binder在Framework层和Native层分别有对应的客户端(Client)、服务(Server)的和服务管理器(Service Manager)。同时在Kernel层(内核空间)有Binder的驱动设备。
这四个角色的作用分别是:
① Client进程:使用服务的进程。
② Server进程:提供服务的进程。
③ ServiceManager进程:ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。
④ Binder驱动:驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。
首先需要注册服务端,只有注册了服务端,客户端才有通讯的目标,服务端通过 ServiceManager 注册服务,注册的过程就是向 Binder 驱动的全局链表 binder_procs 中插入服务端的信息(binder_proc 结构体,每个 binder_proc 结构体中都有 todo 任务队列),然后向 ServiceManager 的 svcinfo 列表中缓存一下注册的服务。
注册完服务后客户端就能够查询并获取注册了的服务了。客户端向ServiceManager 发起获取服务的请求,传递要获取服务的名称。ServiceManager 从服务列表中查找到Client需要的对应服务信息,将该服务的代理Binder(BinderProxy)返回给客户端。其实查询服务相当于ServiceManager 作为服务端,客户端进行使用。所以这也是一次Binder使用服务的过程。
使用服务的过程如上图。
① Binder驱动为跨进程通信做准备:通过调用mmap()系统函数实现内存映射。在Binder驱动中创建一块接收缓存区。同时将内核缓存区地址和Server端中用户空间一块地址同时都映射到该接收缓存区中。这时候就创建了虚拟区间和映射的关系。
② Client进程将数据发送到Server进程。Client进程通过调用copy_from_user()发送数据拷贝到内核中(Binder驱动)的缓存区中,此时Client发起请求的线程会被挂起。由于在①中构建了映射关系,此时相当于也将数据发送到了Server端的用户空间中。之后Binder驱动通知Server端进程执行解包。
③ Server进程根据Client进程发送来的数据,调用目标方法。收到Binder驱动通知后,Server进程对数据进行解包,并调用相关方法处理。
④ Server进程将目标方法处理结果返回给Client进程。将处理结果放回自己的共享空间(即①中映射的Binder驱动缓存区中)。Binder驱动通知Client进程获取返回结果,此时②中被挂起的线程会被重新唤醒。Client进程通过系统调用copy_to_user(),从内核缓存区拷贝Server进程返回的结果。
从上面使用服务的过程可以看到,整个过程只拷贝了一次发送的数据和一次接收的数据。而正如开头所述,消息队列和管道这两种IPC拷贝次数为2次。
Binder模型的线程管理采用Binder驱动线程池,由Binder驱动进行管理,而不是由Server进程来管理。而一个进程的Binder线程默认线程数默认最大是16。也就是说Client的请求数一旦超过16个会被阻塞直到有空间的Binder线程。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)