Android 进程间通信(二) -- 理解 Binder 的机制

2024-06-07 20:08

本文主要是介绍Android 进程间通信(二) -- 理解 Binder 的机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

参考 写给 Android 应用工程师的 Binder 原理剖析 一些文字和图片均参考该文

系列文章
Android 进程间通信(一) – Android 多进程模式
Android 进程间通信(二) – 理解 Binder 的机制
Android 进程间通信(三) --通过 AIDL 理解Binder,并手写Binder服务

上一章中,已经理解了进程之间通信的一些基本知识,这一章来好好学习 Binder。

一、为啥使用Binder

我们知道,Android 底层使用了大量的 Binder 来进行进程之间的通信。那为啥要新设计个 Binder ,而不是用传统的IPC 通信方式呢?
主要是考虑到以下几个方面:

  1. 性能方面:Socket 作为通过接口,但传输效率低,开销大,且阻塞 IO,一般用于跨网络的进程间通信;而消息队列和管道,则采用用存储-转阿发方法,至少经过两次拷贝;共享内存虽然无需拷贝,但实现复杂,控制也麻烦。

图片原来于 Android Binder 设计与实现在这里插入图片描述

  1. 稳定性:Binder 基于C/S架构,client 有什么需求就丢给 server,架构清晰,又相互独立。
  2. 安全性:Android 为每个应用都分配了自己的UID,用来鉴别身份,而传统的 IPC 则无法做到。

想要了解 Binder ,想了解 传统的 Linux 的IPC 机制。

1.1、linux 的 IPC 机制

Linux 采用了虚拟地址空间地址,操作系统将虚拟内存分为 用户空间 (User space) 和内核空间 (Kernel) ,普通的应用程序运行在用户空间,而系统内核运行在内核空间;这也是我们常说的两进程之间的数据不共享,不通过特殊手段不共享的问题。

在这里插入图片描述
从上图可以看出来传统的Linux跨进程涉及的一些点。

  • 进程隔离
  • 进程空间划分:用户空间(User space) 和 内核空间(Kerner space)
  • 系统调用状态:内核态和用户态

1.1.1 进程隔离

在操作系统中,两个进程之间的数据时不共享的,必须通过特殊的通信机制,进程间数据才能共享。

1.1.2 进程空间划分

现在的操作系统都是采用虚拟存储器,对于 32 位系统而言,它的寻址地址就是 2的32次方,即 4GB;
对操作系统而言,其中核心的部分称为内核,它的权限最高,可以访问受限的内存空间,也可以访问硬件设备,为了保护用户进程不能直接操作内核,保证内核的安全性,从逻辑上,将虚拟空间分户为用户空间和内核空间。
其中将高地址的1G划分为内核空间,而低地址的3G划分为用户空间;
在这里插入图片描述

1.1.3 系统调用: 内核态,用户态

虽然有以上哪些划分,但两进程之间不可能永远不通信;当两进程需要进行数据交互时,就需要系统调用来实现。系统调用时用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核控制下进行了,避免了越权访问,提供系统稳定性。

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

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

系统调用如下两个函数来实现:

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

一般来说,A进程和B进程是无法和对方数据交互,但有些情况下,就需要两个进程之间有交互,而这个交互过程就叫做 IPC (Inter Process Communication ,进程间通信),IPC 的实质是 数据的交互。IPC 的通信过程如下:

  • A进程发送方,把要发送的数据放到 用户空间的内存缓存区
  • 内核程序在内核空间开辟一块内核缓存区,并将A进程用户空间的数据,通过 copy_from_user 从内存缓冲区,拷贝到内核空间的内核缓冲区。
  • B进程接收方,也在用户空间开辟一块 内存缓存区,准备接受数据
  • 内核程序将内核缓存区通过 copy_to_user 将数据拷贝到用户空间的内存缓存区。

在这里插入图片描述
通过以上过程,IPC 一次就完成了,但这种方式有比较大的缺陷:

  • 由于不知道需要多大内存用于存放数据,因此都是尽可能开辟大的内存,会导致浪费
  • 性能较低,整个过程需要经过A进程 内存缓存区 - 内核缓存区 - 内存缓存区,需要两次拷贝。

1.2 Binder IPC通信原理

为了克服传统 IPC 的不足;Android 引入了 Binder 机制。Binder 在数据交互这块,可以充当是一个桥梁的作用,让两个进程之间能够相互通信。

从上面知道,数据的通信少不了内核的帮助,而Binder不属于内核,但通过 Linex 的 LKM 机制:

模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行

因此,Binder 作为模块存在于内核中,即成为 Binder 驱动。回顾上一节,传统IPC进程之间的通信需要两次的数据拷贝,Binder 却可以借助 Linux 的另一个特性,只用一次性拷贝,就能实现 IPC 过程,这个就是内存映射

Binder IPC 涉及到的内存映射通过 mmap() 来实现,mmap()是操作系统中一个内存映射的方法;简单来说,内存映射,就是把 用户空间的一块内存区域映射到内核空间,映射建立之后,用户空间的数据,就能反映到这块内核空间的内存来,这样,当用户空间的数据修改,内核空间的数据也会跟着被改动

内存映射能减少拷贝次数,实现用户空间和内核空间的效率互动。两个空间各自修改的数据能直接反射在映射的内存区域,从而被对方空间及时感知。

