Android - 从浅到懂理解 Binder
文章目录
背景
在插件化使用时,进程间通信使用了AIDL进行跨进程通信,而AIDL底层的实现是使用Binder机制。
在深入了解了AIDL之后,我们还需要再深入学习Binder。
为什么需要跨进程通信(IPC)
一个进程一般是对应一个App,你不会希望别的进程(App)能够轻而易举的能操作你的App吧,所以你的App只能访问App内部的数据。
但是有些场景是需要通过一个App去操作另一个App的,比如:从App中调用系统的文件管理,实现文件读写。比如从App中读取手机通信录的联系人信息。这种情况就需要实现进程间通信了。
为什么是Binder?
Android 使用的 Linux 内核拥有着非常多的跨进程通信机制,比如:管道,消息队列,共享内存,System V,Socket等;
那么Android系统中的Binder究竟有何过人之处呢?
上述的进程间通信存在的问题:
Socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。- 消息队列和管道采用
存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。 - 共享内存虽然无需拷贝,但控制复杂,难以使用。
- 传统
IPC没有任何安全措施,完全依赖上层协议来确保。
Binder 的优势是:性能、稳定、安全。
-
性能
| IPC方式 | 数据拷贝次数|
| ---- | ---- |
| 共享内存 | 0 |
| Binder | 1 |
| Socket/管道/消息队列 | 2 | -
稳定
Binder是基于C/S架构。通过客户端(Client)给服务端(Server)发送指令而服务端根据指令返回数据的方式实现。
职责明确且互相独立,因此不易出错稳定性高。 -
安全
传统Linux IPC的接收方无法获得发送方进程可靠的UID/PID,从而无法鉴别对方身份;
而Android作为一个开源系统,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;
对于普通用户,绝不希望从商店下载的App能偷窥隐私数据、后台造成手机耗电等等问题。
Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志
而Binder通信可以获得通信进程的UID,有了UID就可以鉴别进程的身份。
同时 Binder 支持实名 Binder, 保证了安全性。
在分析性能时,谈到了App数据缓存区和内核缓存区。
App数据缓存区用于进程间数据隔离,而内核缓存区的数据是可以共享的。
为了进一步了解进程间通信,我们还需要去了解
- 用户空间/内核空间
- 内核态/用户态
- 内核模块/驱动
用户空间/内核空间
内核空间(Kernel Space)是系统内核运行的空间
用户空间(User Space)是用户程序运行的空间。
为了保证安全性,它们之间是隔离的。但是有的时候用户空间是需要去访问内核空间的。
比如:文件读写操作。
而用户空间访问内核空间的唯一方式就是系统调用。
系统调用:内核态/用户态
Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。
通过系统调用这个统一入口接口,所有的资源访问都是在内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。
当一个进程执行系统调用而陷入内核代码中执行时,我们就称进程处于内核态。此时处理器处于特权级最高的(0级)内核代码中执行
当进程在执行用户自己的代码时,则称其处于用户态。此时处理器在特权级最低的(3级)用户代码中运行。
系统调用主要通过如下两个函数来实现:
copy_from_user()//将数据从用户空间拷贝到内核空间copy_to_user()//将数据从内核空间拷贝到用户空间
传统的IPC就是使用上述两个系统调用的方法来实现进程间通信 。
传统 IPC 的原理
- 消息发送方将要发送的数据存放在
用户的内存缓存区中,通过系统调用进入内核态。 - 内核程序在内核空间开辟一块
内核缓存区,操作系统调用copy_from_user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。 - 接收方进程在自己的
用户空间开辟一块内存缓存区,然后内核程序调用copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。
这样数据发送方进程和数据接收方进程就完成了一次数据传输,也就是进程间通信。
内核模块 / “驱动”
通过系统调用,用户空间可以访问内核空间,
那么如果一个用户空间想与另外一个用户空间进行通信怎么办呢?
很自然想到的是让操作系统内核添加支持;
传统的Linux通信机制,比如Socket, 管道等都是内核支持的;
但是Binder并不是Linux内核的一部分,它是怎么做到访问内核空间的呢?
Linux的动态可加载内核模块(Loadable Kernel Module,LKM)机制解决了这个问题;
该模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。
它在运行时被链接到内核作为内核的一部分在内核空间运行。
Android 系统通过添加一个内核模块运行在内核空间,用户进程之间通过这个模块作为桥梁,就可以完成通信。
在Android系统中,这个运行在内核空间的,负责各个用户进程通过 Binder通信的内核模块叫做Binder驱动;
TIP:
驱动程序一般指的是设备驱动程序(Device Driver),是一种可以使计算机和设备通信的特殊程序。
相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作;
驱动就是操作硬件的接口,为了支持Binder通信过程,Android通过软件层面实现的Binder驱动,它类似于硬件接口用于和内核交互。
因此这个模块被称之为驱动。
前面说到了Binder的数据拷贝只有一次,而传统的IPC除了共享文件外都是最少两次数据拷贝
那么Binder驱动是如何实现的呢?
Binder IPC 机制实现原理
有点深奥,晦涩难懂。以后慢慢啃
Binder IPC 机制中实现数据拷贝仅一次的原理是用到了内存映射。数据拷贝是在
内存映射:
首先
映射是指建立一种关系,而这里的内存映射顾名思义就是将用户空间的一块内存区域映射到内核空间。在映射的过程中数据并没有拷贝,只是双方建立了链接。这个链接在物理上是不存在,只是逻辑上存在。也就是说这个联系我们看不见摸不着,只是我们代码上给它们进行了关联。
映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
而我们如何在代码上进行关联呢?
答案是通过操作系统调用 mmap() 方法来实现,但是 mmap() 通常是用在有物理介质的文件系统上的。
而Binder 并不存在物理介质,因此mmap() 方法 并不是为了在物理介质和用户空间之间建立映射,
mmap() 方法会返回一个指针ptr,该指针指向逻辑地址中的空间,这时候还没有数据拷贝。
要实现诗句拷贝需要将逻辑地址转换为物理地址,这个过程需要通过MMU(MemoryManagementUnit 内存管理单元)实现。
由于第一次数据通信双方还没建立映射,MMU在地址映射表中是无法找到与指针ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,
缺页中断的中断响应函数会在swap 分区中寻找相对应的页面,
如果找不到(也就是该文件从来没有被读入内存的情况),则会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中。
TIP:
swap 分区通常被称为交换分区,这是一块特殊的硬盘空间,即当实际内存不够用的时候,操作系统会从内存中取出一部分暂时不用的数据,放在交换分区中,从而为当前运行的程序腾出足够的内存空间。
所以数据拷贝是通过缺页中断机制将用户数据写入内存中。
一次完整的Binder IPC 通信过程通常是这样:
- 首先
Binder 驱动在内核空间创建一个数据接收缓存区; - 接着在内核空间开辟一块内核缓存区,建立发送方进程和接收方进程对内科缓存区的映射关系;
- 发送方进程通过
系统调用 copy_from_user()将数据copy到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
总结
Binder是Android系统提供的进程间通信的一种方式。
之所以提供Binder是因为传统的IPC机制存在一些问题
- 性能:传统的
IPC机制,如:Socket,管道,消息队列在通信时数据会经历两次拷贝。而Binder仅一次。 - 稳定:
Binder基于C/S架构模式,代码结构清晰不易出错。 - 安全:
Android是开源系统,众多软件鱼龙混杂。传统的IPC机制,通信双方不能鉴别身份。而Binder提供了UID用于标识进程,进而能鉴别通信双方。
传统的IPC通信机制原理是用到了系统调用的两个方法
copy_from_user(): 将数据从用户空间拷贝到内核空间copy_to_user(): 将数据从内核空间拷贝到用户空间
实现过程是:
- 发送方将数据存入
用户的内存缓存区 - 接收方在在自己进程开辟
用户的内存缓存区 - 内核空间开辟
内核缓存区 - 发送方从用户态进入内核态,并通过系统调用
copy_from_user()将发送方的内存缓存区的数据拷贝放入内核缓存区,然后通过copy_to_user()将数据拷贝到接收方的内存缓存区。
这就是传统IPC机制通信需要两次数据拷贝的问题。
而Binder仅需要一次数据拷贝。
它的原理是用到了内存映射和系统调用 mmap() 方法
- 通过内存映射的方式实现
发送方-内核-接收方之间的对应关系。 - 再通过
系统调用 mmap() 方法返回Binder驱动中的逻辑地址,而逻辑地址要和物理地址转换需要通过MMU MMU在连接逻辑地址和物理地址的时候会调用缺页中断方法在swap 分区中寻找相对应的数据。如果没找到,说明数据不存在,则需要进行数据拷贝数据拷贝使用的还是系统调用 copy_from_user()方法。- 由于存在映射关系,所以数据拷贝一次即可实现发送方和接收方的进程通信。
参考
- Binder学习指南
- 为什么 Android 要采用 Binder 作为 IPC 机制?
- Android跨进程通信:图文详解 Binder机制 原理
- Android Bander设计与实现 - 设计篇
- 写给 Android 应用工程师的 Binder 原理剖析!
- 内存映射原理
- 系统调用mmap详解整理
- Linux swap分区及作用详解
本文同步分享在 博客“_龙衣”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。