了解 Binder 看这一篇就够了

Binder机制解析
本文深入剖析了Android中的Binder机制,从进程隔离与IPC入手,详细介绍了Binder的原理、组成及其高效通信的秘密。通过类比网络请求过程,清晰展示了Binder在Android系统中的作用与运作流程。

Binder

一. 前言

笔者一直坚信,在不介绍上下文的情况下直入主题就是在耍流氓,只有建立好完善的知识体系后才能更好的理解和记忆相关内容。因此在介绍Binder之前,先来梳理一下大致的脉络。

二. 进程和IPC

2.1 进程

在 Android 中,可以简单的把进程看作是 JVM 虚拟机,每个进程就是一个独立的虚拟机,它拥有自己特定的资源,例如内存空间。多个进程之间的内存是不共享的,就像平行世界,互相无法直接访问彼此的数据,这就是常说的进程隔离。由于这个机制,进程间的数据交流就需要采用特殊的通信机制:进程间通信(IPC)。

2.2 IPC

我们都知道 Binder 是一种 IPC方式,但Linux本身也有其他的跨进程交互方式的, 那 Android 干嘛要自己重新定义了一个 Binder 机制,并作为其服务及组件的主要交互方式呢?原因有两个:提高效率 & 符合C/S架构。

三. 传统 IPC

理解了上面的几个概念,我们再来看看传统的 IPC 方式中,进程之间是如何实现通信的。

传统 IPC 方式使用到了一个非常重要的概念,那就是**进程的内存空间组成。**一个进程的内存可以分为两类:**用户空间 & 内核空间,其中内核空间是所有进程共用的。**以下为多进程中不同内存空间之间的关系:

  • 用户空间 & 用户空间

    数据不可相互独立,不可直接交互

  • 内核空间 & 内核空间

    数据可共享

  • 用户空间 & 内核空间

    用户控件和内核空间之间可以通过系统调用进行交互,主要执行函数如下:

    // 将用户空间的数据拷贝到内核空间
    copy_from_user();
    // 将内核空间的数据拷贝到用户空间
    copy_to_user();
    

由此可知传统 IPC 实现的大致思路如下:
在这里插入图片描述
可以看出通过这种方式执行一次 IPC 操作需要进行2次读写操作。

四. Binder 原理

理解了上面的传统 IPC 实现原理,通过读写内核空间的方式实现跨进程通信至少需要2次读写操作。那么Binder又是如何做到更高的通信效率呢,这里就需要先了解几个概念:虚拟内存 和 内存映射(mmap)。

4.1 虚拟内存

我们知道程序代码和数据需要被解释在内存中才能得以运行,随着互联网的飞速发展,计算机中安装的软件越来越多,软件所需要的内存也越来越大,如果将软件全部加载到内存中很可能导致内存不足,虚拟内存的概念也就被提出了。

虚拟内存是将系统硬盘空间(ROM)与系统内存空间(RAM)联合在一起供程序使用的技术,**在程序运行时,只把虚拟内存的一小部分存储到内存,其余都存储在硬盘上(也就是说程序虚拟空间就等于实际物理内存加部分硬盘空间)。当被访问的虚拟地址不在内存时,将需要的虚拟地址随即被调入到内存;同时当系统内存紧张时,也可以把当前不用的虚拟内存换出到硬盘,来腾出物理内存空间。**系统如此周而复始地运转——换入、换出,而用户几乎无法查觉,这都是拜虚拟内存机制所赐。

4.2 内存映射(Memory Mapping)

说到内存映射可能第一反应会比较陌生,但是如果说mmap估计好多人就会一拍脑袋,原来是这个。近几年各大厂商在技术演技及性能优化中频繁应用到了mmap的特性,常见的如:MMKV。其优势可以简单总结为两个:快 & 安全。

映射,顾名思义是将两个事物建立一种一一对应的关系,当改动其中任意一个时会对两者造成同样的影响。举个栗子就像人和影子,无论人做出什么样的动作影子都会跟着做出同样的动作。

内存映射就是将进程中部分虚拟内存区域映射到物理内存上,操作程序的虚拟内存就是操作这块物理内存,这样就不用经过内核转换,在整个内存映射过程中,并没有实际的数据拷贝,只是逻辑上放入了内存,其余的操作将又系统调用mmap()来实现,所以映射的整体效率非常高。

  • 文件为例:把硬盘文件内容映射到程序的虚拟内存中,通过对虚拟内存的读取修改就是对文件的读取修改,这样进程就可以像访问普通内存一样访问文件,而不用调用read()、write()等操作了

  • 进程为例:分别把进程A和进程B的虚拟内存区域的一块与共享内存C建立映射关系,假如进程A对自己的这块虚拟内存区域进行修改,那么也会映射到进程B的虚拟内存块;因为这两个进程都映射到了同一个共享内存C,所以A的修改对于B是可见的

在这里插入图片描述

4.3 Binder 组成

在理解了 Binder 底层通信原理后,再来看看 Binder 到底是什么,首先得弄清楚一个概念,这里说的 Binder 并不仅仅是平常所说的某一个 Binder 类,而是基于 Binder 进行 IPC 的整套框架结构。

一次完整的进程间通信必然至少包含两个进程,在 Binder 中,通常我们称通信的双方分别为客户端进程(Client)和服务端进程(Server),由此也可以看出 BInder 的设计理念其实是基于C/S架构。

