Binder从陌生到熟悉(上)

1,175 阅读12分钟

Binder从陌生到熟悉(中)

Binder从陌生到熟悉(下)

概念

什么是Binder

如果我得同事问了我一句,“Binder是什么啊”,我得脑海中可能出现这样几个答案

  • Android系统中进程间的通信机制(IPC)
  • 也是一个Android系统中处于Linux内核层的驱动设备
  • 在Android开发中,如果继承了Binder.java类,实现IBinder接口,可以获得跨进程通信的能力

Android中的多进程

那么Android系统中什么时候会用到多进程呢。我们在平时Android开发中,会创建出一些独立进程,如:WebView、大图浏览、音视频播放、推送等相关功能,同时Android系统服务中的打电话,闹钟等功能也是多进程的实现。如果你写的android应用实现了多进程,那么你的应用可以开辟更多的内存(每一个进程可以向虚拟机申请到的使用内存是有限的,多个进程可以获得更多的内存),可以获得风险隔离(把一些风险操作统一放在一个进程中,即使这个进程崩溃了,也不会影响到主进程)的特性,这些都是多进程的优势。

Linux中的进程间通信

Linux中已有的进程间通信手段包括

  • 管道(Pipe)及有名管道(named pipe):在创建时分配1个page大小的内存空间,缓存区大小比较有限
  • 信号(Signal)和跟踪(Trace):不适合用于信息交换,更适用于进程中断控制,如非法内存访问、杀死某个进程等
  • 共享内存(Share Memory):无需信息拷贝,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须靠各进程利用同步工具解决
  • 套接字(Socket):作为更通用的接口,传输效率低,主要作用于本机上不同进程间的低速通信或者跨网络的进程间通信
  • 报文(Message)队列(消息队列):信息拷贝两次,额外的CPU消耗;不适合频繁或者信息量大的通信
  • 信号量(Semaphore):常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段

既然Android是基于Linux内核的操作系统,那为什么google不直接采用以上几种进程的IPC方案却选择了Binder呢?我们来从以下几个角度对Binder和其他几个IPC方案来进行对比分析

  1. 从性能的角度:Socket作为一款通用接口,传输效率低、开销大,主要用于本机上不同进程间的低速通信或者跨网络的进程间通信。消息队列和管道采用存储-转发方式,即数据先从发送方的缓存区到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方的缓存区,至少经历了两次拷贝过程;Binder只需要一次拷贝;共享内存无需拷贝。Binder在性能上仅次于共享内存

  2. 从稳定性的角度:Binder是基于C/S架构的(客户端Client服务端Server组成的架构,客户端有什么需求,直接发给服务端去完成),架构清晰,客户端和服务端相对独立,稳定性较好。Socket同样支持C/S架构。而管道/消息队列/共享内存/信号量都不支持,虽然可以在这些底层机制上架设一套协议来实现Client-Server通信,但这样增加了系统的复杂性,在手机这种条件复杂,资源稀缺的环境下可靠性也难以保证

  3. 从安全性角度:传统的Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,APP来源广,因此手机的安全显得至关重要。传统的Linux IPC无任何保护措施,完全由上层协议来确保,只能由用户在数据包里填入UID/PID,可靠的身份标识只有由IPC机制本身在内核中添加,而且传统的IPC访问的接入点是开放的,无法建立私有通道。Android为每个安装好的应用程序分配了自己的UID,因而进程的UID是鉴别进程身份的重要标识,在C/S架构中,Android系统中对外只暴露客户端,客户端将任务发送给服务端,服务端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限申请弹框,让用户选择是否允许。同时 Binder 既支持实名 Binder(比如系统服务),又支持匿名 Binder(自己创建的服务),Binder的安全性更高

    Binder共享内存Socket
    性能需要拷贝一次无需拷贝需要拷贝两次
    稳定性基于C/S架构,易用性高 ,稳定性好控制复杂,易用性差 ,稳定性差基于C/S架构,作为一款通用接口,传输效率低,开销大 ,稳定性好
    安全性为每个APP分配UID,同时支持实名和匿名,安全性高依赖上层协议,访问接入点是开放的,不安全依赖上层协议,访问接入点是开放的,不安全

    基于上述原因,可知Binder是Android系统上层进程间通信的不二之选

    结构

进程间是如何通信的

两个进程A和B,进程A是无法之间操作或调用进程B中的代码的,因为Android系统中不同进程间是内存隔离的。内存被操作系统划分为两块,用户空间和内核空间。用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方。为了安全,他们是互相隔离的,即用户的程序崩溃了,也不会影响到内核。 CPU具有不同的操作模式,代表不同的级别,不同的级别具有不同的功能,在较低的级别中将禁止某些操作。Linux系统设计时利用了这种硬件特性,使用了两个级别,最高级别和最低级别,内核运行在最高级别(内核态R0),这个级别可以进行所有操作,而应用程序运行在较低级别(用户态R3),在这个级别,处理器控制着对硬件的直接访问以及对内存的非授权访问。

