Android Binder 进程间通信

什么是Binder

Binder是Android系统中进程间通讯(IPC)的一种方式,也是Android系统中重要的特征之一。Android中的四大组件Activity、Service、Broadcast、ContentProvider,不同的App都运行在不同的进程中,它是这些进程间通讯的桥梁。正如其名"粘合剂"一样,它把系统中各个组件粘合到一起,是各个组件的桥梁。

IPC原理

IPC通信指的是两个进程之间交换数据,如图中的Client进程和Server进程。

Android为每个进程提供了虚拟内存空间,而每个Android进程只能运行在自己进程所拥有的虚拟内存空间。

内存空间又分为用户空间和内核空间,前者的数据不能进程间共享,但是后者可以。图中的Client进程和Server进程就是利用了进程间可以共享各自内核空间的数据,来完成底层通讯的工作。

Android的C/S通信机制

C/S通信指的就是Client和Server两个进程的通信,但实际通信时除了包含这两个进程,还有一个Service Manager,它用于管理各种服务。

这些服务通常是Android系统的核心功能模块,例如传感器管理、电源管理、WIFI管理、闹钟服务等,与Android四大组件中的服务不同。

当一个Server(服务端)想要提供一种服务,首先需要在Service Manager注册该服务。

而当Client(客户端)想要使用Server中的服务时,不能直接访问,而是要从Service Manager获取该服务,才能使用Server所提供的服务,来与Server进行通信。

Binder通信模块

引入Binder通信后,客户端、服务端和Service Manager之间不能通过API直接互相访问,而是与内核空间的Binder驱动通过ioctl方式来完成进程间的数据交换。

关键概念

  • Binder实体对象:Binder服务的提供者,类型是BBinder,位于服务端。
  • Binder引用对象:Binder实体对象在客户端进程的代表,类型是BpBinder,位于客户端。
  • IBinder对象:Binder实体对象和引用对象的统称,也是他们的父类。
  • Binder代理对象:又称接口对象,为客户端的上层应用提供接口服务,类型是IInterface。

Binder引用对象和代理对象都是服务端进程中的,把它们分离的好处是一个引用对象可以有多个代理对象,方便上层应用使用。

通讯过程

注册服务
  • Server进程向Binder驱动申请创建服务的Binder实体。
  • Binder驱动为这个服务创建位于内核的Binder实体和Binder引用。
  • 创建完成后,服务端通过Binder驱动将Binder引用发送给Service Manager。
  • Service Manager收到数据后,取出被创建服务的名字和引用,填入一张查找表。

通过以上步骤,Server进程通过Binder驱动完成了在Service Manager的服务注册。

在注册服务的过程中,Server进程是客户端,而Service Manager是服务端。

获取服务
  • Client进程利用Handle值为0的引用找到Service Manager。
  • Client进程向Service Manager发送xxxService的访问申请。
  • Service Manager从请求表中获取xxxService的名字,在查找表中找到对应的条目,取出对应的Binder引用。
  • Service Manager把xxxService的Binder引用传给Client进程。
使用服务

在使用服务时,Client和Server进程都是发送方和接收方。

这是因为Client在发生服务请求时,Server是接收方;当Server返回数据给Client时,Client变成了接收方。

不论发送方是谁,都会通过自身的Binder实体,把数据发送给接收方的Binder引用。Binder驱动会处理发送请求,利用内核空间进程共享机制如下:

  1. 把发送方的数据存入写缓存(Binder_write_read.write_buffer)(对于接收方这是读缓存)。
  2. 接收方一直处于阻塞状态,当写缓存有数据,会读取数据执行命令操作。
  3. 接收方执行操作后,会把结果返回,同样放在写缓存区(对于发送方是读缓存)。

Binder架构

  • Binder采用C/S架构,从组件视角来说,包含Client、Service、ServiceManager以及Binder驱动,其中ServiceManager用户管理系统中的各种服务。

  • Binder在Framework层进行封装,通过JNI技术调用Native(C/C++)层的Binder架构。

  • Binder在Native层以Ioctl的方式与Binder驱动进行通信。
    Activity、Service等组件都需要与AMS(system_server)通信,这种跨进程的通信是由Binder完成的。从不同角度分析Binder如下:

  • 机制:Binder是一种进程间通信机制。

  • 驱动:Binder是一个虚拟物理设备驱动。

  • 应用层:Binder是一个能发起通信的Java类:在Java中,如果想要进程通信,就要继承Binder。
    为什么要使用多进程进行分发?

