1. Binder内存机制
首先我们来看下安卓应用进程在内存中的分布,如下图所示:
每个Android进程,只能运行在自己的进程所拥有的虚拟地址空间,如果是32位的系统,对应一个4GB的虚拟地址空间,其中3GB是用户空,1GB是内核空间,而内核空间的大小是可以通过参数配置的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间确实可以共享的。Client进程与Server进程通信,恰恰是利用进程间可共享的内核内空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行。(Binder是Android系统提供的一种IPC机制。每个Android的进程,都可以有一块用户空间和内核空间。用户空间在不同进程间不能共享,内核空间可以共享。Binder就是一个利用可以共享的内核空间,完成高性能的进程间通信的方案。)
Binder进程间通信效率高的最主要原因是Binder的内存机制:虚拟进程地址空间(用户空间)和虚拟内核地址空间(内核空间)都映射到同一块物理内存空间。当Client端与Server端发送数据时,Client(作为数据发送端)先从自己的进程空间把IPC通信数据copy_from_user拷贝到内核空间,而Server端(作为数据接收端)与内核共享数据,不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。
而一般IPC的做法,需要Client端进程空间拷贝到内核空间,再由内核空间拷贝到Server进程空间,会发生两次拷贝。对于进程和内核虚拟地址映射到同一个物理内存的操作是发生在数据接收端,而数据发送端还是需要将用户态的数据复制到内核态。
2. 和传统IPC机制相比,谷歌为什么采用Binder
我们先看下Linux中的IPC通信机制:
1、传统IPC:匿名管道(PIPE)、信号(signal)、有名管道(FIFO) 2、AT&T Unix:共享内存,信号量,消息队列 3、BSD Unix:Socket
虽然Android继承Linux内核,但是Linux与Android通信机制是不同的。Android中有大量的C/S(Client/Server)应用方式,这就要求Android内部提供IPC方法,而如果采用Linux所支持的进程通信方式有两个问题:性能和安全性。那
性能:目前Linux支持的IPC包括传统的管道,System V IPC(包括消息队列/共享内存/信号量)以及socket,但是只有socket支持Client/Server的通信方式,由于socket是一套通用当初网络通信方式,其效率低下,且消耗比较大(socket建立连接过程和中断连接过程都有一定的开销),明显在手机上不适合大面积使用socket。而消息队列和管道采用"存储-转发" 方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存中拷贝到接收方缓存中,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。 安全性:在安全性方面,Android作为一个开放式,拥有众多开发者的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽的情况。传统IPC没有任何安全措施,完全依赖上层协议来去报。首先传统IPC的接受方无法获取对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程的身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道、system V的键值,socket的ip地址或者文件名都是开放的,只要知道这些接入点的程序都可以对端建立连接,不管怎样都无法阻止恶意程序通过接收方地址获得连接。
基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,所以就有了Binder。Binder基于Client/Server通信模式,传输过程只需要一次拷贝,为发送发添加UID/PID身份,支持实名Binder也支持匿名Binder,安全性高。下图为Binder通信过程示例:
相比于传统的跨进程通信手段,通信双方必须要处理线程同步,内存管理等问题,工作量大,而且问题多,就像我们前面介绍的传统IPC 命名管道(FIFO) 信号量(semaphore) 消息队列已经从Android中去掉了,同其他IPC相比,Socket是一种比较成熟的通信手段了,同步控制也很容易实现。Socket用于网络通信非常合适,但是用于进程间通信就效率很低。 Android在架构上一直希望模糊进程的概念,取而代之以组件的概念。应用也不需要关心组件存放的位置、组件运行在那个进程中、组件的生命周期等问题。随时随地的,只要拥有Binder对象,就能使用组件的功能。Binder就像一张网,将整个系统的组件,跨进程和线程的组织在一起。
Binder是整个系统的运行的中枢。Android在进程间传递数据使用共享内存的方式,这样数据只需要复制一次就能从一个进程到达另一个进程了(一般IPC都需要两步,第一步用户进程复制到内核,第二步再从内核复制到服务进程。)
PS: 整个Android系统架构中,虽然大量采用了Binder机制作为IPC(进程间通信)方式,但是也存在部分其他的IPC方式,比如Zygote通信就是采用socket。
3. AIDL
aidl会自动生成一个继承自Binder的Stub的抽象类(Stub实现自定义接口),重写了onTransact方法,在onTransact中从data parcel对象中读取参数,然后根据code调用对应接口方法,并在reply parcel对象中写入返回结果;
同时也会自动生成一个Proxy的类(Proxy也实现自定义接口),该类持有传入的IBinder对象,在对应的自定义接口方法中,调用IBinder的transact传入方法对应的code,参数data parcel,以及返回值reply parcel;
Proxy是Client层用到的,Stub是Server层需要继承实现的;
/** Construct the stub at attach it to the interface. */
publicStub()
{
this.attachInterface(this,DESCRIPTOR);
}
/**
* Cast an IBinder object into an android.content.pm.IPackageStatsObserver interface,
* generating a proxy if needed.
*/
public staticandroid.content.pm.IPackageStatsObserverasInterface(android.os.IBinder obj)
{
if((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if(((iin!=null)&&(iininstanceofandroid.content.pm.IPackageStatsObserver))) {
return((android.content.pm.IPackageStatsObserver)iin);
}
return newandroid.content.pm.IPackageStatsObserver.Stub.Proxy(obj);}
4. Binder通信
下图是一次binder通信的交互示例图:
Binder通信涉及的主要对象及其作用如下表所示:
概念
|
作用
|
Binder代理对象
|
类型为BpBinder,在用户空间创建,且执行在Client进程中.会被Client进程中的其他对象引用,另外会引用Binder驱动程序中的Binder引用对象.
|
Binder引用对象
|
类型为binder_ref,在Binder驱动程序中创建,被Binder代理对象引用.
|
Binder实体对象
|
类型为binder_node,在Binder驱动程序中创建,被Binder引用对象所引用
|
Binder本地对象
|
类型为BBinder,在用户空间中创建,且执行在Server进程中.会被Server进程中其他对象引用,还会被Binder实体对象引用.
|
不管是client还是server,跟binder驱动交互都是通过以下步骤:
一次Binder通信最大可以传输多大的数据
Binder调用有oneway-异步和非oneway-同步两种类型。
1MB-8KB的限制来源于哪里呢?(PS:8k是两个pagesize,一个pagesize是申请物理内存的最小单元)
#defineBINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)//这里的限制是1MB-4KB*2
ProcessState::ProcessState(constchar*driver)
{
if(mDriverFD>=0){
// mmap the binder, providing a chunk of virtual address space to receive transactions.
// 调用mmap接口向Binder驱动中申请内核空间的内存
mVMStart=mmap(0,BINDER_VM_SIZE,PROT_READ,MAP_PRIVATE|MAP_NORESERVE,mDriverFD,0);
if(mVMStart==MAP_FAILED){
// *sigh*
ALOGE("Using %s failed: unable to mmap transaction memory.\n",mDriverName.c_str());
close(mDriverFD);
mDriverFD=-1;
mDriverName.clear();
}
}
}
如果一个进程使用ProcessState这个类来初始化Binder服务,这个进程的Binder内核内存上限就是BINDER_VM_SIZE,也就是1MB-8KB。
virtualvoidonZygoteInit()
{
sp<ProcessState>proc=ProcessState::self();
ALOGV("App process: starting thread pool.\n");
proc->startThreadPool();
}
对于普通的APP来说,都是Zygote进程孵化出来的,Zygote进程的初始化Binder服务的时候提前调用了ProcessState这个类,所以普通的APP进程上限就是1MB-8KB。
但是我们可以通过手写mmap绕过ProcessState的限制;Binder服务的初始化有两步,open打开Binder驱动,mmap在Binder驱动中申请内核空间内存,所以我们只要手写open,mmap就可以轻松突破这个限制。但是mmap中有判断如果传入申请的空间大于4M则会最多只申请4M;
但是为什么ContentProvider却能传输超过4M或更大的数据呢,其实用的是匿名共享内存;
服务端需做以下事情:
- 创建一个匿名共享内存(android.os.SharedMemory即android提供的匿名共享内存,一般使用android.os.MemoryFile,MemoryFile即是对SharedMemory的一个简单封装)
- 往这个共享内存中写一个字符数据
- 将这个匿名共享内存的文件句柄(如FileDescriptor)通过binder机制传递给客户端
客户端需做以下事情:
- bindservice获得服务端的binder对象
- 调用binder的接口获得服务端匿名共享内存的文件句柄
- 通过文件句柄,直接访问匿名共享内存中的数据,并打印出log。