文章目录
- 前言
- 一、Linux传统跨进程通信原理
- [二、Android Binder跨进程通信原理](#二、Android Binder跨进程通信原理)
-
- 1、动态内核可加载模块
- 2、内存映射
- [3、Binder IPC 实现原理](#3、Binder IPC 实现原理)
- [三、Android Binder IPC 通信模型](#三、Android Binder IPC 通信模型)
-
- 1、Client/Server/ServiceManager/驱动
- 2、Binder通信过程
- 3、Binder通信中的代理模式
- [4、Binder 的完整定义](#4、Binder 的完整定义)
- 四、Binder机制在Android中的具体实现------实现两个数相加
- 五、Binder高频面试题
前言
对Binder跨进程通信的原理,予以记录!
Binder 是一种进程间通信机制,基于开源的 OpenBinder 实现;OpenBinder 起初由 Be Inc. 开发,后由 Plam Inc. 接手。
一、Linux传统跨进程通信原理
二、Android Binder跨进程通信原理
Android 系统是基于 Linux 内核的,Linux 已经提供了管道、消息队列、共享内存和 Socket 等 IPC 机制。那为什么 Android 还要提供 Binder 来实现 IPC 呢?主要是基于性能、稳定性和安全性几方面的原因。如下图:
理解了 Linux IPC 相关概念和通信原理,接下来我们正式介绍下 Binder IPC 的原理
1、动态内核可加载模块
传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)
2、内存映射
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。
3、Binder IPC 实现原理
Binder IPC 正是基于内存映射(mmap)来实现的,但是 mmap() 通常是用在有物理介质的文件系统上的。
比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘-->内核空间-->用户空间);通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率。
在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder驱动(Binder Dirver)。
而 Binder 并不存在物理介质,因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间。
一次完整的 Binder IPC 通信过程通常是这样:
- 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
- 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
- 发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。如下图:
说明1:Client进程、Server进程 & Service Manager 进程之间的交互都必须通过Binder驱动(使用 open 和 ioctl文件操作函数),而非直接交互
三、Android Binder IPC 通信模型
介绍完 Binder IPC 的底层通信原理,接下来我们看看实现层面是如何设计的。
一次完整的进程间通信必然至少包含两个进程,通常我们称通信的双方分别为客户端进程(Client)和服务端进程(Server),由于进程隔离机制的存在,通信双方必然需要借助 Binder 来实现。
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 驱动的交互来间接的实现跨进程通信。
说明2:Binder请求的线程管理
Server进程会创建很多线程来处理Binder请求,Binder模型的线程管理采用Binder驱动的线程池,并由Binder驱动自身进行管理而不是由Server进程来管理的
一个进程的Binder线程数默认最大是16,超过的请求会被阻塞等待空闲的Binder线程。所以,在进程间通信时处理并发问题时,如使用ContentProvider时,它的CRUD(创建、检索、更新和删除)方法只能同时有16个线程同时工作
说明3: Binder驱动 & Service Manager进程 属于 Android基础架构(即系统已经实现好了);而Client 进程 和 Server 进程 属于Android应用层(需要开发者自己实现)
Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之前的关系。
Binder与路由器之间的角色关系
通常我们访问一个网页的步骤是这样的:首先在浏览器输入一个地址,如http://www.google.com 然后按下回车键。但是并没有办法通过域名地址直接找到我们要访问的服务器,因此需要首先访问 DNS 域名服务器,域名服务器中保存了 http://www.google.com 对应的 ip 地址 10.249.23.13,然后通过这个 ip 地址才能访问到 http://www.google.com 对应的服务器。
Binder 驱动就如同路由器一样,是整个通信的核心;驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,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。
2、Binder通信过程
至此,我们大致能总结出 Binder 通信过程:
- 首先,一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
- Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
- Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
我们看到整个通信过程都需要 Binder 驱动的接入。下图能更加直观的展现整个通信过程(为了进一步抽象通信过程以及呈现上的方便,下图我们忽略了 Binder 实体及其引用的概念):
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 进程,一次通信就完成了。
4、Binder 的完整定义
现在我们可以对 Binder 做个更加全面的定义了:
- 从进程间通信的角度看,Binder 是一种进程间通信的机制;
- 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
- 从 Client 进程的角度看,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理
- 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象做一点点特殊处理,自动完成代理对象和本地对象之间的转换。
四、Binder机制在Android中的具体实现------实现两个数相加
1、定义Client进程需要调用的接口方法
java
public interface IPlus extends IInterface {
//定义需要实现的接口方法,即Client进程需要调用的方法
public int add(int a, int b);
}
2、建立IPCService
java
public class IPCService extends Service {
public static final String DESCRIPTOR = "add two int";
private final MyAddBinder mBinder = new MyAddBinder();
public IPCService() {
/**
* 将(descriptor, plus)作为(key, value)对存入到Binder对象中的一个Map<String, IInterface>中
* 之后binder对象可根据descriptor找到对应IInterface对象的引用,进而调用其方法
*
* @param plus
* @param descriptor
*/
IPlus plus = new IPlus() {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public IBinder asBinder() {
return null;
}
};
/**
* 1.将(add two int,plus)作为(key,value)对存入到Binder对象中的一个Map<String,IInterface>对象中
* 2.之后,Binder对象可根据add two int通过queryLocalIInterface()获得对应IInterface对象
*/
mBinder.attachInterface(plus, DESCRIPTOR);
}
private class MyAddBinder extends Binder {
/**
* 继承自IBinder接口的,执行Client进程所请求的目标的方法(子类需要复写该方法)
* 注:运行在Server进程的Binder线程池中;当Client进程发起远程请求时,远程请求会要求系统底层执行回调该方法
* @param code Client进程请求方法标识符。即Server进程根据该标识确定所请求的目标方法
* @param data 目标方法的参数。(Client进程传进来的,此处就是整数a和b)
* @param reply 目标方法执行后的结果(返回给Client进程)
* @param flags
* @return
*/
@Override
protected boolean onTransact(int code, @NonNull Parcel data, Parcel reply, int flags)
throws RemoteException {
if (code == 1) {
Log.d("TAG", "MyBinder Switch块 -----" + Process.myPid());
data.enforceInterface(DESCRIPTOR);
int a = data.readInt();
int b = data.readInt();
int result = ((IPlus) this.queryLocalInterface(DESCRIPTOR)).add(a, b);
reply.writeNoException();
reply.writeInt(result);
return true;
}
Log.d("TAG", "MyBinder OnTransact() ----- " + android.os.Process.myPid());
return super.onTransact(code, data, reply, flags);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
3、MainActivity中bindService,最后将结果显示在TextView中
java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
public static final String DESCRIPTOR1 = "add two int";
private EditText editText1;
private EditText editText2;
private TextView resultText;
private Button addBtn;
private Button subtractBtn;
private IBinder mBinder;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBinder = service;
Log.d("TAG", "客户端-----" + android.os.Process.myPid());
}
@Override
public void onServiceDisconnected(ComponentName name) {
mBinder = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindViews();
addBtn.setOnClickListener(this);
subtractBtn.setOnClickListener(this);
//service方式
Intent service = new Intent(this, IPCService.class);
bindService(service, mServiceConnection, BIND_AUTO_CREATE);
}
private void bindViews() {
editText1 = (EditText) findViewById(R.id.edit_arg1);
editText2 = (EditText) findViewById(R.id.edit_arg2);
resultText = (TextView) findViewById(R.id.result_arg);
addBtn = (Button) findViewById(R.id.btn_add);
subtractBtn = (Button) findViewById(R.id.btn_subtract);
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btn_add) {
add();
}
}
public void add() {
int a = Integer.parseInt(editText1.getText().toString());
int b = Integer.parseInt(editText2.getText().toString());
if (mBinder != null) {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
int _result = -1;
try {
_data.writeInterfaceToken(DESCRIPTOR1);
_data.writeInt(a);
_data.writeInt(b);
mBinder.transact(1, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
resultText.setText(_result + "");
Toast.makeText(MainActivity.this, "result:" + _result,
Toast.LENGTH_SHORT).show();
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
_reply.recycle();
_data.recycle();
}
} else {
Toast.makeText(MainActivity.this, "未连接服务端或服务端异常!",
Toast.LENGTH_SHORT).show();
}
}
}
五、Binder高频面试题
1、Binder为何能实现一次copy?
Binder的一次copy是利用了mmap(内存映射文件:目的是开辟物理地址),内存映射文件是在堆和栈的空余空间
mmap是在linux中的api,可以通过mmap去开辟物理地址空间。
MMU(Memeory Mananger Unit)将mmap开辟的物理内存地址转化成虚拟内存地址
物理地址:内存条上的地址;
虚拟地址:实际上是MMU提供的虚拟地址;
Binder采用的是C/S模式的,其中提供服务的进程成为Server进程,访问服务的进程成为Client进程;Server和Client进程通信要依靠运行在内核空间的Binder驱动程序来进行;
Service组件在开启时,会将自己注册到一个Service Manager里,以便于Client进程在ServiceManager中找到它;因此ServiceManager也称为Binder进程间通信的上下文管理者;同时它也需要和普通的Server进程和Client进程通信,所以也是可以看做一个特殊的Service组件;
注:Binder通信机制中的Service组件和Android中的Service组件是不同的概念;Service Manager里注册服务是在进程间的注册
为什么会出现物理地址和虚拟地址呢?
由于现在的程序app大小都很大了,如果全部加载到内存中去运行需要很多内存,而手机的内存是有限的,又由于当你在运行一个app的时候不是所有的代码都会被加载到内存中去运行,只会加载一部分正在活动的代码,为了满足程序局部性原则,这时出现的物理地址和虚拟地址刚好能解决,能省下不用的内存空间供其他app使用
物理地址:内存条上的地址;
虚拟地址:MMU提供的虚拟的地址;
MMU(Memory Management Unit)内存管理单元:涉及到一个转换物理地址和虚拟地址;
为什么会存在MMU,因为app的整体大小假如是100M,但是实际的活跃代码在内存中只有1M,其他的代码处于磁盘中,所以,cpu在运行代码的时候不可能说只给1M的内存让cpu在里面运行,所以需要给一个MMU中间件,让CPU感觉运行在512M的内存中。
MMU里有页表:页表里保存有效位+地址,有效位为0表示未缓存,为1表示已缓存,只要有效位是1就肯定有地址;
虚拟地址和物理地址,如果虚拟地址中没有的话,MMU就会读取磁盘并拿出来在内存中开辟一块新的空间,并在MMU中存放物理地址和对应的虚拟地址,cpu就会拿到虚拟地址;
2、两个进程间的通信Binder原理
Binder的通信是进程A调用copy_form_user,到内核空间,内核空间同进程B建立了链接,copy_to_user会将数据传给B进程;而进程间的通信大小是1M-8K(copy到内核空间),8K是由于有请求头等信息,一页是4K,需要是4的整数倍;
页:cpu为了高效执行以及内存管理,每次需要拿一个页的代码,这个页表示一块连续的储存空间。常见的4kb,也称为块。
假如页的大小为P,那么在虚拟内存中VP就称为虚拟页;从虚拟内存中拿到的一页的代码放在物理内存中,那么物理内存中也得有一个同样大小可以页存放虚拟页的代码,物理内存中的页称为物理页(PP);
在任何时刻,虚拟页都有以下三种状态中的一种,且以下状态都是在MMU中体现:
未分配的:VM还未分配页(或者未创建),未分配的页还没有任何数据与代码与他们关联,因此也就不占任何磁盘;
已缓存的:当前缓存在物理内存中已分配页;
未缓存的:当前未缓存在屋里内存中已分配页;