一.为啥需要它?
Linux 自 带 多 种 进 程 通 信 方 式 , 为 什 么Android 都没采用而偏偏使用 Binder 通信
Linux 现有的所有进程间 IPC 方式:
1.管道:
什么是管道:
管道可用于具有亲缘关系进程间的通信,管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。 管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一 个缓冲区不需要很大,它被设计成为环形的数据结构,
以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的
时候,管道也自动消失。
缺点: 在创建时分配一个管道时,缓存区大小比较有限;并不适合Android大量的进程通信
2.消息队列:
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的 限制。
缺点: 信息复制两次,额外的 CPU 消耗;不合适频繁或信息量大的通信;
3.共享内存:什么是共享内存:
顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址 无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;
缺点: 通信需要设计复杂的机制保证各个进程通讯有效性。进程间的同步 问题操作系统无法实现,必须各进程利用同步工具解决; 安全问题比较 突出,如果 Android 采用 Binder 无异于将每个 App 放在一个内存中,这样是非常不安全的
4.套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信;
5.信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
6.信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;
接下来正面回答这个问题,从 5 个角度来展开对 Binder 的分析:
(1) 从性能的角度 数据拷贝次数:Binder 数据拷贝只需要一次,而管道、消息队列、Socket 都需要 2 次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder 性能仅次于共享内存。
(2) 从稳定性的角度 Binder 是基于 C/S 架构的,简单解释下 C/S 架构,是指客户端(Client)和服务端(Server)组成的架构,Client 端有什么需求,直接发送给 Server 端去完成,架构清晰明朗,Server 端与 Client 端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。
仅仅从以上两点,各有优劣,还不足以支撑 google 去采用 binder 的 IPC 机制,
那么更重要的原因是:
(3) 从安全的角度 传统 Linux IPC 的接收方无法获得对方进程可靠的 UID/PID,从而无法鉴别对方身份;而 Android 作为一个开放的开源体系,拥有非常多的开发平台,App 来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从 App 商店下载偷窥隐射数据、后台造成手机耗电等等问题,传统 Linux IPC 无任何保护措施,完全由上层协议来确保。
Android 为每个安装好的应用程序分配了自己的 UID,故进程的 UID 是鉴别进程身份的重要标志,前面提到 C/S 架构,Android 系统中对外只暴露 Client 端, Client 端将任务发送给 Server 端,Server 端会根据权限控制策略,判断 UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行。Android 6.0,也称为 Android M,在 6.0 之前的系统是在 App 第一次安装时,会将整个 App 所涉及的所有权限一次询问,只要留意看会发现很多 App 根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后 App 无法正常使用,而一旦授权后,应用便可以胡作非为。针对这个问题,google 在 Android M 做了调整,不再是安装时一并询问所有权限,而是在 App 运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但**同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。**对于 Android M平台上,有些 App 开发者可能会写出让手机异常频繁弹框的 App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是 App,还有Android 系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大 Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的 Android 手机。
传统 IPC 只能由用户在数据包里填入 UID/PID;另外,可靠的身份标记只有由 IPC机制本身在内核中添加。其次传统 IPC 访问接入点是开放的,无法建立私有通道。
从安全角度,Binder 的安全性更高。
说到这,可能有人要反驳,Android 就算用了 Binder 架构,而现如今 Android 手机的各种流氓软件,不就是干着这种偷窥隐射,后台偷偷跑流量的事吗?没错,确实存在,但这不能说 Binder 的安全性不好,因为 Android 系统仍然是掌握主控权,可以控制这类 App 的流氓行为,只是对于该采用何种策略来控制,在这方面 android 的确存在很多有待进步的空间,这也是 google 以及各大手机厂商一直努力改善的地方之一。在 Android 6.0,google 对于 app 的权限问题作为较 多的努力,大大收紧的应用权限;另外,在 Google 举办的 Android Bootcamp 2016 大会中,google 也表示在 Android 7.0 (也叫 Android N)的权限隐私方面会进一步加强加固,比如 SELinux,Memory safe language(还在 research 中) 等等,在今年的 5 月 18 日至 5 月 20 日,google 将推出 Android N。
话题扯远了,继续说 Binder
(4)从语言层面的角度 大家多知道 Linux 是基于 C 语言(面向过程的语言),而 Android 是基于 Java 语言(面向对象的语句),而对于 Binder 恰恰也符合面向对象的思想,将进程间通信转化为通过对某个 Binder 对象的引用调用该对象的方法,而其独特之处在于 Binder 对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder 模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder 更适合基于面向对象语言的 Android 系统,对于 Linux 系统可能会有点“水土不服”。
另外,Binder 是为 Android 这类系统而生,而并非 Linux 社区没有想到 Binder IPC 机制的存在,对于 Linux 社区的广大开发人员,我还是表示深深佩服,让世界有了如此精湛而美妙的开源系统。也并非 Linux 现有的 IPC 机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种 Linux 的 IPC机制都有存在的价值,同时在 Android 系统中也依然采用了大量 Linux 现有的 IPC 机制,根据每类 IPC 的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在 Android OS 中的 Zygote 进程的 IPC 采用的是 Socket(套接字)机制,Android 中的 Kill Process 采用的 signa(l 信号)机制等等。而 Binder 更多则用在 system_server 进程与上层 App 层的 IPC 交互。
(5) 从公司战略的角度
总所周知,Linux 内核是开源的系统,所开放源代码许可协议 GPL 保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受 GPL 保护的 Linux Kernel 是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层 Kernel,那么也必须遵循 GPL 协议。
而 Android 之父 Andy Rubin 对于 GPL 显然是不能接受的,为此,Google 巧妙地将 GPL 协议控制在内核空间,将用户空间的协议采用 Apache-2.0 协议(允许基于 Android 的开发商不向社区反馈源码),同时在 GPL 协议与 Apache-2.0 之间的 Lib 库中采用 BSD 证授权方法,有效隔断了 GPL 的传染性,仍有较大争议,
但至少目前缓解 Android,让 GPL 止步于内核空间,这是 Google 在 GPL Linux 下开源与商业化共存的一个成功典范。
有了这些铺垫,我们再说说 Binder 的今世前缘
Binder 是基于开源的 OpenBinder 实现的,OpenBinder 是一个开源的系统 IPC机制,最初是由 Be Inc.开发,接着由 Palm, Inc.公司负责开发,现在 OpenBinder的作者在 Google 工作,既然作者在 Google 公司,在用户空间采用 Binder 作为核心的 IPC 机制,再用 Apache-2.0 协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder 也是不错的选择。
另外,再说一点关于 OpenBinder,在 2015 年 OpenBinder 以及合入到 Linux Kernel 主线 3.19 版本,这也算是 Google 对 Linux 的一点回馈吧。
综合上述 5 点,可知 Binder 是 Android 系统上层进程间通信的不二选择。
二.怎样使用它?
Binder的工作机制图
客户端
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
bound = true;
Log.e("androidLog", "client onServiceConnected");
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInt(2);
_data.writeInt(4);
iBinder.transact(0x11,_data,_reply,0);
_reply.readException();
int sum=_reply.readInt();
Log.e("androidLog","sum="+sum);
} catch (RemoteException e) {
e.printStackTrace();
}finally {
_data.recycle();
_reply.recycle();
}
}
服务器
class MyBinder extends Binder{
@Override
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
if(code==0x11){
int num1=data.readInt();
int num2=data.readInt();
int num=sum(num1,num2);
reply.writeNoException();
reply.writeInt(num);
Log.e("androidLog",String.format("num1=%s;num2=%s;num=%s",num1,num2,num));
return true;
}
return super.onTransact(code, data, reply, flags);
}
}
Binder的两个很重要的方法linkToDeath和unlinkToDeath.
我们知道,Binder运行在服务端进程,如果服务端进程由于某种原因异常终止,这个时候我们到服务端的Binder连接断裂(称之为Binder死亡),会导致我们的远程调用失败。更为关键的是,如果我们不知道Binder连接已经断裂,那么客户端的功能就会收到影响。为了这个问题,Binder中提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath,我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,这个时候我们就可以重新发起连接请求从而恢复连接。
IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if(iBookManager==null)
return;
Log.e("androidLog","deathRecipient");
iBookManager.asBinder().unlinkToDeath(this,0);
iBookManager = null;
//TODO:这里重新绑定远程 Service
}
};
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
bound = true;
Log.e("androidLog", "client onServiceConnected");
iBookManager = IBookManager.Stub.asInterface(iBinder);
try {
iBinder.linkToDeath(deathRecipient,0);
} catch (RemoteException e) {
e.printStackTrace();
}
}
三.它的原理是什么?
Binder 通信原理与机制
先上一张 Binder 的工作流程图。(如果不清晰,可以 复制图片链接到浏览器 或 保存到本地 查看,我经常都是这样看图的哈)
一开始上手,陌生的东西比较多,But,其实并不复杂。喔,流程图是用 ProcessOn 画的。很棒的在线画图工具。 出发前预备子弹 我们知道进程之间,虚拟地址不同,是不能直接通信的,这是 一种保护机制。打开任务管理器,查看一下 N 多的进程,试想一下如果这些进 程直接通信会带来什么后果?而用户空间可以通过 System calls(系统回调)与内核空间通信的,如果在内核空间中有一个模块,能够完成数据的转发,那么是不是两个进程就可以通信了呢?如下图:
上面提到一些用户空间、内核空间的概念,用户空间也能大概猜到是什么东西, 而内核空间,就知道它是很底层的东西好了。而模块呢,可以简单的理解为实现一个功能的程序或一个硬件电路等,比如玩单片机的时候,会有红外线模块,蓝牙模块,wifi 模块等。这些概念的东西搜索一下百科知道就好。
Binder 驱动
Binder 驱动运行在内核空间,它就是那个内核模块了。Binder 驱动很重要,承担了进程间通信的数据转发等。一提到驱动,也是比较熟悉,你插个U盘,需要驱动吧。而 Binder 驱动也差不多,虽然名字取得很好,功能还很强大。但也不是什么神奇的东西。
Binder 跨进程通信模型
Binder 的通信模型有 4 个角色:Binder Client、Binder Server、Binder Driver(Binder 驱动)、ServiceManager。
想象一个情景:我到北京旅行,要给高中同学寄一张明信片,明信片肯定要写上地址吧,不然怎么寄给对方呢?那么我怎么拿到这个地址呢,很简单,翻一下毕 业相册就好了。而这个记录着同学们通信地址的毕业相册,就相当与一个通讯录。 在 Binder 的通信模型中扮演的是 ServiceManager 的角色。好,现在已经有了通信地址了,那么就找到邮局寄出去就好了。过几天同学就高高兴兴的收到了明信片。那么这个邮局在 Binder 通信模型中扮演的是 Binder 驱动的角色,而作为寄 信人的我就是 Binder Client,收信人同学就是 Binder Server。
先上一张图来描述上面的那个情景:
可以看到,ServiceManager、Binder Client、Binder Server 处于不同的进程,他们三个都在用户空间,而 Binder 驱动在内核空间。(我是特意把 Binder 驱动画的比较大的,因为 Binder 驱动的作用最大)那先来简述一下这个通信模型: 首先是有一个 ServiceManager,刚开始这个通讯录是空白的,然后 Server 进程向ServiceManager 注册一个映射关系表,比如徐同学把自己的地址广东省广州 市xx 区写进通讯录,那么就形成了一张表:
徐同学 —> 广东省广州市 xx 区
之后 Client 进程想要和 Server 进程通信,首先向 ServiceManager 查询地址, ServiceManager 收到查询的请求之后,返回查询结果给 Client。
注意到这里不管是 Server 进程注册,还是 Client 查询,都是经过 Binder 驱动的, 这也真是 Binder 驱动的作用所在,先不急,下面的原理会分析到。
这时候我就拿着地址就开始寄明信片咯。当我把明信片放扔进邮筒,之后的工作就是由邮局去完成了,也就是 Binder 驱动去完成通信的转发。
Binder 通信原理
从寄明信片的例子中,邮递员从邮筒取出明信片,然后跨越千山万水将明信片送达。从这点我们也能想到,其实 Binder 驱动完成的工作是很重要的。
我们来还原一个 Binder 跨进程通信的过程。 案例:Client 进程调用 Server 进程的 computer 对象的 add 方法。
接下来的内容你可能需要知道代理模式才能更好的理解,不过没学习过代理模式 也没关系,可以先读下去,然后在去补一下代理模式,再回来看这篇文章。思路会清晰很多。
1. Server 进程向 ServiceManager 注册,告诉 ServiceManager 我是谁,我有什么,我能做什么。就好比徐同学(Server 进程)有一台笔记本(computer 对象),这台笔记本有个 add 方法。这时 映射关系表就生成了。
2. Client 进程向 ServiceManager 查询,我要调用 Server 进程的 computer 对象的 add 方法,可以看到这个过程经过 Binder 驱动, 这时候 Binder 驱动就开始发挥他的作用了。当向 ServiceManager查询完毕,是返回一个 computer 对象给 Client 进程吗?其实不然, Binder 驱动将 computer 对象转换成了 computerProxy 对象,并转 发给了 Client 进程,因此,Client 进程拿到的并不是真实的 computer 对象,而是一个代理对象,即 computerProxy 对象。很 容易理解这个 computerProxy 对象也是有 add 方法,(如果连 add方法都没有,岂不是欺骗了 Client?),但是这个 add 方法只是对 参数进行一些包装而已。
3. 当 Client 进程调用 add 方法,这个消息发送给 Binder 驱动,这时驱动发现,原来是 computerProxy,那么 Client 进程应该是需要调用 computer 对象的 add 方法的,这时驱动通知 Server 进程,调用你的 computer 对象的 add 方法,将结果给我。然后 Server 进程就将计算结果发送给驱动,驱动再转发给 Client 进程,这时Client 进程还蒙在了鼓里,他以为自己调用的是真实的 computer 对象的 add 方法,其实他只是调用了代理而已。不过 Client 最终 还是拿到了计算结果。
好了,一个通信过程就完成了。我们发现,其实 Binder 驱动就是一个中转。
总结再来梳理总结一下:当 Client 进程向 ServiceManager 查询 Server 进程(我要调用你的某个对象的某个方法了),这个过程也是一个跨进程通信的过程,也经过了 Binder 驱动,这时 Binder 驱动发挥它的作用,来了个狸猫换太子,将 Server 进程中的真实对象转换成代理对象,返回这个代理对象给 Client 进程。 Client 进程拿到了这个代理对象,然后调用这个代理对象的方法,Binder 驱动继续发挥他的使命,它会通知 Server 进程执行计算工作,将 Server 进程中的真实对象执行的结果返回给了 Client 进程,这样 Client 进程还是如愿的得到了自己想要。跨进程通信完毕
四.附加知识有哪些?
谈一谈 Binder 的原理和实现一次拷贝的流程
面试官: 谈一谈 Binder 的原理和实现一次拷贝的流程
心理分析:能问出该问题,面试官对 binder 的理解是非常深入的。想问求职者对 Android 底层有没有深入理解
**求职者:**应该从 linux 进程通信原理的两次拷贝说起,然后引申为什么 binder 却只有一次拷贝 ,最后阐述内核空间 与用户空间的定义
1 Linux 下传统的进程间通信原理
了解 Linux IPC 相关的概念和原理有助于我们理解 Binder 通信原理。因此,在介绍 Binder 跨进程通信原理之前,我们先聊聊 Linux 系统下传统的进程间通信是如何实现。
1.1 基本概念介绍
这里我们先从 Linux 中进程间通信涉及的一些基本概念开始介绍,然后逐步展开,向大家说明传统的进程间通信的原理。
上图展示了 Liunx 中跨进程通信涉及到的一些基本概念:
· 进程隔离
· 进程空间划分:用户空间(User Space)/内核空间(Kernel Space)
· 系统调用:用户态/内核态
进程隔离
简单的说就是操作系统中,进程与进程间内存是不共享的。两个进程就像两个平行的世界,A 进程没法直接访问 B 进程的数据,这就是进程隔离的通俗解释。
A 进程和 B 进程之间要进行数据交互就得采用特殊的通信机制:进程间通信(IPC)。
进程空间划分:用户空间(User Space)/内核空间(Kernel Space)现在操作系统都是采用的虚拟存储器,对于 32 位系统而言,它的寻址空间(虚拟存储空间)就是 2 的 32 次方,也就是 4GB。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备的权限。为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间(User Space)和内核空间(Kernel Space)。针对Linux 操作系统而言,将最高的 1GB 字节供内核使用,称为内核空间;较低的3GB 字节供各进程使用,称为用户空间。
简单的说就是,内核空间(Kernel)是系统内核运行的空间,用户空间(User Space)是用户程序运行的空间。为了保证安全性,它们之间是隔离的。
系统调用:用户态与内核态
虽然从逻辑上进行了用户空间和内核空间的划分,但不可避免的用户空间需要访问内核资源,比如文件操作、访问网络等等。为了突破隔离限制,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0 级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3 级)用户代码中运行。
系统调用主要通过如下两个函数来实现:
copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间
2.2 Linux 下的传统 IPC 通信原理
理解了上面的几个概念,我们再来看看传统的 IPC 方式中,进程之间是如何实现通信的。
通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用copyfromuser() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copytouser() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。如下图:
这种传统的 IPC 通信方式有两个问题:
1. 性能低下,一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝;
2. 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
2. Binder 跨进程通信原理
理解了 Linux IPC 相关概念和通信原理,接下来我们正式介绍下 Binder IPC 的原理。
2.1 动态内核可加载模块 && 内存映射
正如前面所说,跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?这就得益于Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样, Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。
那么在 Android 系统中用户进程之间是如何通过这个内核模块(Binder 驱动)来实现通信的呢?难道是和前面说的传统 IPC 机制一样,先将数据从发送方进程拷贝到内核缓存区,然后再将数据从内核缓存区拷贝到接收方进程,通过两次拷贝来实现吗?显然不是,否则也不会有开篇所说的 Binder 在性能方面的优势了。
这就不得不通道 Linux 下的另一个概念:内存映射。
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。
2.2 Binder IPC 实现原理Binder IPC 正是基于内存映射(mmap)来实现的,但是 mmap() 通常是用在有物理介质的文件系统上的。
比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘-->内核空间-->用户空间);通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代 I/O 读写,提高文件读取效率。
而 Binder 并不存在物理介质,因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间。
一次完整的 Binder IPC 通信过程通常是这样:
1. 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
2. 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
3. 发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间通信。
如下图:
3. Binder 通信模型
介绍完 Binder IPC 的底层通信原理,接下来我们看看实现层面是如何设计的。一次完整的进程间通信必然至少包含两个进程,通常我们称通信的双方分别为客户端进程(Client)和服务端进程(Server),由于进程隔离机制的存在,通信双方必然需要借助 Binder 来实现。
3.1 Client/Server/ServiceManager/驱动
前面我们介绍过,Binder 是基于 C/S 架构的。由一系列的组件组成,包括Client、Server、ServiceManager、Binder 驱动。其中 Client、Server、ServiceManager 运行在用户空间, 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 中一个对象有多个引用一样。
3.2 Binder 通信过程
至此,我们大致能总结出 Binder 通信过程:
1. 首先,一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
2. Server 通 过 驱 动 向 ServiceManager 中 注 册 Binder ( Server 中 的Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager, ServiceManger 将其填入查找表。
3. Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
我们看到整个通信过程都需要 Binder 驱动的接入。下图能更加直观的展现整个通信过程(为了进一步抽象通信过程以及呈现上的方便,下图我们忽略了 Binder实体及其引用的概念):
4 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 Binder 的完整定义现在我们可以对 Binder 做个更加全面的定义了:
- 从进程间通信的角度看,Binder 是一种进程间通信的机制;
- 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
- 从 Client 进程的角度看, Binder 指的是对 Binder 代理对象,是 Binder实体对象的一个远程代理
- 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。
6. 手动编码实现跨进程调用
通常我们在做开发时,实现进程间通信用的最多的就是 AIDL。当我们定义好AIDL 文件,在编译时编译器会帮我们生成代码实现 IPC 通信。借助 AIDL编译以后的代码能帮助我们进一步理解 Binder IPC 的通信原理。但是无论是从可读性还是可理解性上来看,编译器生成的代码对开发者并不友好。比如一个 BookManager.aidl 文件对应会生成一个 BookManager.java 文件,这个 java 文件包含了一个 BookManager 接口、一个 Stub 静态的抽象类和一个 Proxy 静态类。 Proxy 是 Stub 的静态内部类, Stub 又是 BookManager的静态内部类,这就造成了可读性和可理解性的问题。
Android 之所以这样设计其实是有道理的,因为当有多个 AIDL文件的时候把 BookManager、Stub、Proxy 放在同一个文件里能
有效避免 Stub 和 Proxy 重名的问题。
因此便于大家理解,下面我们来手动编写代码来实现跨进程调用。
6.1 各 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 的相关实现需要开发者自己实现。
6.2 实现过程讲解
一次跨进程通信必然会涉及到两个进程,在这个例子中 RemoteService 作为服务端进程,提供服务;ClientActivity 作为客户端进程,使用 RemoteService 提供的服务。如下图:
那么服务端进程具备什么样的能力?能为客户端提供什么样的服务呢?还记得我们前面介绍过的 IInterface 吗,它代表的就是服务端进程具体什么样的能力。
因此我们需要定义一个 BookManager 接口,BookManager 继承自 IIterface,表明服务端具备什么样的能力。
/**
* 这个类用来定义服务端 RemoteService 具备什么样的能力
*/
public interface BookManager extends IInterface {
void addBook(Book book) throws RemoteException;
}
只定义服务端具备什么样的能力是不够的,既然是跨进程调用,那么接下来我们得实现一个跨进程调用对象 Stub。 Stub 继承 Binder, 说明它是一个 Binder 本地对象;实现 IInterface 接口,表明具有 Server 承诺给 Client 的能力; Stub 是一个抽象类,具体的 IInterface 的相关实现需要调用方自己实现。
public abstract class Stub extends Binder implements BookManager {
...
public static BookManager asInterface(IBinder binder) {
if (binder == null)
return null;
IInterface iin = binder.queryLocalInterface(DESCRIPTOR);
if (iin != null && iin instanceof BookManager)
return (BookManager) iin;
return new Proxy(binder);
}
...
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
switch (code) {
case INTERFACE_TRANSACTION:
reply.writeString(DESCRIPTOR);
return true;
case TRANSAVTION_addBook:
data.enforceInterface(DESCRIPTOR);
Book arg0 = null;
if (data.readInt() != 0) {
arg0 = Book.CREATOR.createFromParcel(data);
}
this.addBook(arg0);reply.writeNoException();
return true;
}
return super.onTransact(code, data, reply, flags);
}
...
}
Stub 类中我们重点介绍下 asInterface 和 onTransact 。
先说说 asInterface ,当 Client 端在创建和服务端的连接,调用 bindService 时需要创建一个 ServiceConnection 对象作为入参。在 ServiceConnection 的回调方法 onServiceConnected 中会通过这个 asInterface(IBinder binder) 拿到BookManager 对象,这个 IBinder 类型的入参 binder 是驱动传给我们的,正如你在代码中看到的一样,方法中会去调用 binder.queryLocalInterface() 去查找 Binder 本地对象,如果找到了就说明 Client 和 Server 在同一进程,那么这个 binder 本身就是 Binder 本地对象,可以直接使用。否则说明是 binder 是个远程对象,也就是 BinderProxy。因此需要我们创建一个代理对象 Proxy,通过这个代理对象来是实现远程访问。
接下来我们就要实现这个代理类 Proxy 了,既然是代理类自然需要实现BookManager 接口。
public class Proxy implements BookManager {
...
public Proxy(IBinder remote) {
this.remote = remote;
}
@Override
public void addBook(Book book) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel replay = Parcel.obtain();
try {
data.writeInterfaceToken(DESCRIPTOR);
if (book != null) {
data.writeInt(1);
book.writeToParcel(data, 0);
} else {
data.writeInt(0);
}
remote.transact(Stub.TRANSAVTION_addBook, data, replay, 0);
replay.readException();
} finally {
replay.recycle();
data.recycle();
}
}
...
}
我们看看 addBook() 的实现;在 Stub 类中,addBook(Book book) 是一个抽象方法,Server 端需要去实现它。
- 如果 Client 和 Server 在同一个进程,那么直接就是调用这个方法。
- 如果是远程调用,Client 想要调用 Server 的方法就需要通过 Binder 代理来完成,也就是上面的 Proxy。在 Proxy 中的 addBook() 方法中首先通过 Parcel 将数据序列化,然后调用remote.transact()。正如前文所述 Proxy 是在 Stub 的 asInterface 中创建,能走到创建 Proxy 这一步就说明 Proxy 构造函数的入参是 BinderProxy,即这里的 remote 是个 BinderProxy 对象。最终通过一系列的函数调用,Client 进程通过系统调用陷入内核态, Client 进程中执行 addBook() 的线程挂起等待返回;驱动完成一系列的操作之后唤醒 Server 进程,调用 Server 进程本地对象的onTransact()。最终又走到了 Stub 中的 onTransact() 中, onTransact() 根据函数编号调用相关函数(在 Stub 类中为 BookManager 接口中的每个函数中定义了一个编号,只不过上面的源码中我们简化掉了;在跨进程调用的时候,不会传递函数而是传递编号来指明要调用哪个函数);我们这个例子里面,调用了Binder 本地对象的 addBook() 并将结果返回给驱动,驱动唤醒 Client 进程里刚刚挂起的线程并将结果返回。
这样一次跨进程调用就完成了。完整的代码我放到 GitHub 上了,有兴趣的小伙伴可以去