一次完整的 Binder IPC 通信过程是这样的:

  1. Binder 驱动在内核空间建立内核缓存区
  2. Binder 驱动在内核空间创建接收数据缓存区,并与内核缓存区建立映射,以及与接收进程的 用户空间地址建立映射关系
  3. 发送方进程通过系统调用 copy_from_user() 将数据copy 到内核中的内核缓存区,由于内核缓存区与接收数据缓存区有映射,接收数据缓存区又与接收方的用户空间地址有映射,所以,数据直接就到接收方的用户空间上了。
    如下图:

在这里插入图片描述

二. Binder 通信模型

上面介绍了 Binder IPC 的通信原理,这里实现层是如何设计的。

上面说到,Binder 是基于 C/S 架构的,由一系列组件组成,包括 Client 、Server、ServerManager,Binder驱动等

其中 Client 、Server、ServerManager 运行在用户空间,Binder 驱动运行在内核空间。ServerManager 和 Binder 驱动是由系统提供,而 Client 、Server则由用户自己创建。

Client 、Server、ServerManager,均是通过系统调用 open、mmap 和ioctl 来访问设备文件 /dev/binder 的,从而实现与 Binder 的交互来 间接实现进程之间的数据通信。
如下图:
在这里插入图片描述

  • Binder 驱动,已经解释过了,就是两个两个进程之间的桥梁
  • ServiceManager :它是binder的服务大管家,它的作用只有一个,注册和查询。一个Binder注册的时候,会携带对应的字符串,而 client 在获取这个binder 的时候,就可以通过这个字符串,通过 ServiceManager 查询拿到 binder。它也是一个looper循环。

三. Binder 的代理模式

从上面已经清楚,Client、Server 借助Binder驱动,完成跨进程的实现机制。
但有个问题,A 进程想要获取B进程的 object是,驱动是不是真的就把 object 返回 A ?

当然不是,当数据在 Binder 驱动 流过时,会对数据进行一层转换;当 A 想获取 B 的object 对象时,驱动并不会把 object 对象给 A,而是返回了一个跟 object 一模一样的代理对象 objectProxy,objectProxy 不具备 object 方法的能力;当A 使用 objectProxy 时,只需要把参数通过 objectProxy 给 Binder 驱动就可以了,看起来就像调用了 object 了。

当B接收到 A 进程的消息时,发现这个是 objectProxy,就去查询自己的表达那,一旦发现这个 B 进程的object 代理对象,就会通知 B 调用 object方法,并把结果返回给自己 或者 A 进程。如下:
在这里插入图片描述
这个章节,在 AIDL 的时候再来详细分析。

这篇关于Android 进程间通信(二) -- 理解 Binder 的机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1040173

相关文章

Linux下进程的CPU配置与线程绑定过程

《Linux下进程的CPU配置与线程绑定过程》本文介绍Linux系统中基于进程和线程的CPU配置方法,通过taskset命令和pthread库调整亲和力,将进程/线程绑定到特定CPU核心以优化资源分配... 目录1 基于进程的CPU配置1.1 对CPU亲和力的配置1.2 绑定进程到指定CPU核上运行2 基于

深入理解Go语言中二维切片的使用

《深入理解Go语言中二维切片的使用》本文深入讲解了Go语言中二维切片的概念与应用,用于表示矩阵、表格等二维数据结构,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧... 目录引言二维切片的基本概念定义创建二维切片二维切片的操作访问元素修改元素遍历二维切片二维切片的动态调整追加行动态

Android kotlin中 Channel 和 Flow 的区别和选择使用场景分析

《Androidkotlin中Channel和Flow的区别和选择使用场景分析》Kotlin协程中,Flow是冷数据流,按需触发,适合响应式数据处理;Channel是热数据流,持续发送,支持... 目录一、基本概念界定FlowChannel二、核心特性对比数据生产触发条件生产与消费的关系背压处理机制生命周期

Android ClassLoader加载机制详解

《AndroidClassLoader加载机制详解》Android的ClassLoader负责加载.dex文件,基于双亲委派模型,支持热修复和插件化,需注意类冲突、内存泄漏和兼容性问题,本文给大家介... 目录一、ClassLoader概述1.1 类加载的基本概念1.2 android与Java Class

Javaee多线程之进程和线程之间的区别和联系(最新整理)

《Javaee多线程之进程和线程之间的区别和联系(最新整理)》进程是资源分配单位,线程是调度执行单位,共享资源更高效,创建线程五种方式:继承Thread、Runnable接口、匿名类、lambda,r... 目录进程和线程进程线程进程和线程的区别创建线程的五种写法继承Thread,重写run实现Runnab

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java进程异常故障定位及排查过程

《Java进程异常故障定位及排查过程》:本文主要介绍Java进程异常故障定位及排查过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、故障发现与初步判断1. 监控系统告警2. 日志初步分析二、核心排查工具与步骤1. 进程状态检查2. CPU 飙升问题3. 内存

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

MySQL中的锁机制详解之全局锁,表级锁,行级锁

《MySQL中的锁机制详解之全局锁,表级锁,行级锁》MySQL锁机制通过全局、表级、行级锁控制并发,保障数据一致性与隔离性,全局锁适用于全库备份,表级锁适合读多写少场景,行级锁(InnoDB)实现高并... 目录一、锁机制基础:从并发问题到锁分类1.1 并发访问的三大问题1.2 锁的核心作用1.3 锁粒度分