简介
Android是如何实现跨进程通信的,大家熟悉的Binder是什么,怎么设计的,进程间的数据如何发送接收的。本文将以及解析,并对Binder驱动实现、Native层实现、Java层实现三块做一个总结分析。
Binder学习思路
- Binder与传统IPC的区别
- Binder驱动的内部设计、数据结构
- Binder驱动与应用程序进程(C/S)之间的通信过程
- Android应用程序通过Binder驱动进行通信的流程
- Android开发人员如何使用Binder通信(AIDL、Java层架构)
基础知识理解
- Unix内核和应用程序进程所使用的物理内存是分开的,内核使用1G的物理内存,其他应用程序有各自的3G物理内存(32位操作系统)
- 因为内核和应用程序的物理内存是分开的,所以两者之间传递数据需要进行数据拷贝
- 内存映射(mmap)可以将两个虚拟内存地址空间(不同进程)映射到同一物理内存段上。实现多进程(或者内核与进程)之间公用一块内存,减少数据拷贝次数
- Unix驱动程序是一个运行在内核态(使用内核对应的物理内存)的程序
- Binder也是一种IPC的实现方式,其与传统的Unix IPC有一定的差别(使用了mmap)
理解Binder驱动的存在
因为要实现跨进程通信,那么,数据是如何传输的,怎么组织的。两个进程之间是如何知道对方的标识(引用)的,这一系列问题,都由Binder驱动解决,每个进程需要为其他进程提供服务(API调用),都需要向Binder驱动注册,其他进程才能知道自己的数据传向哪里。这里大家先忽略ServiceManager的特殊身份。只讨论Binder驱动的觉得定位即可。
这样看来,其实Binder驱动就是一个多个进程之间的中枢神经,支撑起了Android中进程间通信,它内部的设计,与应用程序进程中的业务,不存在任何耦合关系,只负责实现进程间数据通信。可以用如下图来理解Binder驱动与应用程序进程之间的关系。
当然,Android里的Binder架构应该还有ServiceManager这个系统服务。
ServiceManager的存在
ServiceManager下文简称SM,是一个Android操作系统提供的一个系统进程。那么为什么要单独提他呢,因为这个进程里,记录了所有Binder实体(提供服务的Binder实现对象)的信息。
也就是说,SM是用来给应用程序查找其他应用程序的数据中心与校验中心,保障进程间通信的安全新,合法性。
SM是系统服务,在系统启动后,SM便启动,并执行以下事情:
- 打开Binder驱动
- 将自己注册为Binder驱动的大管家(其他进程根据引用编号0可以找到SM对应的Binder实体)
- 进入循环,不断从Binder驱动中读取消息(无消息被阻塞)
- 读取到消息之后处理消息
- 不断循环,永不退出
SM处理的消息类型有:
- 注册Binder实体对象的
- 查询Binder实体对象,以引用编号的形式放回给查询进程
注册Binder实体信息到SM的时候,请求数据中需要写到Binder实体的描述信息,之后进行查询的时候就是根据描述信息来获取到对应的Binder应用编号。
到这里,我们可以看出,其实整个Binder架构就是一个Client,Server,DNS的结构,当然Binder驱动就扮演了一个路由器的角色。
这个结构的前提,就是DNS需要提前注册。也就是说SM进程需要第一个注册到Binder驱动中,而且,Client和Server都知道SM的引用编号(0),能够直接通过SM获取其他进程提供的Binder引用编号
Binder驱动启动过程
打开
- 每个需要通过Binder通信的进程都需要打开/dev/binder驱动一次(至多一次)
- 打开Binder驱动之后,内核会调用驱动程序的binder_open方法,该方法内部将会创建binder_proc结构体,内存存储了进程信息以及UID信息。
内存映射
- 使用mmap对/dev/binder进行内存映射操作
- 在mmap调用之后,内核会会调用驱动程序的binder_mmap方法,该方法内部会为进程创建binder_buffer结构体,也就是为进程创建缓冲区,用于接收数据。并且这块内和缓冲区对应有两个虚拟内存地址区间,一个是内核的虚拟空间,一个是进程用户空间的虚拟空间。此块缓冲区是一个只读的区域,防止用户空间对其进行修改。
动作执行者
对于应用程序进程来说,打开驱动和内存映射动作由Native类ProcessState完成,该类为单利,在构造方法中进行,先打开,再执行内存映射。
Binder与共享内存之间的区别
为什么与共享内存进行对比(性能),是因为共享内存管是unix中最快的一种IPC机制。
共享内存为什么快,是因为共享内存相当于是将两个进程的虚拟地址空间指向了一块物理内存,两个进程对该内存区域的修改,能够直接反应到对方进程中,也就是不需要对数据进行拷贝。
前面说到,Binder是通过mmap来实现的,理论上,mmap也可以让两个进程映射到同一段物理内存区域(文件)上。但是Binder没有这样实现,如果这样的话,和共享内存就一样了。那Binder又是如何实现的呢。
首先,Binder有驱动程序,所有数据传输和接收,都是通过Binder驱动来操作的。这就带来一个问题,Binder驱动是运行在内核态的,那么数据在使用Binder驱动传输时,是需要在内核内存空间与用户内存空间进行拷贝操作的。
试想下,A进程与B进程进行通信,A进程给B进程发送数据data,按照上面的分析,数据data需要先从A进程的用户空间拷贝到Binder驱动的内核空间,再通过Binder驱动写入到(具体实现后面说)B进程的Binder驱动内核空间,最后从Binder驱动再拷贝的B进程的用户空间。如此一来,数据进行了两次拷贝。
其实,Binder驱动内部并不需要两次数据的拷贝,原因在于Binder将内核内存空间与用户内存空间进行了内存映射操作,具体如下图
首先,我们从数据接收进程看,内核与用户内存空间,通过mmap映射到了同一块物理内存上。也就是说对该块物理内存的修改,将会提现到数据接收进程的用户空间和内核空间。
再看数据发送进程,左边的数据发送进程,只是将内核的内存空间映射到了物理内存上。
接着,当数据发送进程需要向数据接收进程传递数据时,数据只需要从数据发送进程的用户内存空间拷贝到数据发送进程的内核内存空间,此时,因为数据发送进程的内核内存空间与物理内存进行了映射,而数据接收进程的用户内存空间与内核内存空间同时都映射到了同一块物理内存上,所以此次拷贝,直接将数据发送进程的用户空间数据,拷贝到了数据接收进程的用户内存空间。
通过上面的分析,也就能理解,为什么说Binder传输数据时需要拷贝1次数据,共享内存不需要拷贝数据
Binder的实现架构
完成对Binder跨进程通信底层IPC实现分析之后,需要思考,Android如何让两个进程建立联系(如何找到通信进程),那就需要一个系统进程,所有应用程序都知道它,并能联系到它,从这个系统进程那边,能够查找到(通过Service名字符串)需要通讯的进程。
最终,Android采用了Client、Server、ServiceManager的实现架构,其中Client需要从ServiceManager中找到Server,然后Client与Server之间即可进行通信
那么什么进程能够在ServiceManager中注册呢,就是在Android操作系统中注册过(APP清单文件中的Service)的那部分服务才能注册,到这,也就能理解Android为什么采用这种架构模式了,在安全上又进一步约束。
Binder驱动
首先要知道Binder驱动是运行在内核态下,内核态的内存是所有进程共享的。
任务一:存储所有进程的Binder信息(引用编号,Server端的虚拟内存地址)
任务二:进程间数据传递
Binder是什么
Binder是什么,需要从多方面解释,不同环境中,其代表的是不一样的东西。
Binder在Server中的表述
Binder在Server中代表的是具体的实现,简称Binder实体
Binder在Client中的表述
Binder的具体实现应该是在Server进程,也就是说Client进程是无法拿到该实现对象的地址信息的。
那么Binder在Client中代表的仅仅是一个引用(驱动给的)编号,Client能够通过该编号向远端Server发送数据。
Binder在驱动中的表述
驱动,是Binder架构在最核心的一部分,驱动需要做的事情很多
- 所有Server端的Binder实体,需要在驱动中注册
- Client端获取Binder时,需要为Client创建Binder引用,并把引用编号信息记录在驱动中
- 维护各个Client中的引用于Binder实体之间的映射关系
- 通过引用编号找到对应实体
- 创建Server端的Binder实体
- etc…
Binder实体(Server端)在驱动中的表述
Binder实体需要在驱动中进行注册,注册时,驱动需要在内核中为Binder实体创建一个结构体binder_node
该结构体中存储的主要数据为
Server端Binder实体对象的内存地址
Server端Binder实体在所有实体链表中的节点结构体
说明:每个Server进程都对应有一个链表,用来存储所有的Binder实体节点,以Binder实体对象的内存地址为索引进行查找。
Binder引用(Client端)在驱动中的表述
Binder引用在驱动中以binder_ref结构体的形式存在。改结构体中存储的主要数据为:
- Binder实体在驱动中的结构体引用
- Binder实体在驱动中的引用号(编号)
- Binder引用在进程链表中的节点(以编号以及实体地址为索引的两个链表节点)
说明:每个Client进程都对应有两个链表,一个是以Binder实体在驱动中的结构体地址为索引建立的链表,一个是以Binder实体在驱动中的引用号为索引简历的链表。
Binder在传输数据中的表述
虽然Binder实体和Binder引用都在驱动中有不同的结构体来标识,但是Client和Server在于Binder进行通信时,并不是通过传递这两个结构体来代表不同的Binder的,而是通过另一个统一的结构体flat_binder_object来代表本次通信对应的Binder。
既然使用的是同一个结构体,那么这个结构体中应该有的内容:
- Binder类型(实体,引用)
- Binder实体的内存地址(类型为实体时用)
- Binder应用的编号(类型为引用时用)
其中Binder类型有以下几种:
- BINDER_TYPE_BINDER:表示传递的是Binder实体,并且指向该实体的引用都是强类型;
- BINDER_TYPE_WEAK_BINDER:表示传递的是Binder实体,并且指向该实体的引用都是弱类型;
- BINDER_TYPE_HANDLE:表示传递的是Binder强类型的引用
- BINDER_TYPE_WEAK_HANDLE:表示传递的是Binder弱类型的引用
- BINDER_TYPE_FD:表示传递的是文件形式的Binder
那么flat_binder_object里的内容填充方式具体是怎样的呢,比如Server将Binder传递给Client,Server发送的flat_binder_object,类型应该是BINDER_TYPE_BINDER,此时,驱动将会在内核中为Server进程创建对应的binder_node结构,并且将flat_binder_object中的Binder实体的内存地址保存起来。接着驱动需要在内核中为Client进程创建一个binder_ref结构,因为Server穿过来的Binder实体的内存地址在Client进程是无效的,所以驱动需要为Client进程创建一个Binder对应的引用编号,并将此编号存入binder_ref结构中。同时,需要将flat_binder_object中的类型改成BINDER_TYPE_HANDLE,以及存储引用编号。
当Client需要使用Server传递过来的Binder的时候,向驱动传递的数据包中,就需要用到Binder的引用编号,驱动将会对引用编号进行校验,这样就能在安全性上得到保障。
Binder表述总结
当一个Server进程创建了一个Binder实体,之后,这个实体在各个环境中的表述情况为
- Server进程中的Binder称为Binder实体,其应该要继承BBinder类(Native类)
- 其在Binder驱动中,以binder_node表述
- 当Server进程的Binder服务需要被Client进程所使用时,Binder驱动会创建一个binder_ref结构体,这也就是Server中创建的Binder实体在Client进程中的表述(存储引用编号)
- 在Client的用户空间中,需要创建一个Binder代理类,该类继承BpBinder类,Client进程通过该代理类与Server端的Binder实体进行通信