Android Binder 进程间通信
什么是Binder
Binder是Android系统中进程间通讯(IPC)的一种方式,也是Android系统中重要的特征之一。Android中的四大组件Activity、Service、Broadcast、ContentProvider,不同的App都运行在不同的进程中,它是这些进程间通讯的桥梁。正如其名“粘合剂”一样,它把系统中各个组件粘合到一起,是各个组件的桥梁。
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)(对于接收方这是读缓存)。
- 接收方一直处于阻塞状态,当写缓存有数据,会读取数据执行命令操作。
- 接收方执行操作后,会把结果返回,同样放在写缓存区(对于发送方是读缓存)。
Binder架构
- Binder采用C/S架构,从组件视角来说,包含Client、Service、ServiceManager以及Binder驱动,其中ServiceManager用户管理系统中的各种服务。
- Binder在Framework层进行封装,通过JNI技术调用Native(C/C++)层的Binder架构。
- Binder在Native层以Ioctl的方式与Binder驱动进行通信。
Activity、Service等组件都需要与AMS(system_server)通信,这种跨进程的通信是由Binder完成的。从不同角度分析Binder如下:
- 机制:Binder是一种进程间通信机制。
- 驱动:Binder是一个虚拟物理设备驱动。
- 应用层:Binder是一个能发起通信的Java类:在Java中,如果想要进程通信,就要继承Binder。
为什么要使用多进程进行分发?
虚拟接分配给各个进程的运行内存是有限制的,Imk也会优先回收占用系统资源大的进程。对于多进程开发的优势一般有以下几点:
- 突破进程内存限制,为占用内存大的单独开辟一个进程。
- 功能稳定性:如为通信进程保持长连接的稳定性。
- 防止内存泄漏:如为容易内存泄漏的WebView单独开辟一个进程。
- 隔离风险:对于不稳定的进程放在独立进程,避免主进程崩溃。
Binder机制
首先需要注册服务端,只有注册了服务端,客户端才有通讯的目标,服务端通过ServiceManager注册服务,注册的过程就是向Binder驱动的全局链表binder_proces中插入服务端的信息(binder_proc结构体,每个binder_proc结构体中都有todo任务队列),然后向ServiceManager的svcinfo列表中缓存一下注册的服务。
有了服务端,客户端就可以跟服务端通讯了,通讯之前需要先获取到服务,拿到服务的代理,也可以理解为引用。比如下面的代码:
//获取WindowManager服务引用 WindowManager wm = (WindowManager)getSystemService(getApplication().WINDOW_SERVICE);
获取服务端的方式就是通过ServiceManager向svcinfo列表中查询一下返回服务端的代理,svcinfo列表就是所有已注册服务的通讯录,保存了所有注册的服务信息。
有了服务端的引用我们就可以向服务端发送请求了,通过BinderProxy将我们的请求参数发送给ServiceManager,通过共享内存的方式使用内核方法copy_from_user()将我们的参数先拷贝到内核空间,这时我们的客户端进入等待状态,然后Binder驱动向服务端的todo队列里面插入一条事务,执行完之后把执行结果通过copy_to_user()将内核的结果拷贝到用户空间(这里只是执行了拷贝命令,并没有拷贝数据,Binder只进行一次拷贝),唤醒等待的客户端并把结果响应回来,这样就完成了一次通讯。
以上就是Binder机制的主要通讯方式,下面我们来看看具体实现。
Binder驱动
我们先来了解下用户空间与内核空间是怎么交互的。
用户空间/内核空间
内核空间定义
Linux 中的系统内存可以分为两个不同的区域:内核空间和用户空间。内核空间是内核(即操作系统的核心)执行(即运行)并提供其服务的地方。
内存由 RAM(随机存取存储器)单元组成,其内容可以以极高的速度访问(即读取和写入),但只能临时保留(即,在使用时或最多在电源保持开启时保留)。它的目的是保存当前正在使用的程序和数据,从而充当 CPU(中央处理器)和速度慢得多的存储之间的高速中介,后者通常由一个或多个硬盘驱动器 (HDD) 组成。
用户空间是用户进程(即内核以外的所有内容)运行的内存位置的集合。进程是程序的执行实例。内核的作用之一是管理此空间内的各个用户进程,并防止它们相互干扰。
用户进程只能通过使用系统调用来访问内核空间。系统调用是类 Unix 操作系统中由活动进程对内核执行的服务(例如输入/输出 (I/O) 或进程创建)的请求。活动进程是当前在 CPU 中正在进行的进程,与等待 CPU 中的下一轮次的进程形成对比。I/O 是指在 CPU 和外围设备(如磁盘驱动器、键盘、鼠标和打印机)之间传输数据的任何程序、操作或设备。
Kernel space是Linux内核的运行空间,User space是用户程序的运行空间。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。
Kernel space可以执行任意命令,调用系统的一切资源;User space只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称System Call),才能向内核发出指令。
系统调用/内核态/用户态
虽然从逻辑在抽离出用户空间和内核空间;但是不可避免的是,总有那么一些用户空间需要访问内核的资源;比如应用程序访问文件,网络是很常见的事情。
Kernel space can be accessed by user processes only through the use of system calls.
用户空间访问内核空间的唯一方式就是系统调用;通过这个统一入口接口,所有的资源访问都是内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。用户软件良莠不齐,要是它们搞乱把系统玩坏了怎么办?因此对于某些特权操作必须交给安全可靠的内核来执行。
当一个任务(线程)执行系统调用而陷入内核代码中执行时,我们就成称进程处于内核运行态(或简称为内核态)此时处理器处于特权级最高的(0级)内核代码中执行。当线程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。处理器在特权等级高的时候才能执行那些特权CPU指令。
内核模块/驱动
通过系统调用,用户空间可以访问内核空间,那么如果一个用户空间想与另外一个用户空间进行通信怎么办呢?很自然想到的是让操作系统内核添加支持;传统的Linux通讯机制,比如Socket、管道等都是内核支持的;但是Binder并不是Linux内核的一部分,它是怎么做到访问内核空间的呢?Linux的动态可加载内核模块(Loadable Kernel Module,LKM)机制解决了这个问题;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被连接到内核作为内核的一部分在内核空间运行。这样,Android系统可以通过添加一个内核模块运行在内核空间,用户进程之间的通过这个模块作为桥梁,就可以完成通讯了。
在Android系统重,这个运行在内核空间的,负责各个用户进程通过Binder通信的内核模块叫做Binder驱动;
驱动程序一般指的是设备驱动程序(Device Driver),是一种可以使计算机和设备通信的特殊程序。相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作;
驱动就是操作硬件的接口,为了支持Binder通信过程,Binder使用了一种“硬件”,因此这个模块被称之为驱动。
熟悉了上面这些概念,我们再来看下上面的图,用户空间中binder_open(),binder_mmap(),binder_ioctl()这些方法通过System Call来调用内核空间Binder驱动中的方法。内核空间与用户空间共享内存通过copy_from_user(),copy_to_user()内核方法来完成用户空间与内核空间内存的数据传输。Binder驱动中有一个全局的binder_porcs链表保存了服务端的进程信息。
Binder进程和线程
对于底层Binder驱动,通过 binder_procs 链表记录所有创建的 binder_proc 结构体,binder 驱动层的每一个 binder_proc 结构体都与用户空间的一个用于 binder 通信的进程一一对应,且每个进程有且只有一个 ProcessState 对象,这是通过单例模式来保证的。在每个进程中可以有很多个线程,每个线程对应一个 IPCThreadState 对象,IPCThreadState 对象也是单例模式,即一个线程对应一个 IPCThreadState 对象,在 Binder 驱动层也有与之相对应的结构,那就是 Binder_thread 结构体。在 binder_proc 结构体中通过成员变量 rb_root threads,来记录当前进程内所有的 binder_thread。
Binder 线程池:每个 Server 进程在启动时创建一个 binder 线程池,并向其中注册一个 Binder 线程;之后 Server 进程也可以向 binder 线程池注册新的线程,或者 Binder 驱动在探测到没有空闲 binder 线程时主动向 Server 进程注册新的的 binder 线程。对于一个 Server 进程有一个最大 Binder 线程数限制,默认为16个 binder 线程,例如 Android 的 system_server 进程就存在16个线程。对于所有 Client 端进程的 binder 请求都是交由 Server 端进程的 binder 线程来处理的。
ServiceManager启动
了解了 Binder 驱动,怎么与 Binder 驱动进行通讯呢?那就是通过 ServiceManager,好多文章称 ServiceManager 是 Binder 驱动的守护进程,大管家,其实 ServiceManager 的作用很简单就是提供了查询服务和注册服务的功能。下面我们来看一下 ServiceManager 启动的过程。
ServiceManager 分为 framework 层和 native 层,framework 层只是对 native 层进行了封装方便调用,图上展示的是 native 层的 ServiceManager 启动过程。
ServiceManager 的启动是系统在开机时,init 进程解析 init.rc 文件调用 service_manager.c 中的 main() 方法入口启动的。 native 层有一个 binder.c 封装了一些与 Binder 驱动交互的方法。
ServiceManager 的启动分为三步,首先打开驱动创建全局链表 binder_procs,然后将自己当前进程信息保存到 binder_procs 链表,最后开启 loop 不断的处理共享内存中的数据,并处理 BR_xxx 命令(ioctl 的命令,BR 可以理解为 binder reply 驱动处理完的响应)。
ServiceManager注册服务
注册 MediaPlayerService 服务端,我们通过 ServiceManager 的 addService() 方法来注册服务。
首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令(ioctl 的命令,BC 可以理解为 binder client 客户端发过来的请求命令)携带 ADD_SERVICE_TRANSACTION 命令,同时注册服务的线程进入等待状态 waitForResponse()。 Binder 驱动收到请求命令向 ServiceManager 的 todo 队列里面添加一条注册服务的事务。事务的任务就是创建服务端进程 binder_node 信息并插入到 binder_procs 链表中。
事务处理完之后发送 BR_TRANSACTION 命令,ServiceManager 收到命令后向 svcinfo 列表中添加已经注册的服务。最后发送 BR_REPLY 命令唤醒等待的线程,通知注册成功。
ServiceManager获取服务
获取服务的过程与注册类似,相反的过程。通过 ServiceManager 的 getService() 方法来注册服务。
首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令携带 CHECK_SERVICE_TRANSACTION 命令,同时获取服务的线程进入等待状态 waitForResponse()。
Binder 驱动收到请求命令向 ServiceManager 的发送 BC_TRANSACTION 查询已注册的服务,查询到直接响应 BR_REPLY 唤醒等待的线程。若查询不到将与 binder_procs 链表中的服务进行一次通讯再响应。
进程一次完整通讯
我们在使用 Binder 时基本都是调用 framework 层封装好的方法,AIDL 就是 framework 层提供的傻瓜式是使用方式。假设服务已经注册完,我们来看看客户端怎么执行服务端的方法。
首先我们通过 ServiceManager 获取到服务端的 BinderProxy 代理对象,通过调用 BinderProxy 将参数,方法标识(例如:TRANSACTION_test,AIDL中自动生成)传给 ServiceManager,同时客户端线程进入等待状态。
ServiceManager 将用户空间的参数等请求数据复制到内核空间,并向服务端插入一条执行执行方法的事务。事务执行完通知 ServiceManager 将执行结果从内核空间复制到用户空间,并唤醒等待的线程,响应结果,通讯结束。
Binder有什么优势
进程间的通讯机制:管道、信号量、共享内存、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时关联了虚拟内存和物理内存,当我们在虚拟内存做操作时,物理内存就会直接被修改。