0
点赞
收藏
分享

微信扫一扫

【Qt之QString】数值与进制字符串间的转换详解

老牛走世界 2023-10-15 阅读 53

文章目录

前言

对Binder跨进程通信的原理,予以记录!

Binder 是一种进程间通信机制,基于开源的 OpenBinder 实现;OpenBinder 起初由 Be Inc. 开发,后由 Plam Inc. 接手。

一、Linux传统跨进程通信原理

Linux传统跨进程通信原理

二、Android Binder跨进程通信原理

Android 系统是基于 Linux 内核的,Linux 已经提供了管道、消息队列、共享内存和 Socket 等 IPC 机制。那为什么 Android 还要提供 Binder 来实现 IPC 呢?主要是基于性能、稳定性和安全性几方面的原因。如下图:
在这里插入图片描述
理解了 Linux IPC 相关概念和通信原理,接下来我们正式介绍下 Binder IPC 的原理

1、动态内核可加载模块

传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。

2、内存映射

Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。

3、Binder IPC 实现原理

Binder IPC 正是基于内存映射(mmap)来实现的,但是 mmap() 通常是用在有物理介质的文件系统上的。

比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘–>内核空间–>用户空间);通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率。

一次完整的 Binder IPC 通信过程通常是这样:

  • 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
  • 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
  • 发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。如下图:
    在这里插入图片描述
    在这里插入图片描述
    说明1:Client进程、Server进程 & Service Manager 进程之间的交互都必须通过Binder驱动(使用 open 和 ioctl文件操作函数),而非直接交互

三、Android Binder IPC 通信模型

介绍完 Binder IPC 的底层通信原理,接下来我们看看实现层面是如何设计的。

一次完整的进程间通信必然至少包含两个进程,通常我们称通信的双方分别为客户端进程(Client)和服务端进程(Server),由于进程隔离机制的存在,通信双方必然需要借助 Binder 来实现。

1、Client/Server/ServiceManager/驱动

Binder 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。

  • Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。

  • Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。

  • Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。
    在这里插入图片描述

说明2:Binder请求的线程管理

Server进程会创建很多线程来处理Binder请求,Binder模型的线程管理采用Binder驱动的线程池,并由Binder驱动自身进行管理而不是由Server进程来管理的

一个进程的Binder线程数默认最大是16,超过的请求会被阻塞等待空闲的Binder线程。所以,在进程间通信时处理并发问题时,如使用ContentProvider时,它的CRUD(创建、检索、更新和删除)方法只能同时有16个线程同时工作

在这里插入图片描述

说明3: Binder驱动 & Service Manager进程 属于 Android基础架构(即系统已经实现好了);而Client 进程 和 Server 进程 属于Android应用层(需要开发者自己实现)

在这里插入图片描述

Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之前的关系。

Binder与路由器之间的角色关系

在这里插入图片描述

2、Binder通信过程

至此,我们大致能总结出 Binder 通信过程:

  • 首先,一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
  • Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
  • Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。

我们看到整个通信过程都需要 Binder 驱动的接入。下图能更加直观的展现整个通信过程(为了进一步抽象通信过程以及呈现上的方便,下图我们忽略了 Binder 实体及其引用的概念):
在这里插入图片描述

3、Binder通信中的代理模式

我们已经解释清楚 Client、Server 借助 Binder 驱动完成跨进程通信的实现机制了,但是还有个问题会让我们困惑。A 进程想要 B 进程中某个对象(object)是如何实现的呢?毕竟它们分属不同的进程,A 进程 没法直接使用 B 进程中的 object。

前面我们介绍过跨进程通信的过程都有 Binder 驱动的参与,因此在数据流经 Binder 驱动的时候驱动会对数据做一层转换。

  • 当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把请求参数交给驱动即可。对于 A 进程来说和直接调用 object 中的方法是一样的。
  • 当 Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了。
    在这里插入图片描述

4、Binder 的完整定义

