binder是Android平台的一种跨进程通信(IPC)机制,从应用层角度来说,binder是客户端和服务端进行通信的媒介。
ipc原理
ipc通信指的是两个进程之间交换数据,如图中的client进程和server进程。
Android为每个进程提供了虚拟内存空间,而每个Android进程只能运行在自己进程所拥有的虚拟内存空间。
内存空间又分为用户空间和内核空间,前者的数据不能进程间共享,但后者可以。图中的Client进程和Server进程就是利用了进程间可以共享各自内核空间的数据,来完成底层通信的工作。
Android的C/S通信机制
C/S通信指的就是Client和Server两个进程的通信,但实际通信时除了包含这两个进程,还有一个Service Manager,它用于管理各种服务。
这些服务通常是Android系统的核心功能模块,例如传感器管理、电源管理、WIFI管理、闹钟服务等等,与Android四大组件中的服务不同。
当一个Server(服务端)想要提供一种服务,首先需要在Service Manager注册该服务;
而当Client(客户端)想要使用Server中的服务时,不能直接访问,而是要从Service Manager获取该服务,才能使用Server所提供的服务,来与Server进行通信。
Binder通信模型
在引入binder机制后,客户端、服务端和Service Manager之间不能通过api直接互相访问,而是与内核空间的binder驱动通过ioctl方式来完成进程间的数据交换。
关键概念
- Binder实体对象:Binder服务的提供者,类型是BBinder,位于服务端
- Binder引用对象:Binder实体对象在客户端进程的代表,类型是BpBinder,位于客户端
- IBinder对象:Binder实体对象和引用对象的统称,也是他们的父类
- Binder代理对象:又称接口对象,为客户端的上层应用提供接口服务,类型是IInterface
Binder引用对象和代理对象都是服务端进程中的,把它们分离的好处是一个代理对象可以有多个引用对象,方便上层应用使用。
通信过程
注册服务
- server进程向binder驱动申请创建服务的binder实体
- binder驱动为这个服务创建位于内核的binder实体和binder引用
- 创建完成后,服务端通过binder驱动将binder引用发送给service manager
- service manager收到数据后,取出被创建服务的名字和引用,填入一张查找表
通过以上步骤,server进程通过binder驱动完成了在service manager的服务注册。
在注册服务的过程中,server进程是客户端,而service manager是服务端。
获取服务
- Client进程利用handle值为0的引用找到service manager
- Client进程向service manager发送xxxservice的访问申请
- service manager从请求表中获取xxxservice的名字,在查找表中找到对应的条目,取出对应的binder引用
- service manager把xxxservice的binder引用传给Client进程
使用服务
在使用服务时,Client和Server进程都是发送方和接收方。
这是因为Client在发送服务请求时,Server是接收方;当Server返回数据给Client时,Client变成了接收方。
不论发送方是谁,都会通过自身的Binder实体,把数据发送给接收方的Binder引用。binder驱动回处理发送请求,利用内核空间进程共享机制如下:
- 把发送方的数据存入写缓存(binder_write_read.write_buffer)(对于接收方这是读缓存)
- 接收方一直处于阻塞状态,当写缓存有数据,会读取数据执行命令操作
- 接收方执行操作后,会把结果返回,同样放在写缓存区(对于发送方这是读缓存)
Android中的Binder
activity、service等组件都需要与ams(system_server)通信,这种跨进程的通信是由binder完成的。从不同角度分析binder如下:
- 机制:binder是一种进程间通信机制
- 驱动:binder是一个虚拟物理设备驱动
- 应用层:binder是一个能发起通信的java类:在java中,如果想要进程通信,就要继承binder
为什么要使用多进程进行开发?
虚拟机分配给各个进程的运行内存是有限制的,lmk也会优先回收占用系统资源大的进程。
对于多进程开发的优势一般有以下几点:
- 突破进程内存限制,为占用内存大的单独开辟一个进程
- 功能稳定性:如为通信线程保持长连接的稳定性
- 防止内存泄漏:如为容易内存泄漏的webview单独开辟一个进程
- 隔离风险:对于不稳定的进程放在独立进程,避免主进程崩溃
Binder有什么优势
首先回顾linux进程间的通信机制:管道、信号量、共享内存、socket。
从性能出发,共享内存 > binder > 其他ipc。
但共享内存的缺点也十分明显。与线程之间共享同一块内存相同,共享内存的进程也很容易出现死锁、数据不同步等问题,操作不方便。同时,socket作为一款通用接口,开销过大。
最重要的一点是安全性。传统ipc模式普遍存在的问题是依赖上层协议和访问接入点是开放的。
以创建服务为例,系统需要知道创建人的身份,但在传统ipc机制中,这个身份的获取是从上层协议获取的,即app将id传给系统,而app传回的内容可以是不真实的。对比之下,服务在被创建时,binder就会为创建人分配唯一的uid(用户身份)。
第二点以服务器为例,如果ip是开放的,服务器很容易就会被攻击。同样的,对于传统ipc,如果接入点被知晓,所有人都可以访问。对比之下,binder同时支持实名和匿名。实名与传统ipc相同,是开放的;匿名指的是如果有人需要获取服务,需要先获取到binder内部的引用,才能进行访问。通常的,系统服务是实名的,个人服务是匿名的。直接在service manager中注册的服务是实名的。
Binder是如何做到一次拷贝的
在回答问题之前详细解释一下ipc和虚拟内存的概念。
进程间通信和线程间不同的原因是两者的内存机制不同。对于线程而言,它们的内存是共享的,但进程之间的内存是相互隔离的。
在ipc原理中我们看到进程内部分为用户空间和内核空间,出于安全两者之间是隔离的,app和系统分别处理用户空间和内核空间。
设想如果进程中没有对这两部份进行隔离,app就可以任意访问系统才能访问的数据,而正常来说这样的访问是需要权限的。但这不意味着两者之间完全隔离。系统为两者通信提供了api(copy_from_user & copy_to_user),使得两者间可以互相通信。
对于我们编程而言,需要系统分配虚拟内存,这是因为物理内存不一定是一整块内存,而这种整块内存恰恰是我们在编程中常常需要的。
虚拟内存是通过MMU内存管理单元来映射到物理内存的。在设计的时候,所有进程的内核空间都被映射到了同一块物理内存。这么做的好处就是实现了内存共享,一个进程可以很方便的去获取物理空间中其他进程的内核空间。
以上就是传统的ipc通信方式。一个进程把自身用户空间的数据通过copy_from_user拷贝到内核空间,而另一个进程通过copy_to_user第二次拷贝从内核空间获取数据到自己的用户空间,这就拷贝了两次。
在binder机制下通信时,内核空间和数据接收方的用户空间映射了同一块物理内存。
简而言之,获取数据的进程的用户空间和内核空间都分配了一小块内存空间,指向同一块物理内存。而这就意味着当被获取数据的进程,从用户空间复制数据到内核空间后,如果内核空间把这个数据存储到这一小块内存空间,另一个进程就能够直接获取,不需要再做一次复制。
MMAP的原理
Linux将一个虚拟内存区域,与磁盘上的物理内存区域关联起来,以这种方式初始化这个虚拟内存区域的内容。这个过程称为内存映射(memory mapping)。
用户空间是不能直接访问磁盘上的内容的,如需访问要通过内核空间,这是肯定很慢的。 首先需要调用write方法从用户空间复制到内核空间,再把数据复制到磁盘,完成写入。
因此当我们使用mmap时关联了虚拟内存和物理内存,当我们在虚拟内存做操作时,物理内存就会直接被修改。