基础概念
- binder 是 Android 中主要的跨进程通信方式,binder 驱动和 service manager 分别相当于网络协议中的路由器和 DNS,并基于 mmap 实现了 IPC 传输数据时只需一次拷贝。
- binder 包括 BinderProxy、BpBinder 等各种 Binder 实体,以及对 binder 驱动操作的 ProcessState、IPCThreadState 封装,再加上 binder 驱动内部的结构体、命令处理,整体贯穿 Java、Native 层,涉及用户态、内核态,往上可以说到 Service、AIDL 等,往下可以说到 mmap、binder 驱动设备,是相当庞大、繁琐的一个机制。
- 从不同的层面分析Binder有不同的解释
- 机制:Binder是一种进程间通信机制
- 驱动:Binder是一个虚拟物理驱动设备
- 应用层:Binder是一个能发起通信的Java类
进程隔离
进程隔离简单的说就是 Linux 操作系统设计的一种机制,使进程之间不能共享数据,保持各自数据的独立性,即A进程不能访问B进程数据,同理B进程也不能访问A进程数据。通过虚拟内存技术,达到 Linux 进程中数据不能共享,从而保持独立的功能。所以,Linux 进程之间要进行数据交互就得采用特殊的通信机制,即 IPC 通信!
进程隔离的优点:
- 稳定性:进程独立,保证一个进程的崩溃不会引起其他进程的崩溃
- 安全性:保护各个进程数据的独立性
Linux内存管理单元
内核空间&用户空间
- 内核空间
- Linux内核的运行空间,Linux操作系统和驱动程序均运行在内核空间,其中虚拟内存0XC0000000~0XFFFFFFFF供内核使用,称为内核空间
- 用户空间
- 用户程序的运行空间,运行这App,其中虚拟内存地址0X000000~0XBFFFFFFF供各个进程使用,称为用户空间
- 为什么区分内核空间和用户空间
- 隔离应用程序和系统程序,使得应用程序的崩溃不会造成内核的崩溃,即应用的崩溃不会导致程序的死机崩溃
- 每个应用程序活进程都会有自己的特定地址和私有数据空间,保证程序之间不会相互影响
- 提供了系统的稳定性
内存管理单元
MMU(Memory Management Unit)主要用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权、多任务多进程操作系统。
- 相关概念
- 地址范围
- 指处理器能够产生的地址集合
- 物理地址&物理地址空间
- 物理地址:CPU地址总线传来的地址,由硬件电路控制其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等)。在程序指令中的虚拟地址经过段映射和页面映射后,就生成了物理地址,这个物理地址被放到CPU的地址线上。
- 物理地址空间:一部分给物理RAM(内存)用,一部分给总线用,这是由硬件设计来决定的,因此在32 bits地址线的x86处理器中,物理地址空间是2的32次方,即4GB,但物理RAM一般不能上到4GB,因为还有一部分要给总线用(总线上还挂着别的 许多设备)。在PC机中,一般是把低端物理地址给RAM用,高端物理地址给总线用。
- 虚拟地址&虚拟地址空间
- 虚拟地址:现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需要MMU(Memory Management Unit)的支持。MMU通常是CPU的一部分,如果处理器没有MMU,或者有MMU但没有启用,CPU执行单元发出的内存地址将直接传到芯片引脚上,被 内存芯片(物理内存)接收,这称为物理地址(Physical Address),如果处理器启用了MMU,CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址。
- 虚拟地址空间:操作系统会给每个进程分配一个虚拟地址空间(vitural address),每个进程包含的栈、堆、代码段这些都会从这个地址空间中被分配一个地址,这个地址就被称为虚拟地址。底层指令写入的地址也是虚拟地址
- 地址范围
- 页的基本结构
为了CPU的高效执行以及方便的内存管理,每次需要拿一个页的代码。这个页,指的是一段连续的存储空间(常见的是4Kb),也叫作块。- 在任何时刻,虚拟页都是以下三种状态中的一种:
- 未分配的:VM系统还未分配的页(或者未创建)。未分配的页还没有任何数据与代码与他们相关联,因此也就不占用任何磁盘。
- 缓存的: 当前已缓存在物理内存中的已分配页
- 未缓存的:未缓存在物理内存中的已分配页
- 在任何时刻,虚拟页都是以下三种状态中的一种:
- 页表的基本结构
页表实际上就是一个数组。这个数组存放的是一个称为页表条目(PTE)的结构。虚拟地址空间的每一个页在页表中,都有一个对应的页表条目(PTE)。虚拟页地址(首地址)翻译的时候就是查询的各个虚拟页在页表中的PTE,从而进行地址翻译的。现在假设每一个PTE都有一个有效位和一个n位字段的地址。- 有效位:表示对应的虚拟页是否缓存在了物理内存中。0表示未缓存。1表示已缓存。
- n位地址字段:如果未缓存(有效字段为0),n位地址字段不为空的话,这个n位地址字段就表示该虚拟页在磁盘上的起始的位置。如果这个n位字段为空,那么就说明该虚拟页未分配。如果已缓存(有效字段为1),n位地址字段肯定不为空,它表示该虚拟页在物理内存中的起始地址。
关键说明
- Android为什么使用binder进行进程通信
- Linux进程通信的方式以及弊端
通信方式 | 效率 | 安全 | 模型 |
---|---|---|---|
管道 | 低(两次copy) | 安全 | 支持1对1 |
共享内存 | 最高(内存共享) | 不安全 | 支持N对N |
Socket | 低 | 不安全 | C/S |
File | 低(文件读写) | 不安全 | 支持N对N |
- Binder&共享内存&Socket对比
Binder | 共享内存 | Socket | |
---|---|---|---|
性能 | 一次copy(copy_from_user) | 无需copy | 两次copy(copy_from_user,copy_to_user) |
特点 | 基于C/S架构,易用性高 | 控制复杂,易用性低 | 基于C/S架构,易用性低 |
安全性 | 为每个App分配UID,同时支持实名和匿名 | 依赖上层协议,访问接入点是开放的,不安全 | 依赖上层协议,访问接入点是开放的,不安全 |
进程通信
-
Linux进程通信流程
-
Binder进程通信流程
关键问题
- Binder如何做到一次拷贝
- 首先Binder驱动在内核空间创建一个数据接收缓存区;
- 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
- 发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信
- mmap原理
- 进程启动后会在虚拟内存空间中串讲虚拟映射区域,即进程在用户空间调用库函数mmap (void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)😉,找到满足需求的连续的虚拟地址,并对该虚拟区进行初始化
- 调用内核空间的系统调用函数mmap(不同于用户空间函数)实现文件物理地址和进程虚拟地址的一一映射关系
- 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
- Binder传输数据的大小限制是多少:1M-8K
- ProcessState.cpp中初始化BInder服务时限制了其大小为1MB-4KB*2 (pagesize是申请物理内存的最小单元,大小为4K)
- 理论上这个限制是可以重新设置的,可以通过调用binder_open和binder_mmap进行修改大小限制,但是会导致Binder驱动的重启,因为APP和其他应用,AMS,WMS的交互可都是依赖于Binder通信,重启必会引起其他不可预估的问题,所以不建议进行该操作
//ProcessState.cpp中初始化BInder服务
#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)//这里的限制是1MB-4KB*2ProcessState::ProcessState(const char *driver)
{if (mDriverFD >= 0) {// mmap the binder, providing a chunk of virtual address space to receive transactions.// 调用mmap接口向Binder驱动中申请内核空间的内存mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);if (mVMStart == MAP_FAILED) {// *sigh*ALOGE("Using %s failed: unable to mmap transaction memory.\n", mDriverName.c_str());close(mDriverFD);mDriverFD = -1;mDriverName.clear();}}
}
- AIDL生成java类的细节
- 其中Stub类继承自Binder,内部有个onTransact方法,当跨进程远程调用服务端方法的时候,这个方法会被调用,在这个方法里会判断要请求的是哪个方法,然后调用服务端中这个Stub类的实现类中的方法;并将要返回的结果写入Parcel对象中
- Proxy代理类是用于代理Binder对象进行跨进程访问,当调用代理类的方法时,内部会先将要请求的参数写入Parcel对象中,然后调用Binder的transact方法进行远程调用并返回结果
- 当调用Binder的transact方法后,底层会通过BpBinder封装数据后转发远程调用请求,然后将数据拷贝到内核层,通过Binder驱动转发给服务端,服务端进程那边会通过BbBinder对数据进行解析,最终调用服务端Stub实现类里的onTransact方法
- Android APP有多少Binder线程,是固定的么
- app启动时在创建进程时默认会创建一个Binder主线程在运行,如果App中定义个其他服务在独立进程中,每个进程都至少会启动一个Binder主线程,后面根据跨进程通信请求次数,Binder会自动调成线程个数
- 最大的Binder线程也不是固定的,在ProcessState类中定义的默认最大线程个数是15个,这15个(不包括主Binder线程和将当前线程加入到Binder线程池中的),这个最大线程个数是可以修改的,比如SystemServer中就将线程池线程最大个数改成了31个
- bindService启动Service与Binder服务实体的流程
- 客户端调用bindService方法后,会跨进程调用AMS方法去查找要启动服务的信息,判断对应的服务所在进程是否已经启动,如果还没有则先通知Zygote启动进程
- 进程启动后会检查对应的Service是否已经创建,如果没有的话会通知ActivityThread先创建服务,创建完了之后会调用它的生命周期方法onCreate和onBind,在onBind方法中服务端会实现Stub类,这个Stub类继承自Binder
- 然后Service所在进程会将Binder对象返回给AMS,ANS则会回调客户端的ServiceConnection接口的onServiceConnected方法,并把服务端onBind方法返回的Binder对象返回给客户端客户端拿到Binder对象后调用asInterface方法,这个方法里会根据是否跟服务端在一个进程中,来决定是返回服务端接口本身,还是返回支持跨进程通信的代理类Proxy
- 接着客户端就可以直接调用Service提供的方法了
- 为什么内核有内核缓存区还要有个数据接收缓存区?
- 因为内核缓冲区是一直就存在的,以往的跨进程通信就是先从发送方拷贝到内核缓存区,然后再拷贝到接收方,所以需要两次拷贝,Binder的出现创建了一块数据接收缓存区,通过mmap将接收方和系统内核缓存区连接起来,从而减少了一次拷贝
- Binder如何找到目标进程
- binder服务会事先在Binder驱动的红黑树中注册结点,当跨进程通信时Binder驱动会先从红黑树中查找目标Binder所在结点,从结点中获取进程、线程信息,然后去唤醒目标进程、线程