现在我们可以对 Binder 做个更加全面的定义了:

  • 从进程间通信的角度看,Binder 是一种进程间通信的机制;
  • 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
  • 从 Client 进程的角度看,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理
  • 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象做一点点特殊处理,自动完成代理对象和本地对象之间的转换。

四、Binder机制在Android中的具体实现——实现两个数相加

1、定义Client进程需要调用的接口方法

public interface IPlus extends IInterface {
    //定义需要实现的接口方法,即Client进程需要调用的方法
    public int add(int a, int b);
}

2、建立IPCService

public class IPCService extends Service {

    public static final String DESCRIPTOR = "add two int";

    private final MyAddBinder mBinder = new MyAddBinder();

    public IPCService() {
        /**
         * 将(descriptor, plus)作为(key, value)对存入到Binder对象中的一个Map<String, IInterface>中
         * 之后binder对象可根据descriptor找到对应IInterface对象的引用,进而调用其方法
         *
         * @param plus
         * @param descriptor
         */
        IPlus plus = new IPlus() {
            @Override
            public int add(int a, int b) {
                return a + b;
            }

            @Override
            public IBinder asBinder() {
                return null;
            }
        };

        /**
         * 1.将(add two int,plus)作为(key,value)对存入到Binder对象中的一个Map<String,IInterface>对象中
         * 2.之后,Binder对象可根据add two int通过queryLocalIInterface()获得对应IInterface对象
         */
        mBinder.attachInterface(plus, DESCRIPTOR);
    }

    private class MyAddBinder extends Binder {
        /**
         * 继承自IBinder接口的,执行Client进程所请求的目标的方法(子类需要复写该方法)
         * 注:运行在Server进程的Binder线程池中;当Client进程发起远程请求时,远程请求会要求系统底层执行回调该方法
         * @param code Client进程请求方法标识符。即Server进程根据该标识确定所请求的目标方法
         * @param data 目标方法的参数。(Client进程传进来的,此处就是整数a和b)
         * @param reply 目标方法执行后的结果(返回给Client进程)
         * @param flags
         * @return
         */
        @Override
        protected boolean onTransact(int code, @NonNull Parcel data, Parcel reply, int flags)
                throws RemoteException {
            if (code == 1) {
                Log.d("TAG", "MyBinder Switch块 -----" + Process.myPid());
                data.enforceInterface(DESCRIPTOR);
                int a = data.readInt();
                int b = data.readInt();
                int result = ((IPlus) this.queryLocalInterface(DESCRIPTOR)).add(a, b);
                reply.writeNoException();
                reply.writeInt(result);
                return true;
            }
            Log.d("TAG", "MyBinder   OnTransact() ----- " + android.os.Process.myPid());
            return super.onTransact(code, data, reply, flags);
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

3、MainActivity中bindService,最后将结果显示在TextView中

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    public static final String DESCRIPTOR1 = "add two int";

    private EditText editText1;
    private EditText editText2;
    private TextView resultText;
    private Button addBtn;
    private Button subtractBtn;

    private IBinder mBinder;
    private final ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mBinder = service;
            Log.d("TAG", "客户端-----" + android.os.Process.myPid());
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mBinder = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        bindViews();
        addBtn.setOnClickListener(this);
        subtractBtn.setOnClickListener(this);
        //service方式
        Intent service = new Intent(this, IPCService.class);
        bindService(service, mServiceConnection, BIND_AUTO_CREATE);
    }

    private void bindViews() {
        editText1 = (EditText) findViewById(R.id.edit_arg1);
        editText2 = (EditText) findViewById(R.id.edit_arg2);
        resultText = (TextView) findViewById(R.id.result_arg);
        addBtn = (Button) findViewById(R.id.btn_add);
        subtractBtn = (Button) findViewById(R.id.btn_subtract);
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.btn_add) {
            add();
        }
    }

