关于Binder知识的整理

1,768 阅读11分钟

什么是进程间通信

百度百科中对进程间通信的解释

进程间通信(IPC,Interprocess communication)是一组编程接口。能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息, 简单来说就是让我们能够在不同进程间,进行数据的交互。

为什么要用进程间通信

在理解了什么是进程间通信,就有一个值得思考的问题。为什么要有进程间通信,放在一起不行么?在回答这个问题之前,想象一下,所有的用户进程都认为是一个小房子,原本大家都住在自己的房间里,没有了障碍,我就能去你家,随意的拿走你家的任何东西,

操作系统为了阻挡你去串门,给你设置了一个障碍,让你不能够所以的访问其他进程的信息,这个障碍在操作系统中有一个专门的名字: 进程隔离,其目的就是为保护操作系统中进程互不干扰。

那么为什么就一定需要进程间通信呢?设想一下,大家都住在一个楼里面,难免会遇到家里没酱油了,就需要去隔壁家借一点酱油,操作系统就不能不让你去借,然后就有一个管理员专门负责把隔壁家的酱油运送到你家,这就是所谓的进程间通信。

进程间通信有哪些方式

ContentProvider为存储和获取数据了提供统一的接口,它可以在不同的应用程序之间共享数据,本身就是适合进程间通信的。ContentProvider底层实现也是Binder,但是使用起来比AIDL要容易许多。系统中很多操作都采用了ContentProvider,例如通讯录,音视频等,这些操作本身就是跨进程进行通信。

  • 文件共享

两个进程通过读写同一个文件来进行数据共享,共享的文件可以是文本、XML、JOSN。文件共享适用于对数据同步要求不高的进程间通信。

  • 序列化

序列化指的是Serializable/Parcelable,Serializable是Java提供的一个序列化接口,是一个空接口,为对象提供标准的序列化和反序列化操作。Parcelable接口是Android中的序列化方式,更适合在Android平台上使用,用起来比较麻烦,效率很高。

  • Bundle Bundle实现了Parcelable接口,所以它可以方便的在不同的进程间传输。Acitivity、Service、Receiver都是在Intent中通过Bundle来进行数据传递。

上面的方式。除了文件共享,看一下其他的几种方法,都是使用Binder去实现进程间通信。

Binder是什么

Binder 前生是Be Inc公司开发的OpenBinder框架,后来后来OpenBinder的作者Dianne Hackborn加入了Google公司,并负责Android平台的开发工作,顺便把这项技术也带进了Android。Binder本身是Android上的进程间通信技术

Android平台绝大部分的跨进程都是通过Binder完成的.而他也承载着极其重要的作用。

为什么要用Binder

回答为什么要用Binder 之前,其实还有一个问题,除了Binder 我们还有哪些选择?

众说周知,Android是基于Linux内核的,Linux 本身就提供了IPC机制,主要有管道(pipe)、信号(sinal)、信号量(semophore)、消息队列(Message)、共享内存(Share Memory)、套接字(Socket)等。

Linux IPC通信的过程,进程A通过copy_from_user的函数将数据拷贝到内核空间的缓冲区,内核空间通过copy_to_user的函数将数据在拷贝给进程B,一共经历了2次数据拷贝,其过程如下图

👆Linux IPC 通信过程

既然已经可以通过Linux这种IPC机制去做进程间通信,为什么还要搞出Binder机制呢。先放上一个Binder的流程图,与Linux 传统的IPC 通信进行对比

👆Binder 通信过程

性能

从性能上分析,binder,通过一次copy将数据复制到内核空间的缓冲区,然后内核空间通过内存映射(mmap)的方式,将数据映射到数据接收缓存区,在以同样的方式将数据接收缓存区的数据映射到进程B,相比 Socket/管道/消息队列 少了一次copy 的过程,其性能也就更高了。相比于内存共享,多了一次拷贝

IPC方式数据copy次数
共享内存0
Binder1
Linux Socket/管道/消息队列2

其次,Linux方式 将数据冲内核空间复制到进程B的过程中,不知道其需要多大的空间,所以需要竟可能申请更大的内存去使用。会造成一定的内存浪费。

所以从性能上分析,Binder的性能更胜一筹,稍差于内存共享

稳定性

从稳定性上分析,Binder 基于 C/S 架构,架构清晰,职责明确,其稳定性完全高于全依赖于内存共享的方式,共享内存没有分层,难以控制,并发同步访问临界资源时,可能还会产生死锁

所以在稳定性是上Binder是要高于内存共享的

安全性

传统的 IPC 接收方无法获得对方可靠的进程用户ID/进程ID(UID/PID),从而无法鉴别对方身份,Android 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志

总结

从性能上,内存共享肯定是最好的。Binder 其次,最差的是Linux 传统的方式。从稳定性上和安全性分析,Binder 最好。综合考虑,Binder 就成为了Android上跨进程通讯的首选方式。

