Android IPC系列(三):Binder概述

933 阅读13分钟

背景知识

为了更好的理解binder,我们要先澄清一下概念,因为Android 基于Linux内核,我们有必要了解相关知识。

进程隔离

进程隔离是为了保护操作系统进程之间互不干扰而设计的,这个技术是为了避免进程A写入进程B准备的,进程隔离的实现,使用了虚拟地址空间,进程A的虚拟地址和进程B的虚拟地址不同,这样就防止进程A的数据写入进程B,操作系统之间不同进程之间,数据不共享,对于每一个进程来说,都天真的以为自己独享了整个系统,不知道其他进程的存在,因此一个进程与其他进程进行通信,需要一种特殊的方式才可以

用户空间/内核空间

Linux Kernel 是操作系统的核心,独立于普通应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。

对于Kernel这么一个高安全级别的东西,显然是不允许其他应用程序随便调用或访问的,所以需要对Kernel提供一定的保护机制,这个保护机制用来告诉那些应用程序,你只可以访问某些许可的资源,不可以访问不许可的资源,于是就把Kernel和上层应用程序抽象的隔离开,分别称之为 Kernel Space 和 User Space ​​​​​​​​​​

在这里插入图片描述

系统调用/内核态/用户态

虽然在逻辑上抽离了用户空间、内核空间,但不可避免的是,仍有用户空间需要访问内核空间的资源,比如程序访问文件。该怎么办?

用户空间访问内核空间的唯一方式就是系统调用 ,通过这一统一入口,所有资源访问都是在内核控制下进行,以免导致用户程序对系统资源的越权访问,从而保障了系统的安全,稳定

当一个任务进程进行系统代码调用而陷入内核代码执行时,我们就称进程处于内核运行状态,简称内核态,此时处理器处于特权级别最高的(0)状态,内核代码中执行,当进程在执行用户自己的代码时,称其处于用户状态,简称用户态,此时处理器在特权最低(3)状态,处理器正在特权高的时候才能执行那些特权cpu指令

内核模块/驱动

通过系统调用可使用户空间访问内核空间,如果一个用户空间访问另一个用户空间该怎么办?我们首先想到的是通过内核添加支持,因为内核是共享的,传统的Linux机制比如Socket ,管道,都可以支持的,但是Binder并不是Linux内核的一部分,他是怎么做到访问内核空间的?Linux的动态可加载内核模块机制解决了这个问题,模块是具有独立功能的程序,他可以被单独编译,但不能独立运行,它在运行时被链接到内核作为内核的一部分在内核空间运行,这样,Android 系统通过添加一个内核模块运行在内核空间,用户进程之间通过这个模块作为桥梁,就可以完成通信。

在Android 系统中,这个运行在内核空间的,负责各个用户进程通过Binder通信的内核模块叫做Binder驱动

驱动程序一般指的是设备驱动程序,是一种可以使计算机和设备通信的特殊程序,相当于硬件的接口,操作系统只有通过这个接口才能操作硬件设备

驱动就是操作硬件的接口,为了支持Binder通信过程,binder使用了一种硬件,因此这个模块称之为驱动。

为什么使用Binder

Android 使用的Linux内核有很多种的跨进程通信的方式,比如:管道,socket那么为什么还要单独搞出一个Binder出来呢?主要有俩点,效率和安全

  • 效率

socket作为通用接口,其传输效率低,开销大,主要用作跨网络进程通信,本机低速通信,管道采用储存_转发方式,即发送发拷贝数据到内核的缓存区,内核缓存区拷贝到接收方的缓存区,俩次拷贝 Binder所有总体效果就相当于对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,只进行了一次拷贝提升了一倍的性能。 顺便再提一点,Linux内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,利用mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’。

IPC 数据拷贝次数
共享内存 0
Binder 1
Socket/管道/消息队列 2
  • 安全

