Android无处不在的Binder

前言

最近看到一篇文章被其标题吸引:《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接口特别需要关注:

  1. RemoteCallbackList支持客户端向服务端注册/注销监听,从而实现跨进程的EventBus;
  2. 由于支持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接口(实现了接口方法);
    IErrorService.java内部结构

其实这个才是真正的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#onBindIErrorService.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#startServciecontext#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)时,实际上会做以下两件事:

  1. 分配一块连续的虚拟地址空间
  2. 更新这些虚拟地址对应的PTE
  3. 返回连续虚拟地址空间的起始地址

在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而言是个挑战。因为作为系统平台,它必然希望降低开发者的开发难度,最好让开发者只用管好自己的事情。因此,让开发者频繁地考虑跨进程数据同步问题不是一个好的选择。

内存共享是真正意义上的零拷贝传输,但是开发者需要自己处理进程间数据同步问题,使用技术门槛过高。

内存拷贝

内存拷贝方案保证不同进程都拥有一块属于自己的数据区域,该区域不用考虑进程间的数据同步问题,然后通过共享的内核空间作为数据中转站。这里又有二次拷贝一次拷贝等不同实现方案:

  • 二次拷贝
    一次是由发送进程的用户空间拷贝到发送进程的内核空间,另一次是由接收进程的内核空间拷贝到接收进程的用户空间。采用二次拷贝的技术有:
    1. Socket:作为一款通用接口,采用C/S架构,传输效率低,开销大,主要用在跨网络的进程间通信本机上进程间的低速通信
    2. 消息队列:采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。
    3. 管道:同消息队列。
  • 一次拷贝
    不同进程中特定的用户空间虚拟地址指向同一物理页(比如借助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通讯

如果你理解了前面Binder池的设计理念,那么就很容易理解下面ServiceManager机制了:

Server向ServiceManager注册Binder

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值