虚拟接分配给各个进程的运行内存是有限制的,Imk也会优先回收占用系统资源大的进程。对于多进程开发的优势一般有以下几点:

  • 突破进程内存限制,为占用内存大的单独开辟一个进程。
  • 功能稳定性:如为通信进程保持长连接的稳定性。
  • 防止内存泄漏:如为容易内存泄漏的WebView单独开辟一个进程。
  • 隔离风险:对于不稳定的进程放在独立进程,避免主进程崩溃。

Binder机制

  • 首先需要注册服务端,只有注册了服务端,客户端才有通讯的目标,服务端通过ServiceManager注册服务,注册的过程就是向Binder驱动的全局链表binder_proces中插入服务端的信息(binder_proc结构体,每个binder_proc结构体中都有todo任务队列),然后向ServiceManager的svcinfo列表中缓存一下注册的服务。

  • 有了服务端,客户端就可以跟服务端通讯了,通讯之前需要先获取到服务,拿到服务的代理,也可以理解为引用。比如下面的代码:

    //获取WindowManager服务引用
    WindowManager wm = (WindowManager)getSystemService(getApplication().WINDOW_SERVICE);
    

    获取服务端的方式就是通过ServiceManager向svcinfo列表中查询一下返回服务端的代理,svcinfo列表就是所有已注册服务的通讯录,保存了所有注册的服务信息。

  • 有了服务端的引用我们就可以向服务端发送请求了,通过BinderProxy将我们的请求参数发送给ServiceManager,通过共享内存的方式使用内核方法copy_from_user()将我们的参数先拷贝到内核空间,这时我们的客户端进入等待状态,然后Binder驱动向服务端的todo队列里面插入一条事务,执行完之后把执行结果通过copy_to_user()将内核的结果拷贝到用户空间(这里只是执行了拷贝命令,并没有拷贝数据,Binder只进行一次拷贝),唤醒等待的客户端并把结果响应回来,这样就完成了一次通讯。

以上就是Binder机制的主要通讯方式,下面我们来看看具体实现。

Binder驱动

我们先来了解下用户空间与内核空间是怎么交互的。

用户空间/内核空间

内核空间定义

Linux 中的系统内存可以分为两个不同的区域:内核空间和用户空间。内核空间是内核(即操作系统的核心)执行(即运行)并提供其服务的地方。

内存由 RAM(随机存取存储器)单元组成,其内容可以以极高的速度访问(即读取和写入),但只能临时保留(即,在使用时或最多在电源保持开启时保留)。它的目的是保存当前正在使用的程序和数据,从而充当 CPU(中央处理器)和速度慢得多的存储之间的高速中介,后者通常由一个或多个硬盘驱动器 (HDD) 组成。

用户空间是用户进程(即内核以外的所有内容)运行的内存位置的集合。进程是程序的执行实例。内核的作用之一是管理此空间内的各个用户进程,并防止它们相互干扰。

用户进程只能通过使用系统调用来访问内核空间。系统调用是类 Unix 操作系统中由活动进程对内核执行的服务(例如输入/输出 (I/O) 或进程创建)的请求。活动进程是当前在 CPU 中正在进行的进程,与等待 CPU 中的下一轮次的进程形成对比。I/O 是指在 CPU 和外围设备(如磁盘驱动器、键盘、鼠标和打印机)之间传输数据的任何程序、操作或设备。

Kernel space是Linux内核的运行空间,User space是用户程序的运行空间。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。

Kernel space可以执行任意命令,调用系统的一切资源;User space只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称System Call),才能向内核发出指令。

系统调用/内核态/用户态

虽然从逻辑在抽离出用户空间和内核空间;但是不可避免的是,总有那么一些用户空间需要访问内核的资源;比如应用程序访问文件,网络是很常见的事情。

Kernel space can be accessed by user processes only through the use of system calls.

用户空间访问内核空间的唯一方式就是系统调用;通过这个统一入口接口,所有的资源访问都是内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。用户软件良莠不齐,要是它们搞乱把系统玩坏了怎么办?因此对于某些特权操作必须交给安全可靠的内核来执行。

当一个任务(线程)执行系统调用而陷入内核代码中执行时,我们就成称进程处于内核运行态(或简称为内核态)此时处理器处于特权级最高的(0级)内核代码中执行。当线程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。处理器在特权等级高的时候才能执行那些特权CPU指令。

内核模块/驱动