在移动设备上,广泛的使用跨进程通信肯定对通信体制本身提出了严格的要求;Binder相对于传统的Socket方式,更加高效,另外,传统 的进程通信方式对通信双方身份并没有做出严格的验证,只要在上层协议上进行架设,,比如Socket通信的ip地址是客户端手动输入的,可以进行伪造 Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。

基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。

Binder通信模型

Android 系统 Binder机制中的四个组件,Client,Server,ServerManager,和Binder驱动关系如下

在这里插入图片描述

  • Client ,Server,ServerManager,实现用户空间,Binder驱动程序实现在内核空间中

  • Binder驱动和ServerManager 在Android 平台已经实现,开发者只需要在用户空间实现自己的Client和Server

  • Binder驱动程序提供设备文件/dev/binder与用户空间交互,Client ,Server,ServerManager 通过open和ioctl文件操作函数与binder驱动程序进行通信。

  • Client和Server 进程通信通过binder驱动程序间接实现

  • ServerManager 是一个守护进程,用来管理Server,并像Client提供查询Server能力

  • ServerManager 建立,首先要有一个进程向驱动提出成为SM。驱动同意后,SM负责管理Server,不过现在是空的

  • 各个Server向SM注册,每个Server端进程启动后,向SM报告,比如我是zhangsan,要找我请返回0X1234,其他Server亦是如此,这样SM就建立一张表,对应着Server的名字和地址

  • Client 要与Server通信,首先询问SM ,请告诉我如何联系zhangsan,SM收到后返回一地址0X1234,Client收到后就可以和Server通信

  • ServerManager提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成SMgr时Binder驱动会自动为它创建Binder实体。其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。也就是说,一个Server若要向SMgr注册自己Binder就必需通过0这个引用号和SMgr的Binder通信

那么Binder驱动在干什么呢,Client和SM通信,Client和Server通信,Server和SM通信都是通过Binder驱动,驱动默默无闻,但做着最重要的工作,驱动是整个通信的核心,因此完成跨进程通信的秘密全部隐藏在驱动里面,这个稍后讨论

Binder机制跨进程原理

上文给出了Binder的通信模型,指出了通信过程的四个角色: Client, Server, SM, driver; 但是我们仍然不清楚Client到底是如何与Server完成通信的。

两个运行在用户空间的进程A和进程B如何完成通信呢?内核可以访问A和B的所有数据;所以,最简单的方式是通过内核做中转;假设进程A要给进程B发送数据,那么就先把A的数据copy到内核空间,然后把内核空间对应的数据copy到B就完成了;用户空间要操作内核空间,需要通过系统调用;刚好,这里就有两个系统调用:copy_from_user, copy_to_user。

但是,Binder机制并不是这么干的。讲这么一段,是说明进程间通信并不是什么神秘的东西。那么,Binder机制是如何实现跨进程通信的呢?

Binder驱动为我们做了一切。

假设Client进程想要调用Server进程的object对象的一个方法add;对于这个跨进程通信过程,我们来看看Binder机制是如何做的。 (通信是一个广泛的概念,只要一个进程能调用另外一个进程里面某对象的方法,那么具体要完成什么通信内容就很容易了。)

在这里插入图片描述

首先,Server进程要向SM注册;告诉自己是谁,自己有什么能力;在这个场景就是Server告诉SM,它叫zhangsan,它有一个object对象,可以执行add 操作;于是SM建立了一张表:zhangsan这个名字对应进程Server;

然后Client向SM查询:我需要联系一个名字叫做zhangsan的进程里面的object对象;这时候关键来了:进程之间通信的数据都会经过运行在内核空间里面的驱动,驱动在数据流过的时候做了一点手脚,它并不会给Client进程返回一个真正的object对象,而是返回一个看起来跟object一模一样的代理对象objectProxy,这个objectProxy也有一个add方法,但是这个add方法没有Server进程里面object对象的add方法那个能力;objectProxy的add只是一个傀儡,它唯一做的事情就是把参数包装然后交给驱动。(这里我们简化了SM的流程,见下文)

