文章

01 Binder基础

01 Binder基础

Binder 基础

Binder 机制介绍

Binder 源自 Be Inc 公司开发的OpenBinder框架,后来该框架转移的 Palm Inc,由 Dianne Hackborn 主导开发。OpenBinder 的内核部分已经合入 Linux Kernel 3.19。

Android Binder 是在 OpneBinder 上的定制实现。原先的 OpenBinder 框架现在已经不再继续开发,可以说 Android 上的 Binder 让原先的 OpneBinder 得到了重生。

Binder 是 Android 系统中大量使用的 IPC(Inter-process communication,进程间通讯)机制。无论是应用程序对系统服务的请求,还是应用程序自身提供对外服务,都需要使用到 Binder。

因此,Binder 机制在 Android 系统中的地位非常重要,可以说,理解 Binder 是理解 Android 系统的绝对必要前提。

Binder 是什么?

Binder 是 Android 最主要的 IPC 通信机制。

  • IPC的角度看,Binder 是 Android 中的一种跨进程通信的机制,Linux 中没有,Android 独有;
  • Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
  • Client 进程的角度看,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理
  • 传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。

Linux 传统的 IPC 机制

在 Unix/Linux 环境下,传统的 IPC 机制包括:

  • 管道
  • 消息队列
  • 共享内存
  • 信号量
  • Socket

管道 半双工,单向的,一般是在父子进程之间使用。
使用 pipe(fds) 创建文件对应的管道,然后使用 write、read、close 方法操作这个管道。

低版本 Looper::Looper

共享内存 很快,0 次拷贝,进程之间无需存在亲缘关系

MemoryFile 使用了 mmap 映射了匿名共享内存

信号 单向的,发出去之后怎么处理是别人的事,知道进程 pid 就能发信号,也可以一次给一群进程发信号,但是只能带个信号,不能带别的参数

Process.killProcess 发送了 SIGNAL_KILL 信号; Zygote 启动子进程后,需要关注子进程退出了没有,如果子进程退出了,Zygote 需要发送 SIGCHLD 信号及时把它回收掉。

Socket 全双工,即可读又可写,两个进程之间无需存在亲缘关系

Zygote 孵化应用进程

为什么不用已有的 IPC 机制,而是新起一个新的 Binder

性能好:Binder 的传输效率和可操作性很好

传输效率主要影响因素是内存拷贝次数,拷贝次数越少,传说速率越高。

  1. 消息队列、Socket 和管道,数据先从发送方的缓存区拷贝到内核开辟的缓存区,再从内核缓存区拷贝到接收方的缓存区,需要 2 次拷贝
  2. 共享内存呢,虽然使用它进行 IPC 通信时进行的内存拷贝次数是 0。但是共享内存操作复杂,也将它排除。
  3. 采用 Binder 机制的话,则只需要经过 1 次内存拷贝即可。即从发送方的缓存区拷贝到内核的缓存区,而接收方的缓存区与内核的缓存区是映射到同一块物理地址的,因此只需要 1 次拷贝即可。

稳定性好

  • 共享内存性能优于 Binder,但共享内存需要处理并发同步问题,容易出现死锁和资源竞争,稳定性较差
  • Socket 虽然基于 C/S 架构,但是它主要是用于网络间的通信,传输效率较低
  • Binder 基于 C/S 架构,Server 端和 Client 端相对独立,稳定性较好

安全性高:Binder 机制的安全性很高

传统 IPC 没有任何安全措施,完全依赖上层协议来确保。传统 IPC 的接收方无法获得对方进程可靠的 UID/PID(用户 ID/进程 ID),从而无法鉴别对方身份。

而 Binder 机制则为每个进程分配了 UID/PID 来作为鉴别身份的标示,并且在 Binder 通信时会根据 UID/PID 进行有效性检测。在实际项目中可能一些敏感的服务,不希望被乱用就可以通过 UID/PID 鉴权的方式来防护。

为什么说 Binder 是安全的? 在数据传输过程中有身份的校验,通过 UID、PID 进行校验

Binder 整体架构

Binder 整体架构如下所示:

image.png

下面的这张图也可以参考:

rfbhs

从图中可以看出,Binder 的实现分为这么几层:

  • Framework 层
    • Java 部分
    • JNI 部分
    • C++ 部分
  • 驱动层

驱动层位于 Linux 内核中,它提供了最底层的数据传递,对象标识,线程管理,调用过程控制等功能。驱动层是整个 Binder 机制的核心

Framework 层以驱动层为基础,提供了应用开发的基础设施。

Framework 层既包含了 C++ 部分的实现,也包含了 Java 部分的实现。为了能将 C++ 的实现复用到 Java 端,中间通过 JNI 进行衔接。

  • Java 层 有一套 binder C/S 架构,通过 JNI 技术,与 Native 的 Binder 对应,Java 层的 Binder 的功能最终都是交给 Native 的 Binder 来完成
  • Native 层有一整套完整的 Binder 通信的 C/S 架构,BPBinder 作为 Client,BBinder 作为 Server

Binder 通信模型 C/S 架构

” 进程间 “ 通讯就至少牵涉到两个进程,Binder 框架是典型的 C/S 架构。在下文中,我们把服务的请求方称之为 Client,服务的实现方称之为 Server。

Client 对于 Server 的请求会经由 Binder 框架由上至下传递到内核的 Binder 驱动中,请求中包含了 Client 将要调用的命令和参数。请求到了 Binder 驱动之后,在确定了服务的提供方之后,会再从下至上将请求传递给具体的服务。整个调用过程如下图所示:

image.png

对网络协议有所了解的读者会发现,这个数据的传递过程和网络协议是如此的相似。

x87o9

Binder 模型中的组件: Binder 是基于 C/S 架构的,由一系列组件组成,包括 Client、Server、ServiceManager 和 Binder 驱动; 其中 Client、Server 和 ServiceManager 运行在用户空间,Binder 驱动运行在内核空间; 其中 ServiceManager 和 Binder 驱动由系统提供,Client 和 Server 由应用程序来实现; Client、Server 和 ServiceManager 都是通过系统调用 open、mmap 和 ioctl 来访问设备文件/dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。

  • Server 提供服务的进程
  • Client 使用服务的进程
  • ServiceManager 管理 Service 的注册和查询(将字符串形式的 Binder 名字转化成 Client 中对该 Binder 的引用)
  • Binder 驱动 一个虚拟设备驱动,工作在内核态,提供 open、mmap 和 ioctl 等标准文件操作;Binder 驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数管理,数据包在进程之间的传递和交换等一系列底层支持。

Binder 通信过程:

  1. 一个进程使用 BINDER_SETCONTENT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager
  2. Server 注册 Server 进程通过 Binder 驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。Binder 驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManager 将其填入查找表

注册的过程就是向 Binder 驱动的全局链表 binder_procs 中插入服务端的信息,然后向 service_manager 的 svcinfo 列表中缓存一下注册的服务

  1. Client 进程通过名字在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信

3lzty

ServiceManager

见: [[02 Binder基础 -大管家 ServiceManager]]

Binder 驱动层

Binder 驱动不是 Linux 内核的一部分,它怎么做到能访问内核空间的呢? 通过 Linux 的动态可加载内核模块(LKM Loadable Kernel Module),它在运行时被链接到内核作为内核的一部分在内核空间运行,用户进程之间通过这个模块作为桥梁,就可以完成通信了。

Binder 是什么? Binder 驱动实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的,它工作于内核态,提供 open(),mmap(),poll(),ioctl() 等标准文件操作,以字符驱动设备中的 misc 设备注册在设备目录/dev 下,用户通过/dev/binder 访问它。

Binder C++ 层

见: [[Binder分层-C++层]]

Binder Java 层

见:[[[Binder分层-Java层]]]

Binder 原理

binder_mmap(文件描述符,用户虚拟内存空间)

binder 进程间通信效率高的核心机制:在内核虚拟地址空间,申请一块与用户虚拟内存相同大小的内存;然后再申请 1 个 page 大小的物理内存,再将同一块物理内存分别映射到内核虚拟地址空间和用户虚拟内存空间,从而实现了用户空间的 buffer 和内核空间的 buffer 同步操作的功能。

pzjvs
binder_mmap 通过加锁,保证一次只有一个进程分配内存,保证多进程间的并发访问。
虚拟进程地址空间 (vm_area_struct) 和虚拟内核地址空间 (vm_struct) 都映射到同一块物理内存空间,当 C 与 S 发送数据时,C 先从自己的进程空间把 IPC 通信数据 copy_from_user 拷贝到内核空间,而 S 端与内核共享数据,不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。
进程和内核虚拟地址映射到同一个物理内存的操作是发生在数据接收端 (Server),而数据发送端 (Client) 还是需要将用户态的数据拷贝到内核态。
Binder 在进程间数据通信的流程图:Binder 的内存转移关系。
f2zsj

Binder 原理

Binder 驱动:Binder 驱动运行在内核空间,负责各个用户进程的通信;Binder 不是 Linux 系统内核的一部分,但 Linux 的动态内核可加载模块的机制,Android可动态添加一个内核模块运行在内核空间。
内存映射:内存映射是通过 mmap() 来实现的,将用户空间的一块内存区域映射到内核空间,映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间;内存映射减少了数据拷贝次数。
Binder IPC 实现原理
Binder IPC 基于内存映射来实现的。Binder IPC 原理通信如下;

  1. 首先 Binder 驱动在内核空间创建一个数据接收缓存区
  2. 接着在内核空间开辟一块内核缓存区,建立内核缓存区内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区接收进程用户空间地址的映射关系
  3. 发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在映射关系,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信

k4619

Binder 进程和线程

hjzi2
进程
底层 Binder 驱动,通过 binder_procs 链表记录所有创建的 Binder_proc 结构体,Binder 驱动层的每一个 binder_proc 结构体都与用户空间的一个用于 Binder 通信的进程一一对应,且每个进程有且只有一个 ProcessState 对象,这是通过单例模式来保证的,每个进程中可以有很多个线程,每个线程对应一个 IPCThreadState 对象,IPCThreadState 对象也是单例模式,即一个线程对应一个 IPCThreadState 对象,在 Binder 驱动对应 binder_thread 结构体,在 binder_proc 结构体中通过成员变量 rb_root_threads 来记录当前进程内所有的 binder_thread
Binder 线程池
每个 Server 进程在启动时会创建一个 Binder 线程池,会注册一个 Binder 线程;后续 Server 进程可以向 Binder 线程池注册新的线程,Binder 驱动在探测到没有空闲线程时也会主动向 Server 进程注册 Binder 线程。
一个 Server 进程默认最多 16 个 Binder 线程

AIDL

见 [[AIDL基础]]

本文由作者按照 CC BY 4.0 进行授权