    public void add() {
        int a = Integer.parseInt(editText1.getText().toString());
        int b = Integer.parseInt(editText2.getText().toString());

        if (mBinder != null) {
            Parcel _data = Parcel.obtain();
            Parcel _reply = Parcel.obtain();
            int _result = -1;
            try {
                _data.writeInterfaceToken(DESCRIPTOR1);
                _data.writeInt(a);
                _data.writeInt(b);
                mBinder.transact(1, _data, _reply, 0);
                _reply.readException();
                _result = _reply.readInt();
                resultText.setText(_result + "");
                Toast.makeText(MainActivity.this, "result:" + _result,
                        Toast.LENGTH_SHORT).show();
            } catch (RemoteException e) {
                throw new RuntimeException(e);
            } finally {
                _reply.recycle();
                _data.recycle();
            }
        } else {
            Toast.makeText(MainActivity.this, "未连接服务端或服务端异常!",
                    Toast.LENGTH_SHORT).show();
        }
    }
}

在这里插入图片描述

五、Binder高频面试题

1、Binder为何能实现一次copy?

Binder的一次copy是利用了mmap(内存映射文件:目的是开辟物理地址),内存映射文件是在堆和栈的空余空间
mmap是在linux中的api,可以通过mmap去开辟物理地址空间。
MMU(Memeory Mananger Unit)将mmap开辟的物理内存地址转化成虚拟内存地址

Binder采用的是C/S模式的,其中提供服务的进程成为Server进程,访问服务的进程成为Client进程;Server和Client进程通信要依靠运行在内核空间的Binder驱动程序来进行;

Service组件在开启时,会将自己注册到一个Service Manager里,以便于Client进程在ServiceManager中找到它;因此ServiceManager也称为Binder进程间通信的上下文管理者;同时它也需要和普通的Server进程和Client进程通信,所以也是可以看做一个特殊的Service组件;

为什么会出现物理地址和虚拟地址呢?

由于现在的程序app大小都很大了,如果全部加载到内存中去运行需要很多内存,而手机的内存是有限的,又由于当你在运行一个app的时候不是所有的代码都会被加载到内存中去运行,只会加载一部分正在活动的代码,为了满足程序局部性原则,这时出现的物理地址和虚拟地址刚好能解决,能省下不用的内存空间供其他app使用

MMU(Memory Management Unit)内存管理单元:涉及到一个转换物理地址和虚拟地址;
为什么会存在MMU,因为app的整体大小假如是100M,但是实际的活跃代码在内存中只有1M,其他的代码处于磁盘中,所以,cpu在运行代码的时候不可能说只给1M的内存让cpu在里面运行,所以需要给一个MMU中间件,让CPU感觉运行在512M的内存中。

MMU里有页表:页表里保存有效位+地址,有效位为0表示未缓存,为1表示已缓存,只要有效位是1就肯定有地址;

虚拟地址和物理地址,如果虚拟地址中没有的话,MMU就会读取磁盘并拿出来在内存中开辟一块新的空间,并在MMU中存放物理地址和对应的虚拟地址,cpu就会拿到虚拟地址;

2、两个进程间的通信Binder原理

Binder的通信是进程A调用copy_form_user,到内核空间,内核空间同进程B建立了链接,copy_to_user会将数据传给B进程;而进程间的通信大小是1M-8K(copy到内核空间),8K是由于有请求头等信息,一页是4K,需要是4的整数倍;

假如页的大小为P,那么在虚拟内存中VP就称为虚拟页;从虚拟内存中拿到的一页的代码放在物理内存中,那么物理内存中也得有一个同样大小可以页存放虚拟页的代码,物理内存中的页称为物理页(PP);

在任何时刻,虚拟页都有以下三种状态中的一种,且以下状态都是在MMU中体现:

未分配的:VM还未分配页(或者未创建),未分配的页还没有任何数据与代码与他们关联,因此也就不占任何磁盘;

已缓存的:当前缓存在物理内存中已分配页;

未缓存的:当前未缓存在屋里内存中已分配页;
举报

相关推荐

0 条评论