关于为什么要使用Binder 刘皇叔在其Android Binder原理(一)学习Binder前必须要了解的知识点一文中也有详细说明。笔者这里更多作为记录。

Binder如何通信的

通过上面两个小节。知道了Binder是什么。以及为什么是Binder 两个问题。继续分析Binder是如何在Android上做到进程间通信的

架构分析

在分析Binder 之前,要知道Binder 是基于C/S 架构。即 Client/Servcie ,Service 需要先注册到ServiceManager. Client 在通过ServiceManager 去获取服务,从而完成连接关系。如下图所示架构

但是发现,ServiceManager属于一个单独的进程,Client/Service和ServiceManager都存在着进程隔离,如此,Servcie注册到ServiceManager进程依然需要Binder去完成,所以最终的进程间通信应该如下所示

如何发送数据

借用之前写 使用AIDL进程间通信 中的示例,传递一个int类型的参数观察其传递过程,本文以AIDL的方式去分析Binder机制。对于不了解AIDL的小伙伴,可以去看一下之前写的「使用AIDL进程间通信」作为知识储备。

JAVA层

先看到JAVA层。执行代理类,数据会被包装成Parcel对象,(注意Parcel是java的Parcel对象).然后执行BinderProxy(代理对象),最终执行 transactNative 方法

//执行自己写的接口方法
var info = 100
aidl?.getAge(info)
//会执行代理中的updateBookInOut方法
private static class Proxy implements com.starot.lib_intent.aidl.ITest{
    private android.os.IBinder mRemote;
    @Override public com.starot.lib_intent.aidl.BookInfo updateBookOut(com.starot.lib_intent.aidl.BookInfo book) throws android.os.RemoteException
    {
        //创建了java 的  Parcel 对象
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        
        //token 示例中的为 `com.starot.lib_intent.aidl.ITest` 可以看到是自己写的AIDL对象的路劲
        _data.writeInterfaceToken(DESCRIPTOR);
        //设置参数到 Parcel中
        _data.writeInt(age);
        //会执行到 BinderProxy.transact 方法 
        boolean _status = mRemote.transact(Stub.TRANSACTION_updateBookOut, _data, _reply, 0);
    }
}
//BinderProxy
public final class BinderProxy implements IBinder {
    /**
     * Native implementation of transact() for proxies
     */
    public native boolean transactNative(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException;

    public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            //最终会执行native 方法
            return transactNative(code, data, reply, flags);
    }
}

Navite 层

数据继续往下层传递。此时就进入Navite层,在这里原先的Java层Parcel就转成了native的Parcel。通过BpBinder将数据传入到IPCThreadState中进行处理,处理的方式是将原先native的数据通过memcpy进行复制,保存在mOut变量中。这个mOut 也是一个Parcel对象。最终走到 ioctl 方法 调用驱动层的代码.将数据进行内存映射。

对应的源码可点击android_util_Binder.cpp查看

static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
        jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
    //对象转换
    Parcel* data = parcelForJavaObject(env, dataObj);
    Parcel* reply = parcelForJavaObject(env, replyObj);
    //创建IBinder
    IBinder* target = getBPNativeData(env, obj)->mObject.get();
    //执行BpBinder.transact
    status_t err = target->transact(code, *data, reply, flags);
    return JNI_FALSE;
}
status_t BpBinder::transact(
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
    //状态活跃
    if (mAlive) {
        //使用IPCThreadState.transact方法将数据传递
        status_t status = IPCThreadState::self()->transact(
                mHandle, code, data, reply, flags);
        if (status == DEAD_OBJECT) mAlive = 0;
        return status;
    }
    return DEAD_OBJECT;
}
status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)
{
    //写入数据,将数据写入到内存当中,不会去触发将数据给binder驱动
    err = writeTransactionData(BC_TRANSACTION_SG, flags, handle, code, data, nullptr);

    //等待数据  根据不同状态retrun 不同
    err = waitForResponse(reply);
    err = waitForResponse(&fakeReply);
    err = waitForResponse(nullptr, nullptr);
    return err;
}

先分析一下写入的逻辑

status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
    int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
{
    //申明了 binder_transaction_data_sg
    binder_transaction_data_sg tr_sg;
    //将数据保存在 tr_sg 中
    tr_sg.transaction_data.data_size = data.ipcDataSize();
    tr_sg.transaction_data.data.ptr.buffer = data.ipcData();
    tr_sg.transaction_data.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);
    tr_sg.transaction_data.data.ptr.offsets = data.ipcObjects();
    tr_sg.buffers_size = data.ipcBufferSize();
   
    //这里的mOut 是定义的一个Parcel对象。
    //他主要是将我们要发送的数据复制一份拷贝到 mOut 当中, mOut 也是定义的一个类Parcel
    mOut.write(&tr_sg, sizeof(tr_sg));

    return NO_ERROR;
}

最终会执行 �system/libhwbinder/Parcel.cppmemcpy方法