通过系统调用,用户空间可以访问内核空间,那么如果一个用户空间想与另外一个用户空间进行通信怎么办呢?很自然想到的是让操作系统内核添加支持;传统的Linux通讯机制,比如Socket、管道等都是内核支持的;但是Binder并不是Linux内核的一部分,它是怎么做到访问内核空间的呢?Linux的动态可加载内核模块(Loadable Kernel Module,LKM)机制解决了这个问题;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被连接到内核作为内核的一部分在内核空间运行。这样,Android系统可以通过添加一个内核模块运行在内核空间,用户进程之间的通过这个模块作为桥梁,就可以完成通讯了。

在Android系统重,这个运行在内核空间的,负责各个用户进程通过Binder通信的内核模块叫做Binder驱动;

驱动程序一般指的是设备驱动程序(Device Driver),是一种可以使计算机和设备通信的特殊程序。相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作;

驱动就是操作硬件的接口,为了支持Binder通信过程,Binder使用了一种"硬件",因此这个模块被称之为驱动。

熟悉了上面这些概念,我们再来看下上面的图,用户空间中binder_open(),binder_mmap(),binder_ioctl()这些方法通过System Call来调用内核空间Binder驱动中的方法。内核空间与用户空间共享内存通过copy_from_user(),copy_to_user()内核方法来完成用户空间与内核空间内存的数据传输。Binder驱动中有一个全局的binder_porcs链表保存了服务端的进程信息。

Binder进程和线程

对于底层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 驱动在探测到没有空闲 binder 线程时主动向 Server 进程注册新的的 binder 线程。对于一个 Server 进程有一个最大 Binder 线程数限制,默认为16个 binder 线程,例如 Android 的 system_server 进程就存在16个线程。对于所有 Client 端进程的 binder 请求都是交由 Server 端进程的 binder 线程来处理的。

ServiceManager启动

了解了 Binder 驱动,怎么与 Binder 驱动进行通讯呢?那就是通过 ServiceManager,好多文章称 ServiceManager 是 Binder 驱动的守护进程,大管家,其实 ServiceManager 的作用很简单就是提供了查询服务和注册服务的功能。下面我们来看一下 ServiceManager 启动的过程。

  • ServiceManager 分为 framework 层和 native 层,framework 层只是对 native 层进行了封装方便调用,图上展示的是 native 层的 ServiceManager 启动过程。

  • ServiceManager 的启动是系统在开机时,init 进程解析 init.rc 文件调用 service_manager.c 中的 main() 方法入口启动的。 native 层有一个 binder.c 封装了一些与 Binder 驱动交互的方法。

  • ServiceManager 的启动分为三步,首先打开驱动创建全局链表 binder_procs,然后将自己当前进程信息保存到 binder_procs 链表,最后开启 loop 不断的处理共享内存中的数据,并处理 BR_xxx 命令(ioctl 的命令,BR 可以理解为 binder reply 驱动处理完的响应)。

ServiceManager注册服务

  • 注册 MediaPlayerService 服务端,我们通过 ServiceManager 的 addService() 方法来注册服务。

  • 首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令(ioctl 的命令,BC 可以理解为 binder client 客户端发过来的请求命令)携带 ADD_SERVICE_TRANSACTION 命令,同时注册服务的线程进入等待状态 waitForResponse()。 Binder 驱动收到请求命令向 ServiceManager 的 todo 队列里面添加一条注册服务的事务。事务的任务就是创建服务端进程 binder_node 信息并插入到 binder_procs 链表中。

  • 事务处理完之后发送 BR_TRANSACTION 命令,ServiceManager 收到命令后向 svcinfo 列表中添加已经注册的服务。最后发送 BR_REPLY 命令唤醒等待的线程,通知注册成功。

ServiceManager获取服务

  • 获取服务的过程与注册类似,相反的过程。通过 ServiceManager 的 getService() 方法来注册服务。

  • 首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令携带 CHECK_SERVICE_TRANSACTION 命令,同时获取服务的线程进入等待状态 waitForResponse()。

  • Binder 驱动收到请求命令向 ServiceManager 的发送 BC_TRANSACTION 查询已注册的服务,查询到直接响应 BR_REPLY 唤醒等待的线程。若查询不到将与 binder_procs 链表中的服务进行一次通讯再响应。

进程一次完整通讯

  • 我们在使用 Binder 时基本都是调用 framework 层封装好的方法,AIDL 就是 framework 层提供的傻瓜式是使用方式。假设服务已经注册完,我们来看看客户端怎么执行服务端的方法。

  • 首先我们通过 ServiceManager 获取到服务端的 BinderProxy 代理对象,通过调用 BinderProxy 将参数,方法标识(例如:TRANSACTION_test,AIDL中自动生成)传给 ServiceManager,同时客户端线程进入等待状态。

  • ServiceManager 将用户空间的参数等请求数据复制到内核空间,并向服务端插入一条执行执行方法的事务。事务执行完通知 ServiceManager 将执行结果从内核空间复制到用户空间,并唤醒等待的线程,响应结果,通讯结束。

