前言
最近看到一篇文章被其标题吸引:《Android 11系统服务的添加(java 层)》,心想应用层现在可以这么玩了吗?看完发现原来是framework开发的文章。虽然没有接触过framework开发,但是整篇看完一点都不陌生,跟APP开发中的AIDL开发流程基本相同,只是多了系统服务注册这一步。我们知道AIDL也是基于Binder实现的,看来Binder实在太重要了,本篇主要结合网络和书本上的资料总结一下Binder相关原理。
在介绍Binder原理之前,先回顾一下我们是怎么写AIDL的,看看Binder出现在哪里?
AIDL
本节相关代码在:AndroidIPCDemo
基本使用
服务端定义接口
首先定义服务端AIDL接口,接口文件扩展名为.aidl
,该接口文件的语法基本按照Java的语法来写,只是需要注意下数据类型,默认支持:
- 基本数据类型:int, long, boolean, float, double, char;
- String,Charsequnce;
- Bitmap等系统自带的Parcelable实体;
- List:上述数据类型的ArrayList;
- Map:上述数据类型的HashMap;
- AIDL接口:通常在服务端用RemoteCallbackList保存;
除上述系统默认支持的数据类型,接口中涉及的自定义数据类型都需要通过编写aidl文件来实现。
其中AIDL接口特别需要关注:
- RemoteCallbackList支持客户端向服务端注册/注销监听,从而实现跨进程的EventBus;
- 由于支持AIDL作为接口返回值,我们可以借此实现BinderPool(Binder连接池),后面会有介绍;
来看一个简单的aidl示例接口:
文件位于位于src/main/aidl/com/hao/android_api_test/aidl/IErrorService.aidl
。
// IStringService.aidl
package com.hao.android_api_test.aidl;
// Declare any non-default types here with import statements
interface IErrorService {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
// void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
// double aDouble, String aString);
void reportErrorMsg(String errorMsg);
String testGetString(String seed);
}
补充:这些aidl方法将运行在服务端的Binder线程池中,而不是UI线程,所以所有的共享数据类型如List等,都需要考虑线程安全。
然后build一下项目,SDK会为我们生成对应的接口实现,上述aidl生成的java文件如下(出于阅读方便的考虑,已做格式化处理):
文件位于:build\generated\aidl_source_output_dir\debug\out\com\hao\android_api_test\aidl\IErrorService.java
/*
* This file is auto-generated. DO NOT MODIFY.
*/
package com.hao.android_api_test.aidl;
// Declare any non-default types here with import statements
public interface IErrorService extends android.os.IInterface {
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.hao.android_api_test.aidl.IErrorService {
private static final java.lang.String DESCRIPTOR = "com.hao.android_api_test.aidl.IErrorService";
/** Construct the stub at attach it to the interface. */
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.hao.android_api_test.aidl.IErrorService interface,
* generating a proxy if needed.
*/
public static com.hao.android_api_test.aidl.IErrorService asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.hao.android_api_test.aidl.IErrorService))) {
return ((com.hao.android_api_test.aidl.IErrorService) iin);
}
return new com.hao.android_api_test.aidl.IErrorService.Stub.Proxy(obj);
}
@Override
public android.os.IBinder asBinder() {
return this;
}
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
java.lang.String descriptor = DESCRIPTOR;
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(descriptor);
return true;
}
case TRANSACTION_reportErrorMsg: {
data.enforceInterface(descriptor);
java.lang.String _arg0;
_arg0 = data.readString();
this.reportErrorMsg(_arg0);
reply.writeNoException();
return true;
}
case TRANSACTION_testGetString: {
data.enforceInterface(descriptor);
java.lang.String _arg0;
_arg0 = data.readString();
java.lang.String _result = this.testGetString(_arg0);
reply.writeNoException();
reply.writeString(_result);
return true;
}
default: {
return super.onTransact(code, data, reply, flags);
}
}
}
private static class Proxy implements com.hao.android_api_test.aidl.IErrorService {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/// void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
// double aDouble, String aString);
@Override
public void reportErrorMsg(java.lang.String errorMsg) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(errorMsg);
mRemote.transact(Stub.TRANSACTION_reportErrorMsg, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
@Override
public java.lang.String testGetString(java.lang.String seed) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.lang.String _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(seed);
mRemote.transact(Stub.TRANSACTION_testGetString, _data, _reply, 0);
_reply.readException();
_result = _reply.readString();
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
static final int TRANSACTION_reportErrorMsg = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_testGetString = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/// void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
// double aDouble, String aString);
public void reportErrorMsg(java.lang.String errorMsg) throws android.os.RemoteException;
public java.lang.String testGetString(java.lang.String seed) throws android.os.RemoteException;
}
先不去看里面的逻辑,我们看下这个生成文件的主要内容:
- 首先这个是个java文件,是最终要打包进apk或aar的;
- IErrorService.java首先将IErrorService.aidl内容完全复制过来(包括注释),然后令接口继承于
android.os.IInterface
,接口方法签名抛出android.os.RemoteException
; - 在IErrorService内部提供了其抽象实现类Stub,Stub继承于
android.os.Binder
,实现IErrorService接口(未实现接口方法),同时,Stub内部还提供了一个Proxy类,同样实现了IErrorService接口(实现了接口方法);
其实这个才是真正的aidl的实现代码,前面我们写aidl文件就是为了生成这个东西,所以aidl文件被方法在了java之外的单独目录下,一是便于找到aidl文件,而是打包的时候可以直接忽略这个目录。如果你能自己写出上面的代码,可以不需要写aidl文件,但这就像Parcelable代码一样,都是固定的模版代码,完全可以自定生成,人为书写反而容易出错。
服务端创建服务实现接口
然后在服务端的java目录同包名下,我们为这个接口提供真正的实现:
代码位于src\main\java\com\hao\android_api_test\aidl\ErrorService.java
package com.hao.android_api_test.aidl;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
public class ErrorService extends Service {
private static final String TAG = "ErrorService";
private IErrorService binder=new IErrorService.Stub() {
@Override
public void reportErrorMsg(String errorMsg) throws RemoteException {
doReportError(errorMsg);
}
@Override
public String testGetString(String seed) throws RemoteException {
return "Hello from ErrorService!!!";
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
return (IBinder) binder;
}
}
这部分比较简单,首先提供IErrorService.Stub的实现类,然后在onBind回调中返回。前面也提到了IErrorService.Stub是一个抽象类,因此我们必须实现IErrorService的方法。
客户端引入服务接口
这步直接将服务端编写的aidl整个文件夹拷贝到客户端这边的main目录下(包名不能改),然后如法炮制build生成IErrorService接口实现。比较简单就不过多介绍了。
客户端调用服务接口
从前面可知服务端通过onBind回调返回IErrorService.Stub实现对象,因此我们需要用context.bindService()来获取远程服务。
这里分两种情况:
- 客户端跟服务端是同一个APP:
// 直接启动服务
Intent intent = new Intent(this, ErrorService.class);
mContext.bindService(intent,connection, Context.BIND_AUTO_CREATE);
- 客户端与服务端不是同一个APP:
// Intent必须为显式
Intent intent = new Intent("me.android.ipc.server.aidl");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {//安卓11
intent.setClassName("com.hao.android_api_test", "com.hao.android_api_test.aidl.ErrorService");
} else {
intent.setPackage("com.hao.android_api_test");
}
mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE);
我们在connection中就可以获取到服务并调用其方法了:
IErrorService myService;
ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
myService=IErrorService.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
myService=null;
}
};
高阶使用
远程服务断开
由于远程服务对于客户端来说是不可控的,我们在使用远程服务期间也需要监听远程的状态,Binder提供主动和被动获取远程服务可用状态的方法:
- 主动判断
boolean alive = myService.isBinderAlive();
- 被动回调
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
@Override
public void binderDied(){
// 这里运行在客户端Binder线程池
if(myService==null) return;
myService.asBinder().unlinkToDeath(mDeathRecipient, 0);
myService = null;
}
}
// 然后在服务连接成功时使用
ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
myService=IErrorService.Stub.asInterface(service);
service.linkToDeath(mDeathRecipient, 0);
}
@Override
public void onServiceDisconnected(ComponentName name) {
// 服务端意外断开时调用,客户端主动断开不会调用
// 这里运行在客户端UI线程,可以尝试重新连接服务
myService=null;
}
};
服务端鉴权
验证方式
验证方式主要有两种:permission
校验和进程名称校验。
permission校验
这种校验方式是Android系统提供的校验方式。具体方法是:
在服务端manifest中声明权限:
<permission android:name="packagename.PERMISSION_NAME"
android:protectionLevel="normal"/>
在服务端验证入口处调用Context#checkCallingOrSelfPermission
检查权限,比如:
@Nullable
@Override
public IBinder onBind(Intent intent) {
int res = checkCallingOrSelfPermission("");
if (res != PackageManager.PERMISSION_GRANTED) {
// 鉴权失败则返回null,令客户端无法获取服务
return null;
}
return mBinder;
}
客户端需要使用此服务则需要在manifest中注册权限:
<uses-permission android:name="packagename.PERMISSION_NAME" />
进程校验
可以通过获取Pid和Uid,然后获取对应进程包名等信息进行校验。
int pid = Binder.getCallingPid();
int uid = Binder.getCallingUid();
String[] packagesForUid = context.getPackageManager().getPackagesForUid(uid);
// do some checking to packagesForUid
有时候出于安全考虑需要结合两种校验方式一起使用
验证入口
验证入口如要有两个:Service#onBind
和IErrorService.Stub#onTransact
。
Service#onBind
@Nullable
@Override
public IBinder onBind(Intent intent) {
if (checkFailed) {
// 鉴权不通过则返回null,令客户端无法获取服务
return null;
}
return mBinder;
}
IErrorService.Stub#onTransact
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
if(checkFailed){
// 鉴权不通过,返回false,使客户端无法使用服务
return false;
}
....
return super.onTransact(code, data, reply, flags);
}
}
Binder连接池
BinderPool设计核心思想是利用AIDL支持接口返回AID类型这个特性,通过一个总的AIDL Service来管理APP中所有的AIDL接口,解决每增加一个AIDL接口就要开启一个对应服务的问题。客户端侧来看,每次都从BinderPool查询AIDL接口(是不是很像context#getSystemService
?ServiceManager 其实就是系统的BinderPool),对于客户端来说也是一件好事(相当于门面模式),因此在使用AIDL时,这是一种非常推荐的实践方案。
这里具体的实现代码出于篇幅的限制(懒),可以参考《Android开发艺术探索》第二章“IPC机制”
分析与小结
从客户端调用入手回顾一下这个流程:
- 客户端
IErrorService.Stub.asInterface(IBinder service)
运行在客户端进程,这一步通过service.queryLocalInterface(DESCRIPTOR)
去查询本地(这里Local应该理解为本进程)服务,DESCRIPTOR
是Binder的ID,通常就是Binder的完整类名。查到说明客户端和服务在同一进程,则直接返回,此时不涉及进程间通讯;否则创建一个客户端代理IErrorService.Stub.Proxy
并返回,此时才是进程间通讯; - 客户端调用IErrorService方法
运行在客户端进程。我们从上一步可知,实际在客户端调用的是IErrorService.Stub.Proxy
对应的方法,Proxy所有方法运行在客户端进程,我们以其中一个方法testGetString
为例:
从前面代码可以看到先是创建了两个android.os.Parcel
用于承载入参和返回值的payload,然后调用mRemote.transact(Stub.TRANSACTION_testGetString, _data, _reply, 0)
,mRemote是远程服务,所以这个方法运行在服务进程,其中code可以理解为方法ID,对应实现在android.os.Binder#transact
:
/**
* Default implementation rewinds the parcels and calls onTransact. On
* the remote side, transact calls into the binder to do the IPC.
*/
public final boolean transact(int code, Parcel data, Parcel reply,
int flags) throws RemoteException {
if (false) Log.v("Binder", "Transact: " + code + " to " + this);
if (data != null) {
data.setDataPosition(0);
}
boolean r = onTransact(code, data, reply, flags);
if (reply != null) {
reply.setDataPosition(0);
}
return r;
}
看到其中调用了onTransact方法,根据这个方法的API doc,我们知道其实就是调用了IErrorService.Stub#onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
,也就是在服务端进程中,所以实现了IPC调用。
值得注意的是执行mRemote.transact(Stub.TRANSACTION_testGetString, _data, _reply, 0)
方法时,由于涉及到进程的变化,客户端当前线程会被挂起,直到onTransact方法返回。从这里我们可以得出结论:挡在UI线程调用AIDL方法时,AIDL方法中不能进行耗时的操作;如果是耗时的AIDL方法,则必须在客户端开线程调用。
这个过程最“神奇”的地方在于在
ServiceConnection#onServiceConnected(ComponentName name, IBinder service)
回调中,这个service对象就是远程服务的IErrorService.Stub实现对象,那么系统是如何做到这一点的呢?
其他IPC场景
我们知道Android四大组件在注册时指定进程名称就可以为其开启独立进程运行模式:
android:process=":remote"
:私有进程,进程名称为packagename:remote
android:process="packagename.push"
:全局进程,进程名称为packagename.push
私有进程归主进程私有,其他组件不能与之运行在一个进程中;而对于全局进程,其他应用可以指定ShareUID与之运行在同一进程,从而相互访问私有数据,比如data、组件信息等。
Messenger
每次需要IPC都要写AIDL是不是很麻烦?Android为我们考虑到了这一点,所以提供了Messenger,你可以理解为通用的AIDL,使用方式也和AIDL极为相似。
服务端提供服务
public class MessengerService extends Service {
private static final String TAG = "MessengerService";
private static class _Handler extends Handler{
private Messenger mClientMessenger;
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
// 获取到客户端Messenger后,可以双向通信了
mClientMessenger = msg.replyTo;
Log.d(TAG, "handleMessage: receive from client: "+msg.getData());
break;
default:
super.handleMessage(msg);
break;
}
}
}
private final Messenger mMessenger = new Messenger(new _Handler());
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
}
至于服务端鉴权,可以通过permission
方式在onBind中实现,这里就不赘述了。
这里还有问题,上面这种代码实现方式服务端只能服务一个客户端,如果有多个客户端该怎么办?
客户端调用服务
public class MessengerActivity extends Activity {
private Messenger mMessenger;
private final ServiceConnection mConnection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mMessenger = new Messenger(service);
Message message = Message.obtain(null, 1);
// message.obj="hello from client"; //跨进程时obj不能是string或其他类型,只能是系统的Parcelable数据
Bundle data = new Bundle();
data.putString("reply", "hello from client");
message.setData(data);
try {
mMessenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FrameLayout frameLayout = new FrameLayout(this);
Button button = new Button(this);
button.setText("调用messenger");
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.gravity= Gravity.CENTER;
frameLayout.addView(button,lp);
setContentView(frameLayout);
Intent intent = new Intent(this, MessengerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(mConnection);
super.onDestroy();
}
}
小结
可以看到通过Messenger可以实现AIDL的数据通讯需求,优点是通用性和扩展性都比较好,但是相比AIDL缺点也很明显了:对于需要返回值的情形,由于Messenger需要借助Handler实现数据响应,这就造成代码实现逻辑上的中断,这就提高了代码的维护难度。不过对于单项数据传输,Messenger不失为一个好的方案。
Bundle
即Intent传参,不同进程的四大组件之间可通过Intent相互发送数据。
Activity
通过context#startActivity
启动其他进程的Activity时传参,以及在页面关闭finish之前通过activity#setResult
返回参数。局限性比较明显,整个生命周期就这两次IPC的机会。
BroadcastReceiver
通过context#sendBroadcast
发送和BroadcastReceiver#onReceive
接受,在生命周期内可以多次调用,不过这种IPC方式在后面发布的新系统中越来越受限制。
ContentProvider *
如果IPC用于数据传输,其实最好的方案就是ContentProvider,因此详细介绍一下。ContentProvider底层同样用Binder实现,经过良好的封装对于使用者已经非常友好,我们只需要关注SQL操作以及SQL注入、鉴权等安全性问题即可。
基本使用
服务端
- 实现ContentProvider
public final class ContentProviderServer extends ContentProvider {
public static final String TAG = "ContentProviderServer";
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Log.d(TAG, "query: uri = "+uri);
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
// 数据发生变化,通知外部监听者
getContext().getContentResolver().notifyChange(uri,null);
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
// 数据发生变化,通知外部监听者
getContext().getContentResolver().notifyChange(uri,null);
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
// 数据发生变化,通知外部监听者
getContext().getContentResolver().notifyChange(uri,null);
return 0;
}
}
注意:由于ContentProvider的CRUD方法运行在Binder的线程池,因此数据必须考虑线程安全性问题。对于SQLite如果代码中只有一个SQLiteDatabase对象,那么数据是可以保证线程安全的,因为SQLiteDatabase内部做了同步处理,但是如果程序内部同一个表有多个SQLiteDatabase对象,那么无法保证数据的线程安全!
- 注册清单文件
<!-其中权限,非必须,一旦注明测客户端必须声明对应权限才能访问 !>
<provider
android:exported="true"
android:authorities="me.android.ipc.server.contentprovider"
android:name=".ipc.ContentProviderServer"
android:permission="permission.name"
android:readPermission="permission.name.read"
android:writePermission="permission.name.write"
/>
客户端
获取ContentResolver调用对应方法:
Uri uri = Uri.parse("content://%authorities_for_provider%[/path]");
ContentResolver cr = context.getContentResolver();
Cursor cursor = cr.query(uri, null, null,null,null);
while(cursor.moveToNext()){
XXX value = cursour.getXXX(index);
}
cursor.close();
....
其他比较重要的点
- 系统提供了许多现成的ContentProvider,如通讯录、日程表、MediaStore等;
- ContentProvider只是提供了标准的CRUD接口(在binder线程池中运行),具体数据存储方式没有要求,可以是sqlite db、xml、文件甚至内存数据;
- ContentProvider的onCreate方法在系统启动时会被调用,可以用来做很多初始化工作,比如三方库初始化,参见androidx的startup包;
Service
可在context#startServcie
和context#bindService
时传参,其中前者可以在生命周期内多次调用,是一个明显的优势。
剪贴板
系统剪切板服务,使用方法这里不介绍。
非Binder方案
共享文件
不同进程读写同一个文件进行数据传输,包括但不限于:SharedPreferences、ObjectStream、file等。这种方式最大隐患在于并发读写场景,无法保证数据同步。
- 文件
跨进程读写同步文件数据,可以考虑像Windows那样生成一个filename.lock,操作完成后将之删除。 - SharedPreferences
不建议跨进程使用,可能造成数据丢失,不过已有支持跨进程的SharedPreferences三方库。
Socket
客户端
有空补上代码,可以参考《Android开发艺术探索》第二章“IPC机制”
服务端
有空补上代码,可以参考《Android开发艺术探索》第二章“IPC机制”
小结
在IPC通讯过程中两个非常重要的点是:
- 服务端:安全性
- 客户端:可靠性和可用性
原理篇
本篇介绍IPC实现原理,来自一些书籍和文章,经过个人理解和梳理后得到,如有错误请在评论中指出。
Linux进程隔离
Linux中不同进程间的用户空间是完全隔离的,内核空间是共享的。要理解Linux是如何实现此设计,要知道一些基本概念。
系统中所有的数据都存储在物理内存中,而进程访问物理内存只能通过虚拟地址。因此,若是想成功访问必须得有个前提:虚拟地址和物理内存之间建立映射关系,称为页表(PTE,Page Table Entry)。页表通过mmap创建和维护,不考虑file-back的mapping,只考虑anonymous mapping,当mmap被调用(flag=MAP_ANONYMOUS)时,实际上会做以下两件事:
- 分配一块连续的虚拟地址空间
- 更新这些虚拟地址对应的PTE
- 返回连续虚拟地址空间的起始地址
在mmap调用结束后,其实并不会立即分配物理页,PTE中的虚拟地址默认都指向了同一个zero page(页内容全为0),进程此时访问PTE中的这些虚拟地址读取出来的值都是0。直到进程向虚拟地址写入数据,在page fault handler中触发一个正常的copy-on-write机制,需要写多少页,就会新分配多少物理页(lazy/on-demand allocation)。
有了这个前提,“进程间用户空间隔离内核空间共享”其实就是不同进程用户空间用不同的页表,而内核空间共用同一个页表,如下图所示:
对于32位系统而言,其寻址空间(虚拟存储空间)为2^32 byte ,也就是4GB,其中高位1GB空间为内核空间,低位3GB空间为用户空间。
内核态和用户态
用户空间需要访问内核资源,比如文件操作、访问网络等等。为了突破隔离限制,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问。当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。
系统调用主要通过如下两个函数来实现:
copy_from_user() //将数据从发送发用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到接收方用户空间
内存映射
mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。
更多关于内存映射可参考:认真分析mmap:是什么 为什么 怎么用
Linux IPC技术
共享内存
A进程通过(用户空间)虚拟地址访问到B进程中的数据,最高效的方式就是修改A/B进程中某些虚拟地址的PTE,使得这些虚拟地址映射到同一片物理区域。这样就不存在任何拷贝,因此数据在物理空间中也只有一份。
共享内存虽然高效,但由于物理内存只有一份,因此少不了考虑各种同步机制。让不同进程考虑数据的同步问题,这对于Android而言是个挑战。因为作为系统平台,它必然希望降低开发者的开发难度,最好让开发者只用管好自己的事情。因此,让开发者频繁地考虑跨进程数据同步问题不是一个好的选择。
内存共享是真正意义上的零拷贝传输,但是开发者需要自己处理进程间数据同步问题,使用技术门槛过高。
内存拷贝
内存拷贝方案保证不同进程都拥有一块属于自己的数据区域,该区域不用考虑进程间的数据同步问题,然后通过共享的内核空间作为数据中转站。这里又有二次拷贝和一次拷贝等不同实现方案:
- 二次拷贝
一次是由发送进程的用户空间拷贝到发送进程的内核空间,另一次是由接收进程的内核空间拷贝到接收进程的用户空间。采用二次拷贝的技术有:- Socket:作为一款通用接口,采用C/S架构,传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。
- 消息队列:采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。
- 管道:同消息队列。
- 一次拷贝
不同进程中特定的用户空间虚拟地址指向同一物理页(比如借助mmap),这样只需要发送进程用户空间到发送进程内核空间一次拷贝,由于指向同一物业页,接收进程用户空间虚拟地址也指向相同物理地址,因此可以直接访问。安卓Binder驱动采用了一次拷贝方案。
补充:Java NIO的零拷贝不是指没有拷贝,而是指没有用户空间的拷贝,直接内核空间到内核空间的一次拷贝方案。
小结
IPC技术除了性能因素外,还有一个重要的指标是安全性,Binder提供了可靠的基于UID/PID的身份验证功能,因此综合使用成本、执行效率和安全性来看,基于一次拷贝的Binder是Android系统最佳的IPC方案。
Android Binder驱动
终于轮到我们的主角Binder了。那么Binder是什么呢?
- 从代码看:Binder是Android SDK中的一个类,其实现了IBinder接口;
- 从定位看:是Android一种IPC方式,还可以理解为一种虚拟的物理设备,其设备驱动是/dev/binder,我们常见的安卓系统架构图中,Binder驱动就位列驱动层;
- 从功能看:是ServiceManager连接各种Manager(ActivityManager等)和相应ManagerService的桥梁;
- 从使用看:是bindService后从ServiceConnection回调中返回的远程服务对象;
如果你理解了前面Binder池的设计理念,那么就很容易理解下面ServiceManager机制了: