Binder 浅析 —— 整体概览
Binder 机制是 Android 开发者去阅读 framework 源码之前必须要啃透的一个重要知识点,是 Android 系统中 IPC 的底层原理,如果说不了解 Binder 机制,就难以对于 Android 系统有一个整体的理解。
Binder 机制涉及到了 Android 系统在 Linux 系统内核中的源码实现,同时需要拥有一些关于 Linux 操作系统相关的知识,以及了解 C/C++ 的基础语法。
可以说 Binder 机制是 Android 开发者在进阶路上最难啃的一块骨头,有很多大厂都使用 Binder 机制来鉴别人才。
为什么 Binder 如此重要?
身为 Android 开发者的你,是不是常常会有这样的疑问:
- 为什么 Activity 间传递对象需要序列化?
- Activity 的启动流程是什么样的?
- 四大组件底层的通信机制是怎样的?
- AIDL 内部的实现原理是什么?
- 插件化编程技术应该从何学起?等等...
这些问题的背后都与 Binder 有莫大的关系,要弄懂上面这些问题理解 Bidner 通信机制是必须的。
我们知道 Android 应用程序是由 Activity、Service、Broadcast Receiver 和 Content Provide 四大组件中的一个或者多个组成的。这些组件运行的时候会涉及到多个进程。这些进程间的通信就依赖于 Binder IPC 机制。不仅如此,Android 系统对应用层提供的各种服务如:AMS、PMS 等都是基于 Binder IPC 机制来实现的。Binder 机制在 Android 中的位置非常重要,毫不夸张的说理解 Binder 是迈向 Android 高级工程的第一步。
Binder 的优势在哪?
Android 系统是基于 Linux 内核的,Linux 已经提供了管道、消息队列、共享内存和 Socket 等 IPC 机制。那为什么 Android 还要提供 Binder 来实现 IPC 呢?
Binder | 共享内存 | Socket | |
---|---|---|---|
性能 | 需要拷贝一次 | 无需拷贝 | 需要拷贝两次 |
特点 | 基于C/S架构,易用性高 | 控制复杂,易用性差 | 基于C/S架构,作为一款通用接口,其传销效率低,开销大 |
安全性 | 为每个APP分配UID,同时支持实名和匿名,安全 | 依赖上层协议,访问接入点是开放的,不安全 | 依赖上层协议,访问接入点是开放的,不安全 |
综合来看,Binder 是这些 IPC 方案中最好的,这也是为什么 Android 系统使用 Binder 来进行 IPC 通信的原因。
Binder IPC 通信原理
想弄清楚 Binder IPC 机制,我们需要对 Linux IPC 的相关概念和原理有一定的了解。
要想弄清楚 Linux IPC 机制,自然就需要知道 Linux 系统中关于进程的相关概念。
进程隔离
简单的说就是操作系统中,进程与进程间内存是不共享的。两个进程就像两个平行的世界,A 进程没法直接访问 B 进程的数据,这就是进程隔离的通俗解释。A 进程和 B 进程之间要进行数据交互就得采用特殊的通信机制:进程间通信(IPC)。
进程空间划分:用户空间(User Space)/ 内核空间(Kernel Space)
现在操作系统中的进程都是采用虚拟存储器(虚拟内存),对于 32 位系统而言,它的寻址空间(虚拟存储空间)就是 2 的 32 次方,也就是 4GB。
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备。
为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间(User Space)和内核空间(Kernel Space)。
针对 Linux 操作系统而言,将地址位最高的 1GB 字节供内核使用,称为内核空间;地址位较低的 3GB 字节供各进程使用,称为用户空间。
简单的说就是,内核空间(Kernel)是系统内核运行的空间,用户空间(User Space)是用户程序运行的空间。为了保证安全性,它们之间是隔离的。
系统调用:用户态与内核态
虽然从逻辑上进行了用户空间和内核空间的划分,但不可避免的用户空间需要访问内核资源,比如文件操作、访问网络等等。为了突破隔离限制,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。
当进程因执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。
当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。
Linux 中传统 IPC 通信原理
消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user()
函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。
同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user()
函数将数据从内核缓存区拷贝到接收进程的内存缓存区。
这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。
这种传统的 IPC 通信方式有两个问题:
- 性能低下:一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝。
- 开销较大:接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
Binder IPC 通信原理
IPC 是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?
这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制。
模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。
如果只是 Binder 驱动,Binder IPC 通信与传统的 Linux IPC 通信是没区别的。Binder的优势体现在使用到了mmap技术,也就是 Linux 系统中的内存映射。
Linux 中通过将一个虚拟内存区域与一个磁盘上的物理内存区域关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。
mmap 是 Linux 中一种实现内存映射的方法。mmap 简单的讲就是将指定的一块虚拟内存区域与指定的一块物理内存区域建立映射关系。映射关系建立后,对这块虚拟内存区域的修改可以直接反应到物理内存上;反之物理内存中对这段区域的修改也能直接反应到虚拟内存上。
Binder IPC 通信的一次拷贝就是使用mmap方法来实现的。
一次完整的 Binder IPC 通信过程通常是这样:
- 在接收方的用户空间中开辟一块用户缓存区,在 Binder 驱动所在的内核空间中开辟一块相同大小的内核缓冲区。
- 将用户缓冲区和内核缓冲区使用 mmap 方法映射到一块相同的物理内存上。
- 发送方通过系统调用
copy_from_user()
将数据拷贝到内核中的内核缓存区,由于内核缓存区和接收方的用户缓冲区存在内存映射,因此也就相当于把数据发送到了接收方的用户空间,这样便完成了一次进程间的通信。