status_t Parcel::write(const void* data, size_t len)
{
    void* const d = writeInplace(len);
    if (d) {
        //这里是将数据内存复制一份给到 mOut
        memcpy(d, data, len);
        return NO_ERROR;
    }
    return mError;
}

数据写完以后,会继续执行waitForResponse方法,接下来我们继续分析

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
    while (1) {
        //执行talkWithDriver 方法
        if ((err=talkWithDriver()) < NO_ERROR) break;
        //获取到返回的对象
        reply->ipcSetDataReference(
              reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
              tr.data_size,
              reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
              tr.offsets_size/sizeof(binder_size_t),
              freeBuffer, this);
    }
}
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
    //定义数据
    binder_write_read bwr;

    //将mOut的数据写入到 bwr
    bwr.write_size = outAvail;
    bwr.write_buffer = (uintptr_t)mOut.data();


    do {
#if defined(__ANDROID__)
        //使用 ioctl 将数据传递出去
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        else
            err = -errno;
#else
        err = INVALID_OPERATION;
#endif
    } while (err == -EINTR);

}

到这里为止,数据已经从java层传到了binder驱动。

内核层

Binder驱动使用mmap的方式内存映射,有兴趣的小伙伴可自行深入了解mmap是如何运行的。笔者对于这一块只知道个大概,不做深入的介绍,免得误人子弟。 PS:个人觉得如果是有兴趣的可以看看,对于Android开发来说,并不一定是非要研究那么深入。例如对于Handler知道了是通过epoll机制实现的就可以了,在往下想知道epoll select 等 有点超纲了哈。

上面都是废话。因为笔者比较菜,还不想承认! (`へ´*)ノ

总结

通过一步一步的分析,可以看到数据被封装成驱动能够识别的数据类型,然后通过 ioctl 方法 将数据传入到驱动,在Binder驱动中,通过mmap 的方式,对数据进行内存映射,从而完成数据从java层到内核的过程。下面是发送流程。

如何接受数据

Java层

这里我们反推,先从Java层入手,获取client传过来的数据,往上看。一步一步分析Java层的数据哪里是源头

//step1:最终是在Service 自定义的Binder 接口中获取数据
class MyBinder : ITest.Stub() {
    override fun getAge(age: Int): Int {
        return 10001
    }
}
//step2:Binder继承自 Stub 在 onTransact 回调中获取数据
public static abstract class Stub extends android.os.Binder implements com.starot.lib_intent.aidl.ITest
{
    @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 TRANSACTION_getAge:
                {
                data.enforceInterface(descriptor);
                int _arg0;
                _arg0 = data.readInt();
                int _result = this.getAge(_arg0);
                reply.writeNoException();
                reply.writeInt(_result);
                return true;
                }
        }
    }
}
//step3:onTransact方法是在Binder的 execTransactInternal 返回出去
private boolean execTransactInternal(int code, long dataObj, long replyObj, int flags,
        int callingUid) {
    res = onTransact(code, data, reply, flags);
    return res;
}
//step4:对与Java层 最开始获取的方法是Binder 的 execTransact 方法
// Entry point from android_util_Binder.cpp's onTransact
@UnsupportedAppUsage
private boolean execTransact(int code, long dataObj, long replyObj,
        int flags) {
        return execTransactInternal(code, dataObj, replyObj, flags, callingUid);
}

Native层

通过注释「Entry point from android_util_Binder.cpp's onTransact」可以知道 Java Binder.java 的 execTransact 方法是在 android_util_Binder.cpp中被触发

class JavaBBinder : public BBinder
{
      status_t onTransact(
        uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) override
    {
        //执行java Binder execTransact 方法
        jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact,
            code, reinterpret_cast<jlong>(&data), reinterpret_cast<jlong>(reply), flags);
        return res != JNI_FALSE ? NO_ERROR : UNKNOWN_TRANSACTION;
    }
}
static int int_register_android_os_Binder(JNIEnv* env)
{
    jclass clazz = FindClassOrDie(env, kBinderPathName);

    gBinderOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    //注意这里 在 register 的时候,就将execTransact 方法保存在mExecTransact中
    gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");
    gBinderOffsets.mGetInterfaceDescriptor = GetMethodIDOrDie(env, clazz, "getInterfaceDescriptor",
        "()Ljava/lang/String;");
    gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");

    return RegisterMethodsOrDie(
        env, kBinderPathName,
        gBinderMethods, NELEM(gBinderMethods));
}

通过倒推的方式,能够知道在Binder虚拟机完成工作以后,执行到了BBinder的onTransact方法,然后到了 java Binder 的 execTransact 方法 最终执行到了 Stub 的 onTransact 方法。

总结

通过AIDL的方式我们能够看到数据如果从Client进程到Service进程。

最后

Binder 是个很大的东西。其中很多部分还没有看懂。以后还是需要在继续看一下。争取彻底搞懂其中每个部分的工作流程。上文的分析,完全是通过数据传递的过程分析。其实还有数据的校验,MMAP 的过程。和Binder驱动交互的过程。等很多细节。

参考