以下为 Binder 的主要组成组件:

  • Binder 驱动

    一种虚拟的设备驱动,和硬件没有关系,只是实现方式和设备驱动是一样的,在设备目录 /dev 下注册一个 Binder 节点,提供标准文件操作,它是连接 Client 、Server、ServerManager的桥梁。

  • Service Manager

    管理进程注册与查询,保存进程信息。每个对外公开服务的进程都需要到这里注册,注册成功会被分配一个唯一id。

  • Client

    调用 Server 的进程。

  • Server

    提供 Server 的进程。

为了便于理解这里可以将每个组件根据功能性类比为我们常见的一次网络请求:

Server —— 后台服务器

Client —— 请求客户端

ServiceManager —— DNS域名服务器

Binder 驱动 —— 路由器

通常我们访问一个网页的步骤是这样的:

  1. 首先后台服务器上搭建好我们的 API 接口,定义 ip 地址 10.xx.xx.xx,将 ip 地址绑定一个对外的稳定域名,如:https://blog.csdn.net/ccw0054,并且在DNS域名服务器中进行注册。
  2. 在客户端上打开浏览器,输入我们的接口域名,按下回车开始请求。
  3. 请求被发送到路由器,但是并没有办法通过域名地址直接找到我们要访问的服务器。
  4. 因此路由器需要首先访问 DNS 域名服务器,域名服务器中保存了对应的 ip 地址,然后通过这个 ip 地址最终访问到对应的后台服务器上搭建好的接口。

除此之外,由于 ServiceManager 本质上也是一个进程,Server 是另一个进程,而 Server需要向 ServiceManager 中注册 Binder 的操作自然也属于 IPC。ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDER_SET_CONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体。

将 Binder 代入翻译过来是这样的:

  1. 首先,一个进程使用 BINDER_SET_CONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager。
  2. Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
  3. Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。

4.4 Binder 中的代理模式

Client、Server 借助 Binder 驱动完成跨进程通信了。但是有个问题会让我们困惑,A 进程是如何将自己的数据(如某个对象)传递给 B 进程的呢。

跨进程通信的过程都有 Binder 驱动的参与,因此在数据流经 Binder 驱动的时候驱动会对数据做一层转换。当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把请求参数交给驱动,由驱动再去调用真正的 object。

4.5 Binder Java 类

  • IBinder: IBinder 是一个接口,代表了一种跨进程通信的能力。只要实现了这个借口,这个对象就能跨进程传输。
  • IInterface: IInterface 代表的就是 Server 进程对象具备什么样的能力(能提供哪些方法,其实对应的就是 AIDL 文件中定义的接口)
  • Binder: Java 层的 Binder 类,代表的其实就是 Binder 本地对象。BinderProxy 类是 Binder 类的一个内部类,它代表远程进程的 Binder 对象的本地代理;这两个类都继承自 IBinder, 因而都具有跨进程传输的能力;实际上,在跨越进程的时候,Binder 驱动会自动完成这两个对象的转换。
  • Stub: AIDL 的时候,编译工具会给我们生成一个名为 Stub 的静态内部类;这个类继承了 Binder, 说明它是一个 Binder 本地对象,它实现了 IInterface 接口,表明它具有 Server 承诺给 Client 的能力;Stub 是一个抽象类,具体的 IInterface 的相关实现需要开发者自己实现。

五. 总结

IPC对比

Binder 底层采用mmap
mmap通过 系统API磁盘与虚拟内存进行映射,使应用在操作这块虚拟内存的同时对磁盘产生实际的修改。

5.1 我的理解

  1. 熟悉操作系统的都知道,操作系统是支持将一部分硬盘(ROM)作为内存(RAM)使用的,这部分空间也就是称为虚拟内存
  2. mmap 所谓的映射在我看来其实就是手动指定一块硬盘作为内存使用并且明确的拿到这块内存的指针
  3. 此时程序对拿到的虚拟内存的操作本质上就是对硬盘的操作,因为此时的内存其实就是硬盘。
  4. 依靠这种方式,将内核空间服务进程用户空间的部分内存同时映射到一块虚拟内存也就是硬盘中,就实现了修改内核空间同时修改服务进程用户空间的能力。
  5. 这时候客户进程只需要通过传统方式copy_from_user将数据发送到内核映射的虚拟内存即完成了对服务进程用户空间的修改,此时只用了一次数据写入,提高了数据传输效率。
  6. 因此实现流程如下:
    server 将部分内存A映射到硬盘空间D ->
    内核同样将部分内存B映射到D ->
    client 写入数据到内核空间的B中 ->
    由于BA同时映射到D ->
    client 实际上数据写入到 D 并且在AB中同时可以看到

5.2 我的疑问

  1. 既然可以将多块虚拟内存映射到同一块磁盘,那为什么不直接将用户、服务进程进行映射,这样能进一步提高传输效率,实现零写入。
    答:控制复杂,不安全,多个用户进程同时操作一块内存空间容易出现并发错误,因此将映射部分工作交给内核实现,减少容错性和开发复杂度,提高安全性,但是确实会牺牲一定的效率。
  2. MMAP是通过磁盘作为虚拟内存进行映射的原理实现的,那么也就是每一次读写其实都是在磁盘上进行的,众所周知磁盘的性能相对于RAM会低非常多,虽然对于需要对硬盘文件修改的操作无所谓,但是对于跨进程通讯这种本来不需要对硬盘进行修改的行为来说会不会反而降低了效率?
    答:暂时未知,可能这就是Android底层限制Binder大小为1M-8k的理由,不推荐通过这种方式进行大数据传输。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值