当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。

当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。

内核态和用户态有自己的内存映射,即自己的地址空间。不同进程的用户空间申请的虚拟内存,映射到独属进程的物理内存上面,是隔离的。内核空间申请的虚拟内存映射到同一块物理内存上,是共享的。用户空间的应用程序,如果想要请求系统服务,比如操作一个物理设备,或者映射一段设备空间的地址到用户空间,就必须通过系统调用来(操作系统提供给用户空间的接口函数)实现,而系统调用主要通过如下两个函数

copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间

传统IPC机制传输数据

传统的IPC机制传输数据相当于传呼机,进程A想给进程B发消息,需要先把消息告诉传呼中心,然后传呼中心再把信息发送到进程B的传呼机上面,这条信息发送了两次,也就是所谓的两次拷贝(只是举例,方便理解)。

Binder机制传输数据

Binder机制传输数据相当于寄快递,进程A想把包裹邮寄给进程B,需要先把包裹交给快递公司,然后快递公司把包裹放到旗下的快递柜里面,进程B直接在快递柜里面取出包裹。这个包裹就相当于数据,只是在进程A交给快递公司时发生了一次拷贝(只是举例,方便理解)。在这个过程中,快递柜这部分空间是快递公司和进程B共享的。

想了解Binder机制传输数据的原理,首先要先了解一个概念:内存映射

Linux通过将一个虚拟内存区域与一个磁盘(物理内存)上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)

MMAP的原理是这样的:

1.线程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域。

先在用户空间调用库函数mmap,并在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续虚拟地址作为内存虚拟映射区域,对此区域初始化并插入进程的虚拟地址区域链表或树中。

2.系统在内核空间调用内核函数mmap,实现文件物理地址和进程虚拟地址之间的一一映射关系。

3.进程发起对这片映射空间的访问

进程读写操作访问虚拟地址,查询页表,发现这一段地址并不在内存的物理页面上,因为虽然建立了映射关系,但是还没有将文件从磁盘移到内存中。由此发生缺页中断,内核请求从磁盘调入页面。调页过程先在交换缓存空间(swap cache)中查找,若没有则通过nopage函数把缺失页从磁盘调入内存。之后进程会对其做读写操作,若写操作改变了页面内容,一段时间后系统会自动回写脏页面到磁盘中。(修改过的脏页面不会立即更新到文件中,可以调用msync()来强制同步,写入文件)

Binder IPC 机制中涉及到的内存映射通过mmap()来实现,mmap() 是操作系统中一种内存映射的方法。映射关系建立后,用户对这块用户空间的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。这样一来,从发送方用户空间拷贝到内核空间缓存区的数据,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝。

Binder框架结构

1.Binder驱动

Linux 把所有的硬件访问都抽象为对文件的读写、设置,这一"抽象"的具体实现就是驱动程序。驱动程序充当硬件和软件之间的枢纽,提供了一套标准化的调用,并将这些调用映射为实际硬件设备相关的操作,对应用程序来说隐藏了设备工作的细节。

Linux 驱动设备分为三类,分别是字符设备、块设备和网络设备。字符设备就是能够像字节流文件一样被访问的设备。对字符设备进行读/写操作时,实际硬件的I/O操作一般也紧接着发生。字符设备驱动程序通常都会实现 openclosereadwrite系统调用,比如显示屏、键盘、串口、LCD、LED 等。

块设备指通过传输数据块(一般为512b1k)来访问的设备,比如硬盘、SD卡、U盘、光盘等。

网络设备是能够和其他主机交换数据的设备,比如网卡、蓝牙等设备。

字符设备中有一个比较特殊的 misc 杂项设备,设备号为 10,可以自动生成设备节点。Android 的AshmemBinder 都属于 misc 杂项设备

Binder 并不是 Linux 系统内核的一部分,得益于 Linux 的动态内核可加载模块Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信

对 Binder 机制来说,Binder驱动是 IPC 通信的路由器,负责实现不同进程间的数据交互,是 Binder 机制的核心;对 Linux 系统来说,Binder驱动是一个字符驱动设备,运行在内核空间,向上层提供 /dev/binder 设备节点及 openmmapioctl 等系统调用

2.ServiceManager

Service Manager充当的是Binder驱动的守护进程,类似于TCP通信中的DNS地位。我们会把相关的Binder注册到里面,最后会通过Service Manager这个服务去查找Binder的代理端。而实际上这个Service Manager在Andrioid Binder体系中,承当了Android系统中第一个注册进入Binder的服务

3.Clinet

本地端,Binder在整个IPC进程通信中,发出请求的一端。相当于C/S架构中的客户端的概念

4.Server 代理端,Binder在整个IPC进程通信中,响应请求的一端。相当于C/S架构中的服务端的概念