Binder跨进程通信原理(三):Binder IPC实现原理

1. 动态内核可加载模块 && 内存映射

正如上一章所说, 跨进程通信是需要内核空间做支持的. 传统的 IPC 机制如 管道, Socket, 都是内核的一部分, 因此通过内核支持来实现进程间通信自然是没问题的.

但是 Binder 并不是 Linux 系统内核的一部分, 那怎么办呢, 这得益于 Linux 的动态内核可加载模块 (Loadable Kernel Module, LKM)的机制

1.1 动态内核可加载模块 (Loadable Kernel Module, LKM)

动态内核可加载模块 (Loadable Kernel Module, LKM)

模块是具有独立功能的程序, 它可以被单独编译, 但是不能独立运行. 它在运行时被链接到内核作为内核的一部分运行.

这样 Android 系统就可以通过动态添加一个内核模块运行在内核空间, 用户进程进程之间通过这个内核模块作为桥梁来实现通信.

在 Android 中, 这个运行在内核空间, 负责各个用户进程通过 Binder 实现通信的内核模块就叫做 Binder 驱动 (Binder Driver)

那么在 Android 系统中用户进程之间是如何通过这个内核模块 (Binder Driver)来实现通信的呢? 显然不是和上一章的传统 IPC 通信一样,进行两次 copy 了, 不然Binder 也不有在性能方面的优势了.

1.2 内存映射

Binder IPC 机制中设计到的内存映射通过 mmap() 来实现, mmap() 是操作系统中一种内存映射的方法.

内存映射

简单的说就是将用户空间的一块内存区域映射到内核空间.
映射关系建立后, 用户对这块内存区域的修改可以直接反应到内核空间.
反之,内核空间对这段区域的修改也能直接反应到用户空间.

内存映射能减少数据 copy 的次数, 实现用户空间和内核空间的高效互动. 两个空间各自的修改也能直接反应在映射的内存区域, 从而被对方空间及时感知. 也正因为如此, 内存映射能够提供对进程间通信的支持.


2. Binder IPC 实现原理

Binder IPC 正是基于内存映射(mmap()) 来实现的, 但是mmap() 通常是用在有物理介质的文件系统上的.

比如进程中的用户区域是不能直接和物理设备打交道的, 如果想要把磁盘上的数据读取到进程的用户区域, 需要两次 copy (磁盘 -> 内核空间 -> 用户空间). 通常在这种场景下 mmap() 就能发挥作用, 通过在物理介质和用户空间之间建立映射, 减少数据的 copy 次数, 用内存读写代替 I/O 读写, 提高文件读取效率.

而 Binder 并不存在物理介质, 因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间映射, 而是用来在内核空间创建数据接收的缓存空间.

一次完整的 Binder IPC 通信过程通常是这样:

  1. 首先 Binder 驱动在内核空间创建一个数据接收缓存区
  2. 接着在内核空间开辟一块内核缓存区.
  3. 建立内核缓存区 和 内核中数据接收缓存区 之间的映射关系.
  4. 建立内核中数据接收缓存区 与 数据接收进程用户空间 的地址的映射关系
  5. 发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区, 由于内核缓存区 与 内核数据接收缓存区 存在映射关系, 而内核数据接收缓存区 又与 数据接收进程用户空间 存在映射关系, 所以相当于 内核缓存区 与 数据接收进程用户空间存在映射关系. 因此也就相当于把数据发送到了数据接收进程的用户空间.

这样就完成了一次进程间通信
如下图:

image

3. Binder 通信模型

介绍完 Binder IPC 的底层通信原理, 接下来我们看看实现层面是如何设计的

一次完成的进程间通信必然至少包含两个进程, 通常我们称通信的双方分别为客户端进程(Client) 和服务端进程(Server), 由于进程隔离机制的存在, 通信双方必然需要借助 Binder 来实现.

3.1 Client/Server/ServiceManager/Binder驱动

BInder 是基于 C/S 架构. 是由一些列组件组成. 包括 Client, Server, ServiceManager, Binder 驱动.

  • Client, Server, ServiceManager 运行在用户空间
  • Binder 驱动运行在内核空间.
  • ServiceManager, Binder 驱动由系统提供.
  • Client, Service 由应用程序来实现

Client, Server, ServiceManager 都是通过系统调用 open, mmap,ioctl 来访问设备文件 /dev/binder, 从而实现与 Binder 驱动的交互来间接的实现跨进程通信.

image

3.1.1 Binder 驱动

Binder 驱动就如如同路由器一样, 是整个通信的核心. 驱动负责进程之间 Binder 通信的建立 / 传递, Binder 引用计数管理, 数据包在进程之间的传递和交互等一系列底层支持.

3.1.2 ServiceManager 与 实名 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 中一个对象有多个引用一样.


4. Binder 通信中的代理模式

我们已经解释清楚 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 中的方法是一样的.


5. Binder 通信过程

了解了上面之后, 我们大致可以推算出 Binder 的通信过程

1. 注册 ServiceManager

一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager.

2. 注册 Server

Server 通过 Binder 驱动向 ServiceManager 中注册 Binder (Server 中 Binder 实体), 表明可以对外提供服务.
Binder 驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用, 然后将名字及新建的引用打包传给 ServiceManager, 最后 ServiceManager 将其填入到查找表.

3. Client 获取 Server 的 Binder 引用

Client 请求获得 Server 的 Binder 引用, Binder 驱动在接收到 Client 的请求的时候, 就去 ServiceManager 中查询, 查询到后会创建一个 Server 端 Binder 对象的 ProxyObject (Binder 的代理对象), 并将该 ProxyObject 返回给 Client.

4. Client 与 Server 通信

Client 收到 Binder 驱动返回的 ProxyObject 后, 通过 ProxyObject 调用其中的方法. ProxyObject 的方法再去调用 Binder 驱动, Binder 驱动会去查询自己维护的表单, 发现这是某个 Server 端 Binder 的代理对象, 就会通知 Server 端调用具体的方法,(会把 Client 端调用 ProxyObject 中方法的参数传过去. ), 并要求 Server 把结果返回给自己. 当 Binder 驱动拿到结果后, 就会转发给 Client 端.

image

6. Binder 的完整定义

  • 从进程间通信的角度看, Binder 是一种进程间通信的机制
  • 从 Server 进程的角度看, Binder 指的是 Server 中的 Binder 实体对象.
  • 从 Client 进程的角度看, Binder 指的是对 Binder 代理对象, 是 Binder 实体对象的一个远程代理.
  • 从传输过程的角度看, Binder 是一个可以跨进程通信的对象. Binder 驱动会对这个跨越进程边界的对象

你可能感兴趣的:(Binder跨进程通信原理(三):Binder IPC实现原理)