文章目录
- 前言
- 一、Linux传统跨进程通信原理
- 二、Android Binder跨进程通信原理
- 三、Android Binder IPC 通信模型
- 四、Binder机制在Android中的具体实现——实现两个数相加
- 五、Binder高频面试题
前言
对Binder跨进程通信的原理,予以记录!
Binder 是一种进程间通信机制,基于开源的 OpenBinder 实现;OpenBinder 起初由 Be Inc. 开发,后由 Plam Inc. 接手。
一、Linux传统跨进程通信原理
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 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
2、内存映射
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
3、Binder IPC 实现原理
Binder IPC 正是基于内存映射(mmap)来实现的,但是 mmap() 通常是用在有物理介质的文件系统上的。
比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘–>内核空间–>用户空间);通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率。
一次完整的 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与路由器之间的角色关系
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进程需要调用的接口方法
public interface IPlus extends IInterface {
//定义需要实现的接口方法,即Client进程需要调用的方法
public int add(int a, int b);
}
2、建立IPCService
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中
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开辟的物理内存地址转化成虚拟内存地址
Binder采用的是C/S模式的,其中提供服务的进程成为Server进程,访问服务的进程成为Client进程;Server和Client进程通信要依靠运行在内核空间的Binder驱动程序来进行;
Service组件在开启时,会将自己注册到一个Service Manager里,以便于Client进程在ServiceManager中找到它;因此ServiceManager也称为Binder进程间通信的上下文管理者;同时它也需要和普通的Server进程和Client进程通信,所以也是可以看做一个特殊的Service组件;
为什么会出现物理地址和虚拟地址呢?
由于现在的程序app大小都很大了,如果全部加载到内存中去运行需要很多内存,而手机的内存是有限的,又由于当你在运行一个app的时候不是所有的代码都会被加载到内存中去运行,只会加载一部分正在活动的代码,为了满足程序局部性原则,这时出现的物理地址和虚拟地址刚好能解决,能省下不用的内存空间供其他app使用
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的整数倍;
假如页的大小为P,那么在虚拟内存中VP就称为虚拟页;从虚拟内存中拿到的一页的代码放在物理内存中,那么物理内存中也得有一个同样大小可以页存放虚拟页的代码,物理内存中的页称为物理页(PP);
在任何时刻,虚拟页都有以下三种状态中的一种,且以下状态都是在MMU中体现:
未分配的:VM还未分配页(或者未创建),未分配的页还没有任何数据与代码与他们关联,因此也就不占任何磁盘;
已缓存的:当前缓存在物理内存中已分配页;
未缓存的:当前未缓存在屋里内存中已分配页;