Binder有什么优势

进程间的通讯机制:管道、信号量、共享内存、Socket。

从性能触发,共享内存 > Binder > 其他IPC。

但是共享内存的缺点也十分明显。与线程之间共享同一块内存相同,共享内存的进程也很容易出现死锁、数据不同步等问题,操作不方便。同时,Socket作为一款通用接口,开销过大。

最重要的一点是安全性。传统ipc模式普遍存在的问题是依赖上层协议和访问接入点是开放的。

以创建服务为例,系统需要知道创建人的身份,但在传统ipc机制中,这个身份的获取是从上层协议获取的,即app将id传给系统,而app传回的内容可以是不真实的。对比之下,服务在被创建时,binder就会为创建人分配唯一的uid(用户身份)。

第二点以服务器为例,如果ip是开放的,服务器很容易就会被攻击。同样的,对于传统ipc,如果接入点被知晓,所有人都可以访问。对比之下,binder同时支持实名和匿名。实名与传统ipc相同,是开放的;匿名指的是如果有人需要获取服务,需要先获取到binder内部的引用,才能进行访问。通常的,系统服务是实名的,个人服务是匿名的。直接在service manager中注册的服务是实名的。

Binder是如何做到一次拷贝的

详细解释一下ipc和虚拟内存的概念:

进程间通信和线程间不同的原因是两者的内存机制不同。对于线程而言,它们的内存是共享的,但进程之间的内存是相互隔离的。

在ipc原理中我们看到进程内部分为用户空间和内核空间,出于安全两者之间是隔离的,app和系统分别处理用户空间和内核空间。

设想如果进程中没有对这两部份进行隔离,app就可以任意访问系统才能访问的数据,而正常来说这样的访问是需要权限的。但这不意味着两者之间完全隔离。系统为两者通信提供了api(copy_from_user & copy_to_user),使得两者间可以互相通信。

对于我们编程而言,需要系统分配虚拟内存,这是因为物理内存不一定是一整块内存,而这种整块内存恰恰是我们在编程中常常需要的。

虚拟内存是通过MMU内存管理单元来映射到物理内存的。在设计的时候,所有进程的内核空间都被映射到了同一块物理内存。这么做的好处就是实现了内存共享,一个进程可以很方便的去获取物理空间中其他进程的内核空间。

以上就是传统的ipc通信方式。一个进程把自身用户空间的数据通过copy_from_user拷贝到内核空间,而另一个进程通过copy_to_user第二次拷贝从内核空间获取数据到自己的用户空间,这就拷贝了两次。

在binder机制下通信时,内核空间和数据接收方的用户空间映射了同一块物理内存。

简而言之,获取数据的进程的用户空间和内核空间都分配了一小块内存空间,指向同一块物理内存。而这就意味着当被获取数据的进程,从用户空间复制数据到内核空间后,如果内核空间把这个数据存储到这一小块内存空间,另一个进程就能够直接获取,不需要再做一次复制。

MMAP的原理

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

用户空间是不能直接访问磁盘上的内容的,如需访问要通过内核空间,这是肯定很慢的。 首先需要调用write方法从用户空间复制到内核空间,再把数据复制到磁盘,完成写入。

因此当我们使用mmap时关联了虚拟内存和物理内存,当我们在虚拟内存做操作时,物理内存就会直接被修改。

相关推荐
恋猫de小郭5 小时前
Android Studio 正式版 10 周年回顾,承载 Androider 的峥嵘十年
android·ide·android studio
aaaweiaaaaaa8 小时前
php的使用及 phpstorm环境部署
android·web安全·网络安全·php·storm
工程师老罗10 小时前
Android记事本App设计开发项目实战教程2025最新版Android Studio
android
pengyu14 小时前
系统化掌握 Dart 编程之异常处理(二):从防御到艺术的进阶之路
android·flutter·dart
消失的旧时光-194314 小时前
android Camera 的进化
android
基哥的奋斗历程16 小时前
Openfga 授权模型搭建
android·adb
Pakho love1 天前
Linux:文件与fd(被打开的文件)
android·linux·c语言·c++
勿忘初心911 天前
Android车机DIY开发之软件篇(九) NXP AutomotiveOS编译
android·arm开发·经验分享·嵌入式硬件·mcu
lingllllove2 天前
PHP中配置 variables_order详解
android·开发语言·php
消失的旧时光-19432 天前
Android-音频采集
android·音视频