一、跨进程通信方式
跨进程通信主要有以下几类:管道、Socket、共享内存、信号。
1. 管道
管道的特点是半双工&单向的,管道里面的数据只能往一个方向流动。一般情况下管道是在父子进程之间使用的。
2. socket
socket的特点是全双工,即可读也可写。可以用在两个无亲缘关系的进程之间,但需要公开路径。
例子:在Android的Framework机制中,zygote就是通过socket来接受AMS的请求,然后启动应用进程的。
3. 共享内存
共享内存的特点:速度快,且不需要多次拷贝,且进程之间不需要存在亲缘关系,只需要拿到文件描述符即可。
这里补充一下:管道和socket的问题在于数据不能太大,否则性能会非常糟糕,相比较共享内存不存在这个问题。
4. 信号
信号的特点是:
a).单向的,发送出去后不管其他人接受者是如何处理的;
b).只能带信号,不能带其他参数。
c).知道进程的pid就可以发信号,而且一次可以一群进程发信号(需 root权限 或 同uid 才行)。
例子:Android的Process#killProcess方法,就是发送的 SIGNAL_KILL 信号。
5. Binder
Binder机制是Android特有的进程间通信的机制,特点为:采用C/S的通信模式、有更好的传输性能,最重要的特点是安全。
Android的四大组件,有时候不同的组件之间所在的进程是不一样的,当处于不同的进程的时候,就需要进行进程间通信了。这些进程间的通信依赖于Binder IPC机制。不仅如此,Android 系统对应用层提供的服务如:AMS、PMS等都是基于Binder IPC机制实现的。Binder机制在Android系统中的位置非常重要。
二、为何 Android 选用 Binder 作为跨进程通信的解决方案
1. 从性能方面考虑
Socket 作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。Binder 只需要一次数据拷贝,性能上仅次于共享内存。
2. 从稳定性方面考虑
Binder 基于 C/S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。共享内存虽然无需拷贝,但是控制负责,难以使用。从稳定性的角度讲,Binder 机制是优于内存共享的。
3. 从安全性方面考虑
传统的 IPC 没有任何安全措施,完全依赖上层协议来确保。首先传统的 IPC 接收方无法获得对方可靠的进程用户ID/进程ID(UID/PID),从而无法鉴别对方身份。Android 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志。Android 需要建立一套新的 IPC 机制来满足系统对稳定性、传输性能和安全性方面的要求,这就是 Binder。
三、Binder的通信架构
首先建议先看一下:Linux下进程间通信的原理 和 Binder 跨进程通信原理 这两篇文章。
Binder的通信架构示例如下图:
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 驱动的交互来间接的实现跨进程通信。
1. 进程如何启动Binder机制
首先打开Binder驱动,Binder驱动会给进程创立一套档案。然后将返回的描述符进行内存映射,分配缓存区。最后启动Binder线程,Binder线程会注册到Binder驱动,同时线程进入Loop循环,不断的跟Binder驱动交互。
2. Binder机制的分层架构
Binder机制的分层架构如下图所示:
因为Binder层Java机制我们通过实践已经知道其基本逻辑,这里我们了解一下Native层的逻辑,方便我们更深入的理解Binder机制。Binder的Native层的代码地址:http://androidxref.com/kernel_3.18/xref/drivers/staging/android/binder.c。
3. binder.c 主要逻辑方法梳理
在之前我们提到了Binder的通信架构和分层架构,这里我们梳理一下binder.c中比较重要的方法。
1). binder_init 方法 - 驱动初始化
在binder_init 方法中,首先创建名为binder的内核线程,用于处理工作队列中的任务。
2). binder_open 方法 - 打开设备
在binder_open方法中,执行了如下操作:首先创建了binder_proc结构体实例,然后初始化一系列成员变之后,将binder_proc链入binder_procs哈希表中,然后进行相关目录及文件的创建。
3). binder_mmap 方法 - 内存映射
在上面打开binder后,需要调用mmap进行内存映射,该函数经过系统调用,会调用到binder驱动的biner_mmap函数。
4). binder_ioctl 方法 - 设备控制
octl是Linux中常见的系统调用,它用于对底层设备的一些特性进行控制的用户态接口,应用程序在调用ioctl进行设备控制时,最后会调用到设备注册struct_file_operations结构体对象时的钩子上。
Binder做为Android中进程间高效通信的核心组件,其底层是以misc设备驱动的形式实现的,但它本身并没有实现read、write操作,所有的控制都是通过ioctl操作来实现。
四、Binder 通信过程的总结
1. 首先,一个进程通过 Binder 驱动将自己注册成为 ServiceManager;
2. Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
3. Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
五、Binder通信机制的同步与异步
一般我们声明AIDL的时候,都是类似如下的方式,此方法是同步方法:
interface IPlayer {
int getVolume();// 同步,假设执行1秒
}
其实我们也可以使用oneway来修饰AIDL内部的方法,修饰完成后就表示此方法是异步调用的。例如:
interface IPlayer {
oneway void start();//异步,假设执行2秒
oneway void stop();//异步,假设执行2秒
int getVolume();// 同步,假设执行1秒
}
在上面的代码中,Client端调用IPlayer.start(),Server端的start需要执行2秒,由于定义的接口是异步的,Client端可以快速的执行IPlayer.start(),不会被Server端block住2秒。但是当Client端调用IPlayer. getVolume(),Server端的getVolume需要执行1秒,由于定义的接口是同步的,Client端在执行IPlayer. getVolume()的时候,会被Server端block住1秒。
为什么Android的Binder机制会提供同步/异步的调用机制呢?答:异步调用在不需要Server端的返回值或者状态的时候,能有效的提高Client的效率。