引言
在写这篇文章之前,参考了很多资料,但是依旧不敢下笔(或者说是不知道从何下笔)。怕自己理解有误差,对大家造成不好的影响,贻笑大方。又担心自己理解的不够透彻,无法清晰准确的表达出Binder的设计思想。直到现在还是战战兢兢,只能说是给出一点自己对binder机制的浅显的理解,以此来抛砖引玉。
1. 为什么研究Binder?
从了解四大组件底层的通信机制,到理解系统的各种Service,到AIDL的实现原理这些的背后深入了解下去,都有着Binder的影子。所以了解Binder机制对理解android系统有着重要的作用。
但是Binder机制细节过于复杂,所以这里主要是从宏观的层面去试着理解Binder中的各种概念和基本的通信过程,侧重于Java层的实现,驱动层不做过多的介绍。
2.概念解释
Android系统是基于Linux内核,先来了解一下一些基础的概念。
进程隔离
"进程隔离是为保护操作系统中进程互不干扰而设计的一组不同硬件和软件的技术。这个技术是为了避免进程A写入进程B的情况发生。 进程的隔离实现,使用了虚拟地址空间。进程A的虚拟地址和进程B的虚拟地址不同,这样就防止进程A将数据信息写入进程B。"这段引用自维基百科,正是由于进程隔离的存在,所以需要一种机制才能完成一个进程与另外一个进程的通信。
用户空间/内核空间
用户空间访问内核空间的唯一方式就是系统调用;通过这个统一入口接口,所有的资源访问都是在内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。用户软件良莠不齐,要是它们乱搞把系统玩坏了怎么办?因此对于某些特权操作必须交给安全可靠的内核来执行。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)此时处理器处于特权级最高的(0级)内核代码中执行。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。处理器在特权等级高的时候才能执行那些特权CPU指令。
内核模块/驱动
传统的Linux通信机制,比如Socket,管道等都有内核支持,Binder不属于Linux内核的一部分,它是通过Linux的动态可加载内核模块(Loadable Kernel Module)机制来实现的。模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行。
驱动就是操作硬件的接口,Binder驱动就是这样一个运行在内核空间的,负责各个用户进程通信的内核模块。
内存映射
"即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。"
内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
3.为什么是Binder?
传统的Linux通信方式有Socket、共享内存、管道、消息队列、信号量等。Android使用的Binder机制不属于Linux。Android不继承Linux中原有的IPC方式,而选择使用Binder,说明Binder具有一定的优势。
1.从传输性能上说,Socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信;消息队列和管道采用存储-转发方式,即数据先从发送方拷贝到内存开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程;共享内存虽然无需拷贝,但控制复杂,难以使用;而Binder只需要拷贝一次;
2.从安全性上说,Linux传统的IPC没有任何安全措施,完全依赖上层协议来确保,具体有以下两点表现:第一,传统IPC的接收方无法获得对方可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份,使用传统IPC时只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用;第二,传统IPC的访问接入点是开放的,无法建立私有通信,只要知道这些接入点的程序都可以和对端建立连接,这样无法阻止恶意程序通过猜测接收方的地址获得连接。
4.Binder IPC 实现原理
Binder IPC 正是基于内存映射(mmap)来实现的,但是 mmap() 通常是用在有物理介质的文件系统上的。比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘-->内核空间-->用户空间);通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率。
而 Binder 并不存在物理介质,因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间。
一次完整的 Binder IPC 通信过程通常是这样:
1.首先 Binder 驱动在内核空间创建一个数据接收缓存区;
2.接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
3.发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信(其实与 copy_from_user()相对应的还有一个copy_to_user()函数用于从内核区中读取数据到用户区)。
5.Binder的架构
从上图可以明显的看出Binder 通信采用 C/S 架构,从组件的视角来看,包括Client、Server、 Service Manager、Binder驱动。Client、Server、Service Manager运行在用户空间,Binder驱动运行在内核空间。Service Manager和Binder驱动已经由系统提供,我们只需要实现Client、Server端。
6.Binder的通信模型
从前文中得知Binder的通信离不开Client、Server、Service Manager、Binder驱动这四部分。为了解释他们在通信过程中扮演的角色,网上有人形象的做了类比。如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)、路由器(Binder驱动)之间的关系。
我们访问一个网页的时候,在浏览器输入www.baidu.com,然后通过路由的转发功能在DNS服务器中把www.baidu.com置换为具体的IP地址115.239.211.112,然后通过这个IP地址访问到百度对应的服务器。
Android Binder 设计与实现一文中对Client、Server、Service Manager、Binder驱动之间的关系作了很详细的描述。以下摘录部分内容来补充对通信过程的解释。
简单点总结就是:
1.一个进程使用BINDER_SET_CONTEXT_MGR命令通过Binder驱动将自己注册成ServiceManager。
2.Server 向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
3.Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
7.Binder 通信中的代理模式:
进程之间通信的数据都会经过在内核空间的驱动,驱动在数据经过的时候做了一些处理,它并不会真正的给Client进程返回一个object对象,而且返回一个看起来跟object一模一样的的代理objectProxy,这个objectProxy也有对象内的方法,比如方法a(),但是a()方法只是一个傀儡,不具备Server进程里面object对象的a方法的实现,唯一的作用就是包装参数然后交给驱动层(这里简化了ServiceManager的流程)。
但是这些对Client进程来说是未知的,Client仍然直接调用objectProxy对象然后调用a()方法,但正如上边所说的,a()方法直接把参数包装后转发给Binder驱动层处理。
驱动层收到这个消息,验证是objectProxy,然后就查询自己维护的表单,发现之前是用objectProxy替换了object发送给Client,它真正要访问的是object对象的add方法;于是Binder驱动通知Server进程,调用你的object对象的add方法,然后把结果返回给我,Server进程收到这个消息,照做之后将结果返回给驱动,驱动然后把结果发给Client进程,整个过程就完成了。
由此可以看出来,Binder跨进程通信并不是真的把一个对象传输到了另外一个进程,它在进程里有一个真身,在另外一个进程copy出一个影子(这个影子可以有多个);Client进程通过对影子的操作,然后影子利用Binder驱动让真身完成操作。
对于Binder的访问,如果是在同一个进程那么直接返回原始的Binder实体,如果在不同的进程,那么就返回一个代理对象。
8.Java层的使用
queryLocalInterface()方法是查找Binder本地对象,如果找到了就说明Client和Server在同一个进程,那么这个 binder 本身就是 Binder 本地对象,可以直接使用。否则说明是 binder 是个远程对象,也就是 BinderProxy。需要通过代理对象来实现远程访问。
IBinder是远程对象调用的基本接口,是Binder机制的核心部分。但是它不仅用于远程调用,也用于进程内的调用。这个接口定义与远程对象交互的协议(不要直接实现这个接口)。IBinder的最主要的一个API就是transact(),与之对应的是另一个方法Binder.onTransact()。当通过transact()发起调用请求,是onTransact()响应请求。IBinder的API的执行都是同步的,比如transact()方法执行后,会一直等待onTransact()方法调用完成后才返回。不管是在同一个进程还是不同的进程内都是这样。
系统为每个进程维护了一个存放交互线程的线程池。专门处理从另外一个进程发送来的IPC调用。比如,当进程A发起调用到进程B,A中发出调用的线程就阻塞在transact()中了。进程B中的线程池拿一个线程接收这个调用,调用Binder.onTransact(),处理完后用Parcel包裹下数据返回过去。然后进程A中等待的那个线程在收到返回的Parcel数据后继续执行。表面上看起来就像在当前进程里的一个线程一样,但其实不是当前进程创建的。
而且,Binder机制支持进程间的递归调用。例如,进程A执行IBinder的transact()方法调用进程B的binder,而进程B在Binder.onTransact()中又用transact()向进程A发起调用,这个时候进程A在等待它之前自己发出的调用返回的同时,还会用自己的Binder.onTransact()来响应进程B的transact()。
备注:flag==>0 ,当为0的时候表示是正常的一次IPC调用模式,否则就是单向的,不返回数据。
从组建的视角总结一下:
Server:一个Binder服务端实际上就是提供一个Binder类的对象,对象创建后,通过线程池的来等待接收Binder驱动发送来的消息,收到消息后执行到Binder对象中的onTransact()函数,按照不同的参数执行不同的代码。
Binder驱动:任意一个服务端的Binder对象被创建时,其实也会在Binder驱动中创建一个mRemote对象,也是一个Binder类。客户端访问服务端其实都是通过mRemote(其实省略掉了Service Manager,transact()调用底层native层,native层到驱动,驱动并不会直接转发响应请求,而是驱动程序通过唤醒Service Manager来响应)。
Client:客户端要访问服务端,必须获取服务端在Binder对象中对应的mRemote引用。拿到mRemote后,就可以调用其transact()方法,其实在Binder驱动中,mRemote对象也重载了transact()方法。但是给人的感觉是似乎是直接调用远程服务端对应的Binder,实际上驱动做了一层中转。