但是Client进程并不知道驱动返回给它的对象动过手脚,毕竟伪装的太像了,如假包换。Client开开心心地拿着objectProxy对象然后调用add方法;我们说过,这个add什么也不做,直接把参数做一些包装然后直接转发给Binder驱动。

驱动收到这个消息,发现是这个objectProxy;一查表就明白了:我之前用objectProxy替换了object发送给Client了,它真正应该要访问的是object对象的add方法;于是Binder驱动通知Server进程,调用你的object对象的add方法,然后把结果发给我,Sever进程收到这个消息,照做之后将结果返回驱动,驱动然后把结果返回给Client进程;于是整个过程就完成了。

由于驱动返回的objectProxy与Server进程里面原始的object是如此相似,给人感觉好像是直接把Server进程里面的对象object传递到了Client进程;因此,我们可以说Binder对象是可以进行跨进程传递的对象

但事实上我们知道,Binder跨进程传输并不是真的把一个对象传输到了另外一个进程;传输过程好像是Binder跨进程穿越的时候,它在一个进程留下了一个真身,在另外一个进程幻化出一个影子(这个影子可以很多个);Client进程的操作其实是对于影子的操作,影子利用Binder驱动最终让真身完成操作。

理解这一点非常重要;务必仔细体会。另外,Android系统实现这种机制使用的是代理模式, 对于Binder的访问,如果是在同一个进程(不需要跨进程),那么直接返回原始的Binder实体;如果在不同进程,那么就给他一个代理对象(影子);我们在系统源码以及AIDL的生成代码里面可以看到很多这种实现。

另外我们为了简化整个流程,隐藏了SM这一部分驱动进行的操作;实际上,由于SM与Server通常不在一个进程,Server进程向SM注册的过程也是跨进程通信,驱动也会对这个过程进行暗箱操作:SM中存在的Server端的对象实际上也是代理对象,后面Client向SM查询的时候,驱动会给Client返回另外一个代理对象。Sever进程的本地对象仅有一个,其他进程所拥有的全部都是它的代理。

一句话总结就是:Client进程只不过是持有了Server端的代理;代理对象协助驱动完成了跨进程通信。

Binder到底是什么?

Binder对于使用者来言,Server端的Binder和Client端的Binder 没有任何区别,一个Binder对象就代表了所有,不用关心实现的细节,甚至不用关心Binder驱动和SM的存在,这就是抽象

1 通常意义上讲,Binder是指的一种通信机制,我们说的aidl使用Binder进行通信,指的就是这种Binder的IPC机制

2 对于Server进程来说,Binder指的是Binder本地对象

3 对于Client进程来说,Binder指的是Binder代理对象,他只是Binder本地对象的一个远程代理,对这个Binder代理对象进行操作,会通过驱动最终发送到Binder本地对象去完成,对一个拥有Binder对象使用者来说,它无需关心Binder是代理还是本地,对于代理对象和本地对象操作并没有区别。

4 对于传输过程来言,Binder是可以进行跨进程传递的对象,Binder驱动会对具有跨进程传递的能力的对象进行处理,自动完成代理对象和本地对象的转换。

驱动里的Binder 我们现在知道,Server里的Binder指的是Binder本地对象,Client里的Binder指的是Binder代理对象,Binder在进行跨进程传递的时候,Binder驱动自动完成这俩种类型的转换,因此,Binder驱动必然保存了每一个,跨进程Binder对象的信息,在驱动中,Binder本地对象的代表是一个叫做Binder_node的数据结构,Binder代理对象是一个binder_ref的数据结构,有的地方吧binder本地对象成为Binder实体,把Binder代理对象称作Binder引用(句柄),其实指的是Binder对象在驱动中的表现形式,理解就好

参考:

blog.csdn.net/luoshengyan…

blog.csdn.net/universus/a…

weishu.me/2016/01/12/…