美文网首页
图解 | 不得错过的Binder浅析(一)

图解 | 不得错过的Binder浅析(一)

作者: 哈利迪ei | 来源:发表于2020-11-02 13:35 被阅读0次

    Framework和Binder的内容挺深的,本文还是站在应用层开发者的角度来建立基本认知,能在遇到问题的时候有思路和方向即可。(本文将带着关键问题和核心流程展开,不会面面俱到)

    大纲:

    • 背景
      • 为什么要多进程
      • 为什么要Binder
      • Binder简单架构
    • 简单示例
    • 源码分析
      • 客户端与驱动交互
      • 服务端与驱动交互
    • 总结
    • 细节补充
      • Binder为什么高效
      • Binder为什么不用shm
    • 提问
    • 参考资料

    本文约4.0k字,阅读大约17分钟。

    Android源码基于8.0。

    背景

    为什么要多进程

    Binder是Android系统的一种跨进程通信(IPC)机制。

    在Android系统中,单个进程被分配了有限的内存,多进程可以使用更多内存隔离崩溃风险等。

    多进程在Android中常见的使用场景有独立进程的WebView、推送、保活、系统服务等,既然是多进程场景,那么就需要跨进程通信了。

    为什么要Binder

    Linux自带了一些跨进程通信方式:

    • 管道(pipe):管道描述符是半双工,单向的,数据只能往一个方向流,想要读写需要两个管道描述符。Linux提供了pipe(fds)来获取一对描述符,一个读一个写。匿名管道只能用在具有亲缘关系的父子进程间的通信,有名管道无此限制。

    • Socket:全双工,可读可写。如Zygote进程等待AMS系统服务发起socket请求来创建应用进程。

    • 共享内存(shm,Shared Memory):会映射一段能被多个进程访问的内存,是最高效的IPC方式,他通常需要结合其他跨进程方式如信号量来同步信息。Android基于shm改进得到匿名共享内存Ashmem(Anonymous Shared Memory),因高效而适合处理较大的数据,如应用进程通过共享内存来读取SurfaceFlinger进程合成的视图数据,进行展示。

    • 内存映射(mmap):Linux通过将一个虚拟内存区域与一个磁盘上的文件关联起来,以初始化这个虚拟内存区域的内容。通过指针的方式读写内存,系统会同步进对应的磁盘文件。Binder用到了mmap

    • 信号(signal):单向的,发个信号就完事,无返回结果。只能发信号,带不了参数。如子进程被杀掉后系统会发出SIGCHLD信号,父进程会清理子进程在进程表的描述信息防止僵尸进程的发生。

    另外还有文件共享、消息队列(Message)等跨进程通信方式...

    这些跨进程通信方式都各有优劣,Android最终选择了自建一套兼顾好用、高效、安全的Binder。

    • 好用:易用的C/S架构(借助AIDL后只需编写业务逻辑)
    • 高效:用mmap进行内存映射,只需一次拷贝
    • 安全:内核态管理身份标记,每个App有UID来校验权限,同时支持实名(系统服务)和匿名(自己创建的服务)

    Binder简单架构

    Linux内存被分为用户空间内核空间,用户空间需要经过系统调用才能访问到内核空间。

    image

    (图片来源:「写给Android应用工程师的Binder原理剖析」)

    Binder整体基于C/S架构。运行在内核空间的Binder驱动程序,会为用户空间暴露出一个设备文件/dev/binder,进程间通过该文件来建立通信通道。

    image

    Binder的启动过程:

    1. 打开binder驱动(open)
    2. 将驱动文件的描述符(mDriverFD)进行内存映射(mmap),分配缓冲区
    3. 服务端运行binder线程,把线程注册到binder驱动,进入循环等待客户端的指令(两端通过ioctl与驱动交互)

    简单示例

    AIDL(Android接口定义语言)可以辅助生成Binder的Java类,减少重复工作,使用姿势网上有很多,这里就直接手写吧,方便理解。

    示例调用流程如下:

    image

    代码不多,大部分是log,重点看注释就行。

    客户端Activity:

    //NoAidlActivity.java
    
    protected void onCreate(Bundle savedInstanceState) {
        Intent intent = new Intent(this, MyService.class);
    
        bindService(intent, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                //1. 从对象池拿到可复用的对象(享元模式)
                Parcel data = Parcel.obtain();
                Parcel reply = Parcel.obtain();
    
                Log.e("哈利迪", "--- 我是客户端 NoAidlActivity , pid = "
                      + Process.myPid() + ", thread = "
                      + Thread.currentThread().getName());
    
                String str = "666";
                Log.e("哈利迪", "客户端向服务端发送:" + str);
                //2. 往data写数据,作为请求参数
                data.writeString(str);
    
                //3. 拿到服务端的IBinder句柄,调用transact
                //约定行为码是1;需要服务端的返回值,所以flags传0表示同步调用
                service.transact(1, data, reply, 0);
    
                Log.e("哈利迪", "--- 我是客户端 NoAidlActivity , pid = "
                      + Process.myPid() + ", thread = "
                      + Thread.currentThread().getName());
    
                //4. 从reply读取服务端的返回值
                Log.e("哈利迪", "客户端接收服务端返回:" + reply.readString());
            }
        }, Context.BIND_AUTO_CREATE);
    }
    

    service.transact传入了flags为0,表示同步调用,会阻塞等待服务端的返回值。如果服务端进行了耗时操作,此时用户操作UI则会引起ANR。

    flags的另一个值是1,表示异步调用的one way不需要等待服务端的返回结果,先忽略。

    来看服务端运行的Service,

    class MyService extends Service {
    
        @Override
        public IBinder onBind(Intent intent) {
            //返回服务端的IBinder句柄
            return new MyBinder();
        }
    }
    

    注册服务,让服务端Service运行在:remote进程,来实现跨进程,

    <service
             android:name=".binder.no_aidl.MyService"
             android:process=":remote" />
    

    运行在服务端的Binder对象,

    class MyBinder extends Binder {
    
        @Override
        protected boolean onTransact(int code, Parcel data, Parcel reply, int flags){
            if (code == 1) {//如果是约定好的行为码1
                Log.e("哈利迪", "--- 我是服务端 MyBinder , pid = "
                      + Process.myPid() + ", thread = "
                      + Thread.currentThread().getName());
                //1. 从data读取客户端参数
                Log.e("哈利迪", "服务端收到:" + data.readString());
    
                String str = "777";
                Log.e("哈利迪", "服务端返回:" + str);
                //2. 从reply向客户端写返回值
                reply.writeString(str);
    
                //3. 处理完成
                return true;
            }
            return super.onTransact(code, data, reply, flags);
        }
    }
    

    运行如下,7行日志:

    image

    由于我们的flags传入的是0同步调用,可以试着在服务端onTransact里sleep几秒,会发现客户端需要几秒后才能打印出返回值。所以如果服务端需要进行耗时操作,客户端则需要在子线程里进行binder调用。

    延伸:从 IT互联网大叔 的「android获取进程名函数,如何优化到极致」一文可见,在使用系统API时,如果有更好的方案,还是建议将跨进程方案getSystemService放到最后作为兜底,因为他需要的binder调用本身有开销,而且作为应用层开发者也很少会去关注远方进程的内部实现,万一对方有潜在的耗时操作呢?

    通过这个例子,我们可以看出,Binder机制使用了Parcel来序列化数据,客户端在主线程调用了transact来请求(Parcel data传参),服务端在Binder线程调用onTransact来响应(Parcel reply回传结果)。

    源码分析

    Binder的调用流程大致如下,native层BpBinder的Bp指的是Binder proxy

    image

    可见,需要经过如下调用才能完成一次通信:

    1. 请求:客户端Java层->客户端native层->Binder驱动层->服务端native层->服务端Java层
    2. 响应:服务端Java层->服务端native层->Binder驱动层->客户端native层->客户端Java层

    即Binder驱动层充当着一个中转站的作用,有点像网络分层模型。

    客户端与驱动交互

    先来看客户端与驱动的交互。因为是跨进程调用(指定了:remote),示例里onServiceConnected回调回来的service对象是个BinderProxy代理实例(不跨进程的话会发生远程转本地,后面讲),我们以service.transact(1, data, reply, 0)这行调用作为入口跟进。

    BinderProxy类写在Binder类文件里面:

    //BinderProxy.java
    
    public boolean transact(int code, Parcel data, Parcel reply, int flags){
        //调用了native方法
        return transactNative(code, data, reply, flags);
    }
    

    这个native方法在android_util_Binder.cpp里注册,

    //android_util_Binder.cpp
    
    //JNI注册
    static const JNINativeMethod gBinderProxyMethods[] = {
        { "transactNative",
         "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z",
         (void*)android_os_BinderProxy_transact},
    };
    
    //native方法具体实现
    static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
            jint code, jobject dataObj, jobject replyObj, jint flags){
        //转成native层的Parcel
        Parcel* data = parcelForJavaObject(env, dataObj);
        Parcel* reply = parcelForJavaObject(env, replyObj);
        //拿到native层的句柄BpBinder
        IBinder* target = (IBinder*)
            env->GetLongField(obj, gBinderProxyOffsets.mObject);
        //调用BpBinder的transact
        status_t err = target->transact(code, *data, reply, flags);
    }
    

    继续跟BpBinder.cpp

    //BpBinder.cpp
    
    status_t BpBinder::transact(...){
        //交给线程单例处理,驱动会根据mHandle值来找到对应的binder句柄
        status_t status = IPCThreadState::self()->transact(
            mHandle, code, data, reply, flags);
    }
    

    IPCThreadState是一个线程单例,负责与binder驱动进行具体的指令通信,跟进IPCThreadState.cpp

    //IPCThreadState.cpp
    
    status_t IPCThreadState::transact(...){
        //将数据写入mOut,见1.1
        err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
    
        //...先忽略one way异步调用的代码,只看有返回值的同步调用
        //跟binder驱动交互,传入reply接收返回数据,见1.2
        err = waitForResponse(reply);
    }
    
    //1.1 将数据写入mOut
    status_t IPCThreadState::writeTransactionData(...)
    {
        binder_transaction_data tr;
        //...打包各种数据(data size、buffer、offsets)
        tr.sender_euid = 0;
        //将BC_TRANSACTION指令写入mOut
        mOut.writeInt32(cmd);
        //将打包好的binder_transaction_data写入mOut
        mOut.write(&tr, sizeof(tr));
    }
    
    //1.2 跟binder驱动交互,传入reply接收返回数据
    status_t IPCThreadState::waitForResponse(...){
        //这个循环很重要,客户端就是在这里休眠等待服务端返回结果的
        while (1) {
            //跟驱动进行数据交互,往驱动写mOut,从驱动读mIn,见1.3
            talkWithDriver();
            //读取驱动回复的指令
            cmd = (uint32_t)mIn.readInt32();
            switch (cmd) {
                case BR_TRANSACTION_COMPLETE:
                    //表示驱动已经收到客户端的transact请求
                    //如果是one way异步调用,到这就可以结束了
                    if (!reply && !acquireResult) goto finish;
                    break;
                case BR_REPLY:
                    //表示客户端收到服务端的返回结果
                    binder_transaction_data tr;
                    //把服务端的数据读出来,打包进tr
                    err = mIn.read(&tr, sizeof(tr));
                    //再把tr的数据透传进reply
                    reply->ipcSetDataReference(...);
                    //结束
                    goto finish;
            }
        }
    }
    
    //1.3 跟驱动进行数据交互,往驱动写mOut,从驱动读mIn
    status_t IPCThreadState::talkWithDriver(bool doReceive){
        binder_write_read bwr;
        //指定写数据大小和写缓冲区
        bwr.write_size = outAvail;
        bwr.write_buffer = (uintptr_t)mOut.data();
    
        //指定读数据大小和读缓冲区
        if (doReceive && needRead) {
            bwr.read_size = mIn.dataCapacity();
            bwr.read_buffer = (uintptr_t)mIn.data();
        } else {
            bwr.read_size = 0;
            bwr.read_buffer = 0;
        }
    
        //ioctl的调用进入了binder驱动层的binder_ioctl
        ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);
    
        if (bwr.write_consumed > 0) {
            //数据已经写入驱动,从mOut移除
            if (bwr.write_consumed < mOut.dataSize())
                mOut.remove(0, bwr.write_consumed);
            else
                mOut.setDataSize(0);
        }
        if (bwr.read_consumed > 0) {
            //从驱动读出数据存入mIn
            mIn.setDataSize(bwr.read_consumed);
            mIn.setDataPosition(0);
        }
    }
    

    ioctl的调用进入了binder驱动层的binder_ioctl,驱动层的代码先不跟。

    服务端与驱动交互

    从「一图摸清Android应用进程的启动」一文可知,服务端创建了一个线程注册进binder驱动,即binder线程,在ProcessState.cpp

    //ProcessState.cpp
    
    virtual bool threadLoop()
    {   //把binder线程注册进binder驱动程序的线程池中
        IPCThreadState::self()->joinThreadPool(mIsMain);
        return false;
    }
    

    跟进IPCThreadState.cpp

    //IPCThreadState.cpp
    
    void IPCThreadState::joinThreadPool(bool isMain){
        //向binder驱动写数据,表示当前线程需要注册进binder驱动
        mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
        status_t result;
        do {
            //进入死循环,等待指令的到来,见1.1
            result = getAndExecuteCommand();
        } while (result != -ECONNREFUSED && result != -EBADF);
        //向binder驱动写数据(退出循环,线程结束)
        mOut.writeInt32(BC_EXIT_LOOPER);
    }
    
    //1.1 等待指令的到来
    status_t IPCThreadState::getAndExecuteCommand(){
        //跟驱动进行数据交互,驱动会把指令写进mIn
        talkWithDriver();
        //从mIn读出指令
        cmd = mIn.readInt32();
        //执行指令,见1.2
        result = executeCommand(cmd);
        return result;
    }
    
    //1.2 执行指令
    status_t IPCThreadState::executeCommand(int32_t cmd){
        //客户端发请求到驱动,驱动转发到服务端
        switch ((uint32_t)cmd) {
            case BR_TRANSACTION:{
                //服务端收到BR_TRANSACTION指令
                binder_transaction_data tr;
                //读出客户端请求的参数
                result = mIn.read(&tr, sizeof(tr));
    
                //准备数据,向上传给Java层
                Parcel buffer; Parcel reply;
                buffer.ipcSetDataReference(...);
    
                //cookie保存的是binder实体,对应服务端的native层对象就是BBinder
                reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,
                                                                &reply, tr.flags);
                //服务端向驱动写返回值,让驱动转发给客户端
                sendReply(reply, 0);
            }
        }
    }
    
    //1.3 服务端向驱动写返回值,让驱动转发给客户端
    status_t IPCThreadState::sendReply(const Parcel& reply, uint32_t flags){
        err = writeTransactionData(BC_REPLY, flags, -1, 0, reply, &statusBuffer);
        //服务端返回结果给客户端就行,不用等待客户端,所以传NULL
        return waitForResponse(NULL, NULL);
    }
    

    然后看下BBinder的transact是怎么向上传递到Java层的,在Binder.cpp中,

    //Binder.cpp
    
    status_t BBinder::transact(uint32_t code, const Parcel& data, 
                               Parcel* reply, uint32_t flags){
        switch (code) {
                //ping指令用来判断连通性,即binder句柄是否还活着
            case PING_TRANSACTION:
                reply->writeInt32(pingBinder());
                break;
            default:
                //看这,通过JNI调用到Java层的execTransact,见1.1
                err = onTransact(code, data, reply, flags);
                break;
        }
        return err;
    }
    
    //android_util_Binder.cpp
    
    //1.1 通过JNI调用到Java层的execTransact
    virtual status_t onTransact(...){
        JNIEnv* env = javavm_to_jnienv(mVM);
        jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact, ...);
    }
    

    回到Java层,execTransact如下:

    //android.os.Binder.java
    
    private boolean execTransact(...) {
        res = onTransact(code, data, reply, flags);
    }
    

    至此就回调到了示例代码中服务端MyBinder的onTransact了,我们在示例中处理请求参数data和返回值reply,最后由native层的sendReply(reply, 0)真正向驱动写返回值,让驱动转发给客户端。

    将调用代码和流程图结合起来:

    image

    然后是指令交互图(非one way模式):

    image

    binder同步调用等到服务端的BR_REPLY指令后就真正结束,服务端则继续循环,等待下一次请求。

    总结

    本文主要介绍了Binder的背景和调用流程,将留下3个疑问继续探讨。

    1. binder句柄是怎么传输和管理的(binder驱动和ServiceManager进程)
    2. binder句柄的远程转本地
    3. one way异步模式和他的串行调用(async_todo)、同步模式的并行调用

    系列文章:

    细节补充

    Binder为什么高效

    Linux用户空间是无法直接读写磁盘的,系统所有的资源管理(读写磁盘文件、分配回收内存、从网络接口读写数据)都是在内核空间完成的,用户空间需要通过系统调用让内核空间完成这些功能。

    传统IPC传输数据:发送进程需要copy_from_user从用户到内核,接收进程再copy_to_uer从内核到用户,两次拷贝。

    而Binder传输数据:用mmap将binder内核空间的虚拟内存和用户空间的虚拟内存映射到同一块物理内存copy_from_user将数据从发送进程的用户空间拷贝到接收进程的内核空间(一次拷贝),接收进程通过映射关系能直接在用户空间读取内核空间的数据

    image

    (图片来源:「写给Android应用工程师的Binder原理剖析」)

    Binder为什么不用shm

    shm通常需要结合其他跨进程方式如信号量来同步信息,使用没有mmap方便。

    提问

    • 上期提问: SurfaceFlinger进程为什么不是通过Zygote进程的fork创建,而是由init进程创建?

    参考资料


    更多性感文章,关注原创技术公众号:哈利迪ei

    相关文章

      网友评论

          本文标题:图解 | 不得错过的Binder浅析(一)

          本文链接:https://www.haomeiwen.com/subject/zmcmvktx.html