虽然Android继承使用Linux的内核,但Linux与Android的通信机制不同。
在Linux中使用的IPC通信机制如下:
1.管道(Pipe):点对点通信,因为采用存储转发方式,需要拷贝2次数据,效率低下
2.Socket:是一个通用的接口,通常用于网络请求,但是建立连接和断开连接开销太大,效率低下
3.共享内存:虽然在传输时没有拷贝数据,但其控制机制复杂(比如跨进程通信时,需获取对方进程的pid,得多种机制协同操作)。因为内存共享,数据很容易被一方篡改,所以安全性不高
android的binder通信:
性能方面
在移动设备上(性能受限制的设备,比如要省电),广泛地使用跨进程通信对通信机制的性能有严格的要求,Binder相对出传统的Socket方式,更加高效。Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,共享内存方式一次内存拷贝都不需要,但实现方式又比较复杂。
安全方面
首先传统IPC的接收方无法获得对方进程可靠的UID和PID(用户ID进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。其实Binder机制主要是在本机内进行跨进程通信,而socket等IPC方式主要是用于跨网络的通信手段,因而socket不能限制了入口和出口,所以从使用场景来看,socket给用户提供的使用范围更广,而binder由于为了保证它的数据安全性,必须限制于android系统的机子中,而且在内核中就已经对UID进行了验证。
我们知道以前程序直接访问和操作的都是物理内存;这样做有很大弊端,首先不能进行多进程运行,试想一下:你必须执行完一条指令后才能接着执行下一条,如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。这时候就抽象一个概念叫虚拟内存:每个进程都有自己独立的逻辑地址空间,然后这个空间以“页”为单位,将连续的地址空间分开,对于进程来看,逻辑上貌似有很多内存空间,其中一部分对应物理内存上的一块(称为页框,通常页和页框大小相等),还有一些没加载在内存中的对应在硬盘上。那么虚拟内存和物理内存如何实现映射呢?是通过一个叫MMU(内存管理单元),MMU里面主要负责两件事:1.记录该页是否在物理内存中运行 2.记录该页空间在物理内存哪里运行 。如果虚拟内存的页并不存在于物理内存中,会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。
这样通过引入虚拟空间的概念,就能让物理内存充分的利用,同时利用各个进程逻辑地址分页运行的特性,实现多进程运行也不是难事
我们上面已经知道了虚拟内存的由来;每个进程可以拥有4GB的虚拟内存。Linux内核将这4GB的空间分为两个部分。最高的1GB(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间“。而较低的3GB(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间”,因为每个进程可以通过系统调用进入内核,因此,Linux内核空间由系统内的所有进程共享。所以,每个进程可以拥有4GB的虚拟地址空间(也叫虚拟内存)。每个进程有各自的私有用户空间(0~3GB),这个空间对系统中的其他进程是不可见的。最高的1GB内核空间则为所有进程以及内核所共享(虽然共享,但是一个完整的虚拟地址空间就是4GB,内核空间是每个进程都要被分配的,只是他们对应相同的地址空间)。
为了保护进程空间不被别的进程破坏或者干扰,Linux的进程是相互独立的(进程隔离),而且一个进程空间还分为用户空间和内核(Kernel)空间,相当于把Kernel和上层的应用程序抽像的隔离开。这里有两个隔离,一个进程间是相互隔离的,二是进程内有用户和内核的隔离。
Binder框架定义了四个角色:Server,Client,ServiceManager以及Binder驱动。其中Server,Client,ServiceManager运行于用户空间,驱动运行于内核空间。这四个角色的关系类似:Server是服务器,Client是客户终端,ServiceManager是服务注册中心(类似房屋中介)。
要进行Client-Server之间的通信,从面向对象的角度,在Server内部有一个Binder实体,在Client内部有一个Binder对象的引用,其实就是Binder的一个代理,Client通过对Binder引用间接的操作Server内部的Binder实体,这样就实现了通信。
但是现在的问题是会有很多个提供不同服务的Server(比如有媒体播放服务,音视频捕获服务等),而且会有很多Client(比如多个应用都要调用媒体播放服务),那么我们怎么才能够实现正确的Client调用正确的Server呢?就好比房客怎么才能够租到自己想要租的房子(联系上房东),这个时候中介就起到重要作用,房东想要出租自己的房子就必须要到中介注册,房客想要租房子就要去中介那里找,同样的道理,这里Client就是房客,Server就是房东,ServiceManager就是房屋中介,每个Server如果要提供服务就必须要去ServiceManager那里去注册,ServiceManager在一张查找表中记录一个Server的名字,对应着Server的引用。Client想要获得Server,必须通过名字到ServiceManager取找Server的引用,获得这个Server的binder引用,通过这个binder引用去和Server通信。
Binder 驱动:
Binder 驱动就如同路由器一样,是整个通信的核心;驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。
可以说是Client,Server,ServerManager之间沟通的桥梁
ServiceManager 与实名 Binder(Server)**
ServiceManager 和 DNS 类似,作用是将字符形式的 Binder 名字转化成 Client 中对该Binder 的引用,使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用。注册了名字的 Binder 叫实名 Binder,就像网站一样除了除了有 IP 地址以外还有自己的网址。Server创建了 Binder,并为它起一个字符形式,可读易记得名字,将这个 Binder 实体连同名字一起以数据包的形式通过 Binder 驱动发送给 ServiceManager ,通知 ServiceManager 注册一个名为“张三”的 Binder,它位于某个 Server 中。驱动为这个穿越进程边界的 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager。ServiceManger 收到数据后从中取出名字和引用填入查找表。细心的读者可能会发现,ServierManager 是一个进程,Server 是另一个进程,Server 向ServiceManager 中注册 Binder 必然涉及到进程间通信。当前实现进程间通信又要用到进程间通信,这就好像蛋可以孵出鸡的前提却是要先找只鸡下蛋!Binder 的实现比较巧妙,就是预先创造一只鸡来下蛋。ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡)。其次这个 Binder 实体的引用在所有 Client 中都固定为 0 而无需通过其它手段获得。也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个0 号引用和 ServiceManager 的 Binder 通信。类比互联网,0 号引用就好比是域名服务器的地址,你必须预先动态或者手工配置好。要注意的是,这里说的 Client 是相对于ServiceManager 而言的,一个进程或者应用程序可能是提供服务的 Server,但对于ServiceManager 来说它仍然是个 Client。
Client 获得实名 Binder 的引用
Server 向 ServiceManager 中注册了 Binder 以后, Client 就能通过名字获得 Binder 的引用了。Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder: 我申请访问名字叫张三的 Binder 引用。ServiceManager 收到这个请求后从请求数据包中取出 Binder名称,在查找表里找到对应的条目,取出对应的 Binder 引用作为回复发送给发起请求的Client。从面向对象的角度看,Server 中的 Binder 实体现在有两个引用:一个位于ServiceManager 中,一个位于发起请求的 Client 中。如果接下来有更多的 Client 请求该Binder,系统中就会有更多的引用指向该 Binder ,就像 Java 中一个对象有多个引用一样
上面说过。进程之间的内存划分为各自进程的用户空间和内核空间;用户空间指的是用户程序所运行的空间,内核空间是 Linux 内核的运行空间,为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。
用户态和内核态虽然从逻辑上进行了用户空间和内核空间的划分,但不可避免的用户空间需要访问内核资源,比如文件操作、访问网络等等。为了突破隔离限制,就需要借助系统调用来实现
copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间
Linux下的传统IPC通信原理
通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copyfromuser() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copytouser() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。这种传统的 IPC 通信方式有两个问题:性能低下,一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝;接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
Binder跨进程通信原理动态内核可加载模块 && 内存映射这个动态内核可加载模块,就是Binder驱动,运行在内核空间,使各个用户进程通过Binder实现通信。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。Binder涉及到的内存映射通过mmap()实现
Binder实现原理:
首先Binder驱动在内核空间创建一个数据接收缓存区;接着在内核空间开辟一块内存缓存区,建立内核缓冲区和内核中数据接收缓存缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系发送方通过系统调用copy from user()将数据copy到内核中的内核缓冲区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间。
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 进程,一次通信就完成了。