摘要:本节主要来说解Android10.0 Binder的设计原理,如何设计一个Binder通讯html
阅读本文大约须要花费15分钟。java
文章首发微信公众号:IngresGenode
专一于Android系统级源码分析,Android的平台设计,欢迎关注我,谢谢!linux
[Android取经之路] 的源码都基于Android-Q(10.0) 进行分析android
[Android取经之路] 系列文章:git
《系统启动篇》github
《日志系统篇》数据库
《Binder通讯原理》设计模式
在Android应用、Android系统开发的时候,相信不少人都听过Binder的概念,并且无心间就用到了Binder机制,例如:写一个应用打开手电筒功能,某个应用启动服务等。缓存
这些动做都涉及到一个概念-进程间通讯。Android 中的每一个应用都是独立的进程,都有本身虚拟内存,两个进程之间不能互相访问数据。
在Android中,应用进程间互相访问数据,经常使用的通讯方式就Binder。
从前一节,咱们知道从Android 8.0 开始,Binder机制,被拆分红了Binder(System分区 进程间通讯)、HwBinder(支持System/Vendor分区进程间通讯)、VndBinder(Vendor分区进程间通讯)。
如今咱们先单独分析一下Binder的机制,HwBinder和VndBinder留到后面慢慢分析。
下图中涉及到Binder模型的4类角色:Binder驱动,ServiceManager,Server和Client。Binder机制的目的是实现IPC(Inter-Process Communication),即Client和Server之间的通讯。
其中Server,Client,ServiceManager运行于用户空间,Binder驱动运行于内核空间。这四个角色的关系和互联网相似:Server是服务器,Client是客户终端,ServiceManager是域名服务器(DNS),驱动是路由器。
1)IPC (进程间通讯-Inter process communication)
IPC属于通讯机制,Android中经常使用的IPC通讯:管道、共享内存、消息队列、信号量、socket、binder。
2)RPC (远程过程调用 Remote Procedure call)
RPC属于通讯机制中的调用方法,目的:不一样的进程之间,一个进程调用另外一个进程的对象。
RPC在调用一个远程过程后,本身进入等待状态,传往远程过程的参数包括过程参数,返回参数包括执行结果;
当收到包括执行结果的消息后,本地进程从消息中取得结果,调用进程从新开始执行。在服务器一方,有一个程序在等待调用,当有一个调用到达时,服务器进程取得进程参数,计算结果,而后返回结果。
调用能够同步的也能够是异步的;服务器能够建立一个线程来接收用户请求,也能够本身来接收用户请求
3)代理模式
为其余对象提供代理对象,以控制对这个对象的访问。
因为进程隔离的存在,一个进程内部的对象对另一个进程来讲没有任何意义。另外若是是代理对象的话,它能够存在各个进程内,就比如我们的AMS和PMS。
4)进程隔离
进程隔离是为保护操做系统中进程互不干扰而设计的一组不一样硬件和软件的技术。这个技术是为了不进程A写入进程B的状况发生。进程的隔离实现,使用了虚拟地址空间。进程A的虚拟地址和进程B的虚拟地址不一样,这样就防止进程A将数据信息写入进程B。
5)内核空间
能够访问受保护的内存空间,有访问底层硬件设备的全部权限。
6)用户空间
相对于内核空间,上层应用程序所运行的空间以及Native层进程运行的空间就是用户空间,用户空间访问内核空间的惟一方式就是系统调用。
7)系统调用/内核态/用户态
从逻辑上看,内核空间和用户空间是独立的,那么用户空间总要有办法调用内核空间,惟一的调用方式就是系统调用(System Call),经过这个统一入口接口,全部的资源访问都是在内核的控制下执行,以避免致使对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,咱们就称进程处于内核运行态(或简称为内核态)。
当进程在执行用户本身的代码时,则称其处于用户运行态(用户态)。
Binder的英文意思是粘结剂,把两个不相关的进程粘在一块儿,让两个进程能够进行数据交互。 好比咱们写一个应用,打开手电筒功能,那么要把打开这个动做,发给管理服务,这个发送的过程就是经过Binder机制来实现。
Android使用的Linux内核拥有着很是多的跨进程通讯机制,好比管道、共享内存、消息队列、信号量、socket等;为何还须要单独搞一个Binder出来呢?
主要有两点,性能和安全。
1)高效
在移动设备上,普遍地使用跨进程通讯确定对通讯机制自己提出了严格的要求;Binder相对出传统的Socket方式,更加高效;
IPC 内存拷贝次数:
Binder只须要一次拷贝就能将A进程用户空间的数据为B进程所用
数据从用户空间拷贝到内核中的时候,是直接拷贝到目标进程的内核空间,这个过程是在请求端线程中处理的,只不过操做对象是目标进程的内核空间。
Binder拷贝方式: 数据发送端(虚拟内存) copy_from_user --> 内核虚拟内存 <--mmap--> 数据接收端(虚拟内存)
内核虚拟内存和数据接收端虚拟内存采用mmap映射到同一块物理内存,不存在拷贝动做,数据发送端(Client)要把IPC数据 拷贝到内核虚拟内存空间,存在一次拷贝,因此Binder只存在一次内存拷贝
管道、队列等须要两次内存拷贝:
发送方缓存区--memcpy-->内核缓存区 --memcpy-->接收方缓存区 ,存在两次拷贝
共享内存SMD(Shared Memory Driver),虽然无需拷贝,但控制复杂,难以使用。
socket做为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通讯和本机上进程间的低速通讯。
2)安全
传统的进程通讯方式对于通讯双方的身份并无作出严格的验证,接收方没法得到对方进程可靠的UID/PID(用户ID/进程ID),只有在上层协议上进行架设;
好比Socket通讯ip地址是客户端手动填入的,均可以进行伪造;而Binder机制为每一个进程分配了UID/PID来做为鉴别身份的标示,从协议自己就支持对通讯双方作身份校检,于是大大提高了安全性。
3)能够很好的实现Client-Server(CS)架构
对于Android系统,Google想提供一套基于Client-Server的通讯方式。当Client须要获取某Server的服务时,只须要Client向Server发送相应的请求,Server收到请求以后进行处理,处理完毕再将反馈内容发送给Client。
可是,目前Linux支持的"传统的管道/消息队列/共享内存/信号量/Socket等"IPC通讯手段中,只有Socket是Client-Server的通讯方式。可是,Socket主要用于网络间通讯以及本机中进程间的低速通讯,它的传输效率过低。
在理解Binder架构前,咱们来考虑下,若是是你,该如何设计一个Binder的进程间通讯机制。
要实现一个IPC通讯那么须要几个核心要素:
1)发起端:确定包括发起端所从属的进程,以及实际执行传输动做的线程
2)接收端:接收发送端的数据。
3)待传输的数据
4)内存映射,内核态
首先先画一个最简单的IPC通讯图:
进程Process1和进程Process2 经过IPC通讯机制进行通讯。
再进行扩展调整,把IPC机制换成Binder机制,那么就变成以下的图形:
因为Android存在进程隔离,那么两个进程之间是不能直接传输数据的,Process1须要获得Process2的代理,Process2须要一个实体。
为了实现RPC,咱们的代理都是提供接口,称为“接口代理”,实体须要提供“接口实体”,以下图所示:
咱们把代理改为BpBinder,实体改为BBinder,接口代理改为BpInterface,接口实现体改为BnInterface。
咱们都知道两个进程的数据共享,须要陷入内核态,那就须要一个驱动设备“/dev/binder”,同时须要一个守护进行来进行service管理,咱们成为ServiceManager。
进一步演变为:
假如咱们想要把经过Process1 的微信信息发送给Process2的微信,咱们须要作下面几步:
0)Process2在腾讯服务器中进行注册(包括微信名称、当前活动的IP地址等)
1)Process1从朋友列表中中查找到 Process2的名称,这就是Process2的别名:"service_name"
2)Process1 编写消息消息内容,点击发送。 这里的消息内容就是IPC数据
3)数据会发送到腾讯的服务器,服务器理解为Binder驱动
4)服务器从数据库中解析出IPC数据,找到Process2信息,转到Process2注册的地址, 数据库理解为ServiceManager
5)把数据发给Process2,完成Process1和Process2的通讯
咱们能够简单的把上面的顺序内容进行转换:
1)Binder驱动---腾讯服务器
2)数据库--ServiceManager
3)Service_name: Process2的微信名称
4)IPC数据:Process1 发送的微信消息
Native C/C++和内核进行通讯须要经过系统调用,ServiecManager的主要用来对Service管理,提供了add\find\list等操做。Native进程的数据直接能够经过系统调用陷入内核态,进入图像转换,变为以下:
上面列举的是Native C/C++空间的进程进行Binder通讯机制,那么JAVA层是如何通讯的呢,Native层的Binder提供的是libbinder.so,那么从JAVA到Native,须要通过JNI、Framework层的封装,
JNI层的命名一般为android_util_xxx,咱们这里是binder机制,那么JNI层的文件为 android_util_binder,同时Native的BBinder不能直接传给JAVA层,在JNI里面转换了一个JavaBBinder对象。
Framework层给应用层提供时,其实提供的也是一个代理,咱们也称之为BinderProxy。
在JAVA侧要对应一个Binder的实体,称之为Binder。
JAVA侧的服务进行也须要一个管理者,相似于Native,建立了JAVA的ServiceManager,那么设计以下:
Binder 通讯采用 C/S 架构,从组件视角来讲,包含 Client、 Server、 ServiceManager 以及 Binder 驱动,其中 ServiceManager 用于管理系统中的各类服务。
Binder 在 framework 层进行了封装,经过 JNI 技术调用 Native(C/C++)层的 Binder 架构。
Binder 在 Native 层以 ioctl 的方式与 Binder 驱动通信。
Binder通讯流程以下:
1.首先服务端须要向ServiceManager进行服务注册,ServiceManager有一个全局的service列表svcinfo,用来缓存全部服务的handler和name。
2.客户端与服务端通讯,须要拿到服务端的对象,因为进程隔离,客户端拿到的实际上是服务端的代理,也能够理解为引用。客户端经过ServiceManager从svcinfo中查找服务,ServiceManager返回服务的代理。
3.拿到服务对象后,咱们须要向服务发送请求,实现咱们须要的功能。经过 BinderProxy 将咱们的请求参数发送给 内核,经过共享内存的方式使用内核方法 copy_from_user() 将咱们的参数先拷贝到内核空间,这时咱们的客户端进入等待状态。而后 Binder 驱动向服务端的 todo 队列里面插入一条事务,执行完以后把执行结果经过 copy_to_user() 将内核的结果拷贝到用户空间(这里只是执行了拷贝命令,并无拷贝数据,binder只进行一次拷贝),唤醒等待的客户端并把结果响应回来,这样就完成了一次通信。
在这里其实会存在一个问题,Client和Server之间通讯是称为进程间通讯,使用了Binder机制,那么Server和ServiceManager之间通讯也叫进程间通讯,Client和Server之间还会用到ServiceManager,也就是说Binder进程间通讯经过Binder进程间通讯来完成,这就比如是 孵出鸡前提倒是要找只鸡来孵蛋,这是怎么实现的呢?
Binder的实现比较巧妙:预先创造一只鸡来孵蛋:ServiceManager和其它进程一样采用Binder通讯,ServiceManager是Server端,有本身的Binder对象(实体),其它进程都是Client,须要经过这个Binder的引用来实现Binder的注册,查询和获取。
ServiceManager提供的Binder比较特殊,它没有名字也不须要注册,当一个进程使用BINDER_SET_CONTEXT_MGR_EXT命令将本身注册成ServiceManager时Binder驱动会自动为它建立Binder实体(这就是那只预先造好的鸡)。
其次这个Binder的引用在全部Client中都固定为0(handle=0)而无须经过其它手段得到。也就是说,一个Server若要向ServiceManager注册本身Binder就必须经过0这个引用号和ServiceManager的Binder通讯。
类比网络通讯,0号引用就比如域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的Client是相对ServiceManager而言的,一个应用程序多是个提供服务的Server,但对ServiceManager来讲它仍然是个Client。
图片来源csdn-jeanboydev
ServiceManager启动后,会经过系统调用mmap向内核空间申请128K的内存,用户进程会经过mmap向内核申请(1M-8K)的内存空间。
这里用户空间mmap (1M-8K)的空间,为何要减去8K,而不是直接用1M?
Android的git commit记录:
Modify the binder to request 1M - 2 pages instead of 1M. The backing store in the kernel requires a guard page, so 1M allocations fragment memory very badly. Subtracting a couple of pages so that they fit in a power of two allows the kernel to make more efficient use of its virtual address space.
大体的意思是:kernel的“backing store”须要一个保护页,这使得1M用来分配碎片内存时变得不好,因此这里减去两页来提升效率,由于减去一页就变成了奇数。
系统定义:BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2) = (1M- sysconf(_SC_PAGE_SIZE) * 2)
这里的8K,其实就是两个PAGE的SIZE, 物理内存的划分是按PAGE(页)来划分的,通常状况下,一个Page的大小为4K。
内核会增长一个guard page,再加上内核自己的guard page,正好是两个page的大小,减去后,就是用户空间可用的大小。
在内存分配这块,还要分为32位和64位,32位的系统很好区分,虚拟内存为4G,用户空间从低地址开始占用3G,内核空间占用剩余的1G。
ARM32内存占用分配:
但随着如今的硬件发展愈来愈迅速,应用程序的运算也愈来愈复杂,占用空间愈来愈大,原有的4G虚拟内存已经不能知足用户的需求,所以,如今的Android基本都是用64位的内存机制。
理论上讲,64位的地址总线能够支持高达16EB(2^64)的内存。AMD64架构支持52位(4PB)的地址总线和48位(256TB)的虚拟地址空间。在linux arm64中,若是页的大小为4KB,使用3级页表转换或者4级页表转换,用户空间和内核空间都支持有39bit(512GB)或者48bit(256TB)大小的虚拟地址空间。
2^64 次方太大了,Linux 内核只采用了 64 bits 的一部分(开启 CONFIG_ARM64_64K_PAGES 时使用 42 bits,页大小是 4K 时使用 39 bits),该文假设使用的页大小是 4K(VA_BITS = 39)
ARM64 有足够的虚拟地址,用户空间和内核空间能够有各自的 2^39 = 512GB 的虚拟地址。
ARM64内存占用分配:
用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间。
Client(数据发送端)先从本身的用户进程空间把IPC数据经过copy_from_user()拷贝到内核空间。而Server端(数据接收端)与内核共享数据(mmap到同一块物理内存),再也不须要拷贝数据,而是经过内存地址空间的偏移量,便可获悉内存地址,整个过程只发生一次内存拷贝。
图片来源于Gityuan
下图展现了Binder中各个角色之间的关系:
图片来源:universus
1)Binder实体
Binder实体,是各个Server以及ServiceManager在内核中的存在形式。
Binder实体其实是内核中binder_node结构体的对象,它的做用是在内核中保存Server和ServiceManager的信息(例如,Binder实体中保存了Server对象在用户空间的地址)。简言之,Binder实体是Server在Binder驱动中的存在形式,内核经过Binder实体能够找到用户空间的Server对象。
在上图中,Server和ServiceManager在Binder驱动中都对应的存在一个Binder实体。
2)Binder引用\代理
说到Binder实体,就不得不说"Binder引用"。所谓Binder引用,其实是内核中binder_ref结构体的对象,它的做用是在表示"Binder实体"的引用。换句话说,每个Binder引用都是某一个Binder实体的引用,经过Binder引用能够在内核中找到它对应的Binder实体。
若是将Server看做是Binder实体的话,那么Client就比如Binder引用。Client要和Server通讯,它就是经过保存一个Server对象的Binder引用,再经过该Binder引用在内核中找到对应的Binder实体,进而找到Server对象,而后将通讯内容发送给Server对象。
Binder实体和Binder引用都是内核(Binder驱动)中的数据结构。每个Server在内核中就表现为一个Binder实体,而每个Client则表现为一个Binder引用。这样,每一个Binder引用都对应一个Binder实体,而每一个Binder实体则能够多个Binder引用。
3)远程服务
Server都是以服务的形式注册到ServiceManager中进行管理的。若是将Server自己看做是"本地服务"的话,那么Client中的"远程服务"就是本地服务的代理。远程服务就是本地服务的一个代理,经过该远程服务Client就能和Server进行通讯。
4)ServiceManager守护进程
ServiceManager是用户空间的一个守护进程。当该应用程序启动时,它会和Binder驱动进行通讯,告诉Binder驱动它是服务管理者;对Binder驱动而言,它则会新建ServiceManager对应的Binder实体,并将该Binder实体设为全局变量。
1)binder驱动
/kernel/msm-4.9/drivers/android/*
2)servicemanager
/frameworks/native/cmds/servicemanager/*
3)libbinder
/frameworks/native/libs/binder/*
4)JAVA层
/frameworks/base/core/java/android/os/*
源码下载:
https://github.com/LineageOS
个人微信公众号:IngresGe
参考: