Binder,英文的意思是别针、回形针。我们经常用别针把两张纸”别“在一起,而在Android中,Binder用于完成进程间通信(IPC),即把多个进程”别“在一起。比如,普通应用程序可以调用音乐播放服务提供的播放、暂停、停止等功能。Binder工作在Linux层面,属于一个驱动,只是这个驱动不需要硬件,或者说其操作的硬件是基于一小段内存。从线程的角度来讲,Binder驱动代码运行在内核态,客户端程序调用Binder是通过系统调用完成的。
无论是Android系统,还是各种Linux衍生系统,各个组件、模块往往运行在各种不同的进程和线程内,这里就必然涉及进程/线程之间的通信。对于IPC(Inter-Process Communication, 进程间通信),Linux目前有一下这些IPC机制:
-
管道(Pipe)
管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。
- 管道是半双工的,数据只能向一个方向流动。需要双方通信时,需要建立起两个管道。
- 只能用于父子进程或兄弟进程之间(具有亲缘关系的进程)。比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。
- 管道只能在本地计算机中使用,而不可用于网络间的通信。
-
命名管道(FIFO)
命名管道是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了管道的弊端,他可以允许没有亲缘关系的进程间通信。
-
共享内存(Share Memory)
共享内存是多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建一个特殊地址范围,它将出现在该进程的地址空间中。其他进程可以将同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存的地址,如果一个进程向共享内存中写入数据,所做的改动将立刻被其他进程看到。
- 共享内存是IPC最快捷的方式,共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。
- 共享内存本身并没有同步机制,需要程序员自己控制。
-
内存映射(Memory Map)
内存映射是由一个文件到一块内存的映射,在此之后进程操作文件,就像操作进程空间里的内存地址一样。
-
套接字(Soket)
套接字机制不但可以在单机的不同进程间通信,而且可以在跨网机器间进程通信。
套接字的创建和使用与管道是有区别的,套接字明确的将客户端与服务器区分开来,可以实现多个客户端连到同一个服务器。
Android额外还有Binder IPC机制,Android OS中的Zygote进程的IPC采用的是Socket机制,在上层system server、media server以及上层App之间更多的是采用Binder。 IPC方式来完成跨进程间的通信。对于Android上层架构中,很多时候是在同一个进程的线程之间需要相互通信,例如同一个进程的主线程与工作线程之间的通信,往往采用的Handler消息机制。所以对于Android最上层架构而言,最常用的通信方式是:
-
Binder
Android中最常用的跨进程通信方式。
-
Socket
Socket通信方式也是C/S架构,比Binder简单很多。在Android系统中采用Socket通信方式的主要有:
- zygote:用于孵化进程,system_server创建进程是通过socket向zygote进程发起请求;
- installd:用于安装App的守护进程,上层PackageManagerService很多实现最终都是交给它来完成;
- lmkd:lowmemorykiller的守护进程,Java层的LowMemoryKiller最终都是由lmkd来完成;
- adbd:这个也不用说,用于服务adb;
- logcatd:这个不用说,用于服务logcat;
- vold:即volume Daemon,是存储类的守护进程,用于负责如USB、Sdcard等存储设备的事件处理。
等等还有很多,这里不一一列举,Socket方式更多的用于Android framework层与native层之间的通信。
-
Handler
主要用于线程之间的通信。
进程隔离是操作系统为了保护进程之间不相互干扰而设计的,避免进程A写入进程B的情况发生,其实现就是使用虚拟地址空间,两个进程虚拟地址不同,这样就可以防止A进程写入数据到B进程。也就是说,操作系统的不同进程之间,数据不共享,在每个进程看来,自己都独享了整个系统空间,完全不知道其他进程的存在,因此一个进程想要与另一个进程通信,需要某种系统机制才能完成。
Linux Kernel是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
对于Kernel这中高安全级别的功能,显然是不允许其它应用程序随便调用或访问的,所以需要对Kernel提供一定的保护机制,这个保护机制用来告诉那些应用程序,你只可以访问某些许可的资源,不许可的资源是不能访问的,于是操作系统就把kernel和上层的应用程序抽象的隔离开,分别称之为kenel space和user space,即内核空间和用户空间。
虽然从逻辑上抽离出用户空间和内核空间,但是不可避免的是,总有那么一些用户空间需要访问内核的资源:比如应用程序访问文件、网络这种,那么这种情况下该怎么处理?
用户空间访问内核空间的唯一方式就是系统调用,通过这个统一入口,所有的资源访问都是在内核的控制下执行,以免导致用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。
Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。
通过系统调用,用户空间可以访问内核空间,那么如果一个用户空间想与另外一个用户空间进行通信该怎么处理?
那就是让操作系统内核添加支持,传统的linux通信机制,比如socket、管道等都是内核的一部分,因此通过内核支持来实现进程间通信自然是没有问题的,但是binder并不是linux系统内核的一部分,那它是怎么做到访问内核空间的呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)(尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的)。
从进程角度来看IPC机制,每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。
Binder作为Android系统提供的一种IPC机制。首先一个问题就是为什么Linux已经有那么多IPC通信的机制,Android还要用Binder。
接下来正面回答这个问题,从5个角度来展开对Binder的分析:
-
从性能的角度 :
对比与Linux的通信机制,socket是一个通用接口,导致其传输效率低、开销大。管道和消息队列因为采用存储转发的方式,所以至少需要拷贝2次数据,效率低。而共享内存虽然在传输时没有拷贝数据,但其控制机制复杂(比如跨进程通信时,需获取对方进程的pid,需要多种机制协同操作)。如果在APP级别,多拷贝一次或许没什么问题,但是如果上升到系统级别,系统内部通信频次是极高的,如果效率不够,用户体验会很差。
具体原因如下,我们先来看看传统的 IPC 方式中,进程之间是如何实现通信的。
通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copyfromuser() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copytouser() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。
这种传统的 IPC 通信方式有两个问题:
- 性能低下,一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝;
- 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
那Binder是怎么操作的呢?这就需要知道 Linux 下的另一个概念:内存映射。
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。
一次完整的 Binder IPC 通信过程通常是这样:
-
首先 Binder 驱动在内核空间创建一个数据接收缓存区;
-
接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
-
发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间。
Client(数据发送端)先从自己的用户进程空间把IPC数据通过copy_from_user()拷贝到内核空间。而Server端(数据接收端)与内核共享数据(mmap到同一块物理内存),不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。
-
从稳定性的角度 Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。
仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是:
-
从安全的角度 Linux的IPC机制在本身的实现中,并没有安全措施,得依赖上层协议来进行安全控制。而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐私数据、后台造成手机耗电等等问题。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。
针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。
Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。
-
从语言层面的角度 大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。
-
从公司战略的角度
总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。
而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下开源与商业化共存的一个成功典范。
综合上述5点,可知Binder是Android系统上层进程间通信的不二选择。
到这里又迷糊了,你上面说了这么多Binder这么好,那为啥Android里面还有地方用Socket?SystemServer和Zygote之间的通信为啥不用Binder?
SystemServer和Zygote之间通信使用Socket的原因是为了要解决fork的问题。UNIX上C++程序设计守则3中规定:多线程程序里不准使用fork。
而Binder通讯是需要多线程操作的,代理对象对Binder的调用是在Binder线程,需要再通过Handler调用主线程来操作。比如AMS与应用进程通讯,AMS的本地代理IApplicationThread通过调用ScheduleLaunchActivity,调用到的应用进程ApplicationThread的ScheduleLaunchActivity是在Binder线程,需要再把参数封装为一个ActivityClientRecord,sendMessage发送给H类(主线程Handler,ActivityThread内部类)。主要原因是害怕父进程binder线程有锁,然后子进程的主线程一直在等待其子线程(从父进程拷贝过来的子进程)的资源,但是其实父进程的子进程并没有被拷贝过来,造成死锁。
所以fork不允许存在多线程。而非常巧的是Binder通讯偏偏就是多线程,所以干脆父进程(Zygote)这个时候就不使用Binder线程了。
所以也并非Linux现有的IPC机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种Linux的IPC机制都有存在的价值,同时在Android系统中也依然采用了大量Linux现有的IPC机制,根据每类IPC的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制,Android中的Kill Process采用的signal(信号)机制等等。而Binder更多则用在system_server进程与上层App层的IPC交互。
-
当年Andy Rubin有个公司Palm做掌上设备的就是当年那种PDA有个系统叫PalmOS后来palm被收购了以后 Andy Rubin创立了Android
-
Palm收购过一个公司叫Be里面有个移动系统叫BeOS,进程通信自己写了个实现叫OpenBinder由一个叫Dianne Hackbod的人开发并维护后来Binder也被用到了PalmOS里
-
Android创立了以后Andy从Palm带走了一大批人,其中就有Dianne。Dianne成为安卓系统总架构师。
-
如果你是她,你会选择用Linux已有的进程通信手段吗? 不会,要不当年也不会搞个新东西出来
-
重写一个新东西?也不会,binder反正是自己写的开源库
-
用binder?已经被两个公司用过而且是自己写的可靠放心
我是她我就用binder。
你可以看到 如果当年Dianne没有加入Be或者Be没有被收购 ,又或者Dianne没有和Andy加入Android 那Android也不一定会用binder。
在Android系统的Binder机制中,由一系统组件组成,分别是Client、Server、Service Manager和Binder驱动程序,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间。其中Service Manager和Binder驱动由系统提供,而Client、Server由应用程序来实现。其中,核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与Binder驱动的交互来间接的实现跨进程通信。
如果统观Binder中的各个组成元素,就会惊奇的发现它和TCP/IP网络有很多相似之处:
- Binder驱动 -> 路由器
- Service Manager -> DNS
- Binder Client -> 客户端
- Binder Server -> 服务端
TCP/IP中一个典型的服务连接过程(比如客户端通过浏览器方位Google主页):
在这个简化的流程图中共出现了四种角色,即Client、Server、DNS和Router。它们的目标是让Client和Server建立连接,主要分为如下几个步骤。
-
Client向DNS查询Google.com的IP地址。
显然,Client一定要先知道DNS的IP地址,才有可能向它发起查询。DNS的IP设置是在客户端接入网络前就完成了的,否则Client将无法正常访问域名服务器。当然,如果Client已经知晓了Server的IP,那么完全可以跨越这一步而直接与Server连接。比如Windows操作系统就提供了一个hosts文件,用于查询常用网址域名与其IP地址的对应关系。当用户需要访问某个网址时,系统会先从这个文件中判断是否已经存在有这个域名的对应IP。如果有就不需要再大费周折地向DNS查询了,从而加快访问速度。
-
DNS将查询结果返回Client。
因为Client的IP地址对于DNS也必须是可知的,这些信息都会被封装在TCP/IP包中。
-
Client发起连接。
-
Client在得到Google.com的IP地址后,就可以据此来向Google服务器发起连接了。
在这一些列流程中,我们并没有特别提及Router的作用。因为它所担负的责任是将数据包投递到用户设定的目标IP中,即Router是整个通信结构中的基础。
从这个典型的TCP/IP通信中,我们还能得到什么提示呢?
首先,在TCP/IP参考模型中,对于IP层及以上的用户来说,IP地址是他们彼此沟通的凭证,任何用户在整个互联网中的IP标志符都是唯一的。
其次,Router是构建一个通信网络的基础,它可以根据用户填写的目标IP正确的把数据包发送到位。
最后DNS角色并不是必需的,它的出现是为了帮助人们使复杂难记的IP地址与可读性更强的域名建立关联,并提供查询功能。而客户端能使用DNS的前提是它已经正确配置了DNS服务器的IP地址。
Binder的本质目标用一句话来描述,就是进程1(客户端)希望与进程2(服务端)进行互访。但因为它们之间是跨进程(跨网络)的,所以必须借助于Binder驱动(路由器)来把请求正确投递到对方所在进程(网络)中。而参与通信的进程们需要持有Binder“颁发”的唯一标志(IP地址)。和TCP/IP网络类似,Binder中的DNS也并不是必需的,前提是客户端能记住它要访问的进程的Binder标志(IP地址)。而且要特别注意这个标志是动态IP,这就意味着即使客户端记住了本次通信过程中目标进程的唯一标志,下一次访问仍然需要重新获取,这无疑加大了客户端的难度。DNS的出现可以完美的解决这个问题,用于管理Binder标志与可读性更强的域名间的对应关系,并向用户提供查询功能。既然Service Manager是DNS,那么它的IP地址是什么呢? Binder机制对此作了特别规定,Service Manager在Binder通信过程中的唯一标志永远都是0.
-
Server
一个Binder服务端实际上就是一个Binder类的对象,该对象一旦创建,内部就启动一个隐藏线程。该线程接下来会接收Binder驱动发送的消息,收到消息后,会执行到Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务代码。因此,要实现一个Binder服务就必须重载onTransact()方法。
可以想象,重载onTransact()函数的主要内容是把onTransact()函数的参数转换为服务函数的参数,而onTransact()函数的参数来源是客户端调用transact()函数时传入的,因此,如果transact()有固定格式的输入,那么onTransact()就会有固定格式的输出。
每个Server进程在启动时会创建一个binder线程池,并向其中注册一个Binder线程;之后Server进程也可以向binder线程池注册新的线程,或者Binder驱动在探测到没有空闲binder线程时会主动向Server进程注册新的的binder线程。对于一个Server进程有一个最大Binder线程数限制,默认为16个binder线程,例如Android的system_server进程就存在16个线程。对于所有Client端进程的binder请求都是交由Server端进程的binder线程来处理的。
-
Binder驱动
和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,不提供read(),write()接口,因为ioctl()灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用write()和read()。Binder驱动的代码位于linux目录的drivers/misc/binder.c中。
任何一个服务端Binder对象被创建时,同时会在Binder驱动中创建一个mRemote对象,该对象的类型也是Binder类。客户端要访问远程服务时,都是通过mRemote对象。
在Binder驱动层,每个接收端进程都有一个todo队列,用于保存发送端进程发送过来的binder请求,这类请求可以由接收端进程的任意一个空闲的binder线程处理;接收端进程存在一个或多个binder线程,在每个binder线程里都有一个todo队列,也是用于保存发送端进程发送过来的binder请求,这类请求只能由当前binder线程来处理。binder线程在空闲时进入可中断的休眠状态,当自己的todo队列或所属进程的todo队列有新的请求到来时便会唤醒,如果是由所需进程唤醒的,那么进程会让其中一个线程处理响应的请求,其他线程再次进入休眠状态。
-
Client
Server向ServiceManager注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向ServiceManager请求访问某个Binder:我申请获得名字叫张三的Binder的引用。ServiceManager收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个Binder对象现在有了两个引用:一个位于ServiceManager中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用Binder实体就不会被释放掉。通过以上过程可以看出,ServiceManager象个火车票代售点,收集了所有火车的车票,可以通过它购买到乘坐各趟火车的票-得到某个Binder的引用。
并不是所有Binder都需要注册给ServiceManager广而告之的。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder实现。由于这个Binder没有向ServiceManager注册名字,所以是个匿名Binder。Client将会收到这个匿名Binder的引用,通过这个引用向位于Server中的实体发送请求。匿名Binder为通信双方建立一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用,向该Binder发送请求。
客户端想要访问远程服务,必须获取远程服务在Binder对象中对应的mRemote引用,获得该mRemote对象后,就可以调用其transact()方法,调用该方法后,客户端线程进入Binder驱动,Binder驱动就会挂起当前线程,并向远程服务发送一个消息,消息中包含了客户端传进来的包裹。服务端拿到包裹后,会对包裹进行拆解,然后执行指定的服务函数,执行完毕后,再把执行结果放入客户端提供的reply包裹中。然后服务端向Binder驱动发送一个notify的消息,从而使得客户端线程从Binder驱动的代码区返回到客户端代码区。transact()的最后一个参数的含义是执行IPC调用的模式,分为两种:一种是双向,用常量0表示,其含义是服务端执行完指定的服务后返回一定的数据。另一种是单向,用常量1表示,其含义是不返回任何数据。最后,客户端就可以从reply中解析返回的数据了,同样,返回包裹中包含的数据也必须是有序的,而且这个顺序也必须是服务端和客户端事先约定好的。
从这里可以看出,对应用程序开发员来说,客户端似乎是直接调用远程服务对应的Binder,而事实上则是通过Binder驱动进行了中转。即存在两个Binder对象,一个是服务端的Binder对象,另一个则是Binder驱动中的Binder对象,所不同的是Binder驱动中的对象不会再额外产生一个线程。
-
ServiceManager
ServiceManager是Binder IPC通信过程中的守护进程,本身也是一个Binder服务,但并没有采用libbinder中的多线程模型来与Binder驱动通信,而是自行编写了binder.c直接和Binder驱动来通信,并且只有一个循环binder_loop来进行读取和处理事务,这样的好处是简单而高效。ServiceManager是由init进程通过解析init.rc文件而创建的。
ServiceManager本身的工作很简单:注册服务、查询服务、列出所有服务,启动一个死循环来解析Binder驱动读写动作,进行事务处理。ServiceManager用于管理系统中的各种服务。架构图如下所示:
可以看出无论是注册服务和获取服务的过程都需要ServiceManager(与DNS类似),需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程。Binder机制使用的是代理模式,在Server端的对象是实际对象,其他各个进程端所持有的都是Server端对象的Proxy代理对象,ServiceManager中会有一个类似map的结构,会存储Server端的信息,当Server端进程初始化时,会向ServiceManager中注册自己的信息。当client端想要访问时,也需要先向ServiceManager进行查询,当进行通信时,数据会流经内核空间中的binder驱动,此时驱动会对要传递的数据做转换。
和DNS类似,ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址。Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给ServiceManager,通知ServiceManager注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及ServiceManager对实体的引用,将名字及新建的引用打包传递给ServiceManager。ServiceManager收数据包后,从中取出名字和引用填入一张查找表中。
细心的读者可能会发现其中的蹊跷:ServiceManager是一个进程,Server是另一个进程,Server向ServiceManager注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder的实现比较巧妙:预先创造一只鸡来孵蛋:ServiceManager和其它进程同样采用Binder通信,ServiceManager是Server端,有自己的Binder对象(实体),其它进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。ServiceManager提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成ServiceManager时Binder驱动会自动为它创建Binder实体(这就是那只预先造好的鸡)。其次这个Binder的引用在所有Client中都固定为0(handle=0)而无须通过其它手段获得。也就是说,一个Server若要向ServiceManager注册自己Binder就必须通过0这个引用号和ServiceManager的Binder通信。类比网络通信,0号引用就好比域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的Client是相对ServiceManager而言的,一个应用程序可能是个提供服务的Server,但对ServiceManager来说它仍然是个Client。
图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder驱动进行交互的。
ServiceManger集中管理系统内的所有服务,通过权限控制进程是否有权注册服务,通过字符串名称来查找对应的Service; 由于ServiceManger进程建立跟所有向其注册服务的死亡通知, 那么当服务所在进程死亡后, 会只需告知ServiceManager. 每个Client通过查询ServiceManager可获取Server进程的情况,降低所有Client进程直接检测会导致负载过重。
ServiceManager启动流程:
- 打开binder驱动,并调用mmap()方法分配128k的内存映射空间:binder_open();
- 通知binder驱动使其成为守护进程:binder_become_context_manager();
- 验证selinux权限,判断进程是否有权注册或查看指定服务;
- 进入循环状态,等待Client端的请求:binder_loop()。
- 注册服务的过程,根据服务名称,但同一个服务已注册,重新注册前会先移除之前的注册信息;
- 死亡通知: 当binder所在进程死亡后,会调用binder_release方法,然后调用binder_node_release.这个过程便会发出死亡通知的回调.
ServiceManager最核心的两个功能为查询和注册服务:
- 注册服务:记录服务名和handle信息,保存到svclist列表;
- 查询服务:根据服务名查询相应的的handle信息。
-
首先,一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
-
Server通过驱动向ServiceManager中进行服务注册,表明可以对外提供服务。ServiceManager有一个全局的service列表svcinfo,用来缓存所有服务的handler和name。驱动为这个Binder创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
-
客户端与服务端通信,需要拿到服务端的对象,由于进程隔离,客户端拿到的其实是服务端的代理,也可以理解为引用。客户端通过Client 通过名字,在 Binder 驱动的帮助下从ServiceManager的svcinfo中查找服务,ServiceManager返回服务的代理。
-
Server进程启动之后,会进入中断等待状态,等待Client的请求。
-
当Client需要和Server通信时,会通过BinderProxy将我们的请求参数发送给 内核,通过共享内存的方式使用内核方法 copy_from_user() 将我们的参数先拷贝到内核空间,这时我们的客户端进入等待状态。
-
Binder驱动收到请求之后,会唤醒Server进程,Binder驱动向服务端的 todo 队列里面插入一条事务,执行完之后把执行结果通过 copy_to_user() 将内核的结果拷贝到用户空间。
-
Server进程解析出请求内容,并将回复内容发送给Binder驱动。
-
接着,Binder驱动收到回复之后,唤醒Client进程。Binder驱动还会反馈信息给Client,告诉Client:它发送给Binder驱动的请求,Binder驱动已经完成。
-
接着,Binder驱动还会反馈信息给Server,告诉Server:它发送给Binder驱动的回复,Binder驱动已经收到。
-
Server将回复发送成功之后,再次进入等待状态,等待Client的请求。
-
最后,Binder驱动将回复转发给Client。
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内存占用分配:
用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间,这也是Binder进程间通信效率高的核心机制所在。
虚拟进程地址空间(vm_area_struct)和虚拟内核地址空间(vm_struct)都映射到同一块物理内存空间。当Client端与Server端发送数据时,Client(作为数据发送端)先从自己的进程空间把IPC通信数据copy_from_user
拷贝到内核空间,而Server端(作为数据接收端)与内核共享数据,不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。一般地做法,需要Client端进程空间拷贝到内核空间,再由内核空间拷贝到Server进程空间,会发生两次拷贝。
对于进程和内核虚拟地址映射到同一个物理内存的操作是发生在数据接收端,而数据发送端还是需要将用户态的数据复制到内核态。到此,可能有读者会好奇,为何不直接让发送端和接收端直接映射到同一个物理空间,那样就连一次复制的操作都不需要了,0次复制操作那就与Linux标准内核的共享内存的IPC机制没有区别了,对于共享内存虽然效率高,但是对于多进程的同步问题比较复杂,而管道/消息队列等IPC需要复制2两次,效率较低。这里就不先展开讨论Linux现有的各种IPC机制跟Binder的详细对比,总之Android选择Binder的基于速度和安全性的考虑。
Binder驱动是Android专用的,但底层的驱动架构与Linux驱动一样。binder驱动在以misc设备进行注册,作为虚拟字符设备,没有直接操作硬件,只是对设备内存的处理。主要是驱动设备的初始化(binder_init),打开 (binder_open),映射(binder_mmap),数据操作(binder_ioctl),binder_ioctl()函数负责在两个进程间收发IPC数据和IPC reply数据。
用户态的程序调用Kernel层驱动是需要陷入内核态,进行系统调用(syscall
),比如打开Binder驱动方法的调用链为: open-> __open() -> binder_open()。 open()为用户空间的方法,__open()便是系统调用中相应的处理方法,通过查找,对应调用到内核binder驱动的binder_open()方法,至于其他的从用户态陷入内核态的流程也基本一致。
简单说,当用户空间调用open()方法,最终会调用binder驱动的binder_open()方法;mmap()/ioctl()方法也是同理。
参考: https://www.zhihu.com/question/39440766/answer/93550572
https://zhuanlan.zhihu.com/p/35519585
- 邮箱 :charon.chui@gmail.com
- Good Luck!