1. Linux 和 Binder 的 IPC 通信原理
进程通信的简单模型如下所示:
1.1 内核空间(Kernel space)和用户空间(User space)
操作系统从逻辑上将虚拟空间划分为用户空间和内核空间。Linux 操作系统将较高的 1GB 供内核使用,称为内核空间,将较低的 3GB 供各进程使用,称为用户空间。
内核空间是 Linux 内核的运行空间,用户空间是用户程序的运行空间。为了保证内核的安全,它们是隔离的,不能直接操作内核,这样,即使用户的程序崩溃了,内核也不会收到影响。内核空间的数据是可以进程间共享的,而用户空间的数据则不可以。
如上图所示,进程 A 的用户空间是不能和进程 B 的用户空间共享的。
1.2 进程隔离
进程隔离指的是一个进程不能直接操作或着访问另一个进程,也就是进程 A 不可以直接访问进程 B 的数据。
1.3 系统调用
用户空间需要访问内核空间,就需要借助系统调用来实现。系统调用时用户空间访问内核空间的唯一方式,保证了所有资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统的安全性和稳定性。
进程 A 和进程 B 的用户空间可以通过如下系统函数和内核空间交互:
- copy_from_user:将用户空间的数据复制到内核空间;
- copy_to_user:将内核空间的数据复制到用户空间;
1.4 内存映射
由于应用程序不能直接操作设备硬件地址,所以操作系统提供了一种机制:内存映射(Memory Map),把设备地址映射到进程虚拟内存区。
例如,用户空间需要读取磁盘的文件,如果不采用内存映射,那么就需要在内核空间建立一个页缓存,页缓存去复制磁盘中的文件,然后用户空间复制页缓存的文件,这就需要两次复制。如果采用内存映射,映射模型如下所示:
由于新建了虚拟内存区域,那么磁盘文件和虚拟内存区域就可以直接映射,少了一次复制。
在 Linux 中通过系统调用函数 mmap 来实现内存映射。将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反映到内核空间,反之亦然。内存映射能减少数据复制次数,实现用户空间和内核空间的高效互动。
1.5 Linux 的 IPC 通信原理
Linux 的 IPC 通信模型如下所示:
内核程序在内核空间分配内存并开辟一块内核缓存区,发送进程通过 copy_from_user() 函数将数据复制到内核空间的缓冲区。同样,接收进程接收数据时在自己的用户空间开辟一块内核缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区复制到接收进程。这样数据发送进程和数据接收进程就完成了一次数据传输,也就是一次进程间的通信。
Linux 的 IPC 通信原理有以下两个问题:
- 一次数据传递需要经历:用户空间 —> 内核缓存区 —> 用户空间,需要两次数据复制,这样效率不高;
- 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,以免浪费了空间或者时间;
1.6 Binder 的通信原理
Binder 是基于开源的 OpenBinder 实现的,OpenBinder 最早并不是由 Google 公司开发的,而是由 Be Inc 公司开发的,接着由 Palm Inc 公司负责开发。后来 OpenBinder 的作者 Dianne Hackborn 加入 Google 公司,并负责 Android 平台的开发工作,把这项技术也带进了 Android。
Binder 是基于内存映射来实现的,内存映射通常是用在有物理介质的文件系统上,然后 Binder 没有物理介质,它使用内存映射是为了跨进程传递数据的。 Linux 的 IPC 通信模型如下所示:
Binder 通信的步骤如下所示:
- Binder 驱动在内核空间创建一个数据接收缓存区;
- 在内核空间开辟一块内核缓存区,建立内核缓存区和数据接收缓存区之间的映射关系,以及数据接收缓存区和接收进程用户空间地址的映射关系;
- 发送方进程通过 copy_from_user() 函数将数据复制到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
整个过程只使用了一次复制,不会因为不知道数据的大小而浪费空间或者是假,这样效率更高。
1.7 使用 Binder 的原因
Android 是基于 Linux 内核的,Linux 提供了很多 IPC 机制,而 Android 自己设计了 Binder 来进行通信,主要原因有以下四个方面。
1.7.1 性能方面
性能方面主要影响的因素是数据复制次数,管道、消息队列、Socket 的复制次数都是两次,性能不是很好,共享内存不需要复制,性能很好,Binder 的复制次数为一次,性能仅次于内存复制。
1.7.2 稳定性方面
Binder 是基于 C/S 架构的,这个架构通常采用两层结构,在技术上已经很成熟了,稳定性也是没有问题的。共享内存没有分层,难以控制,并非同步访问临界资源时,可能还会产生死锁。从稳定性的角度讲,Binder 是优于共享内存的。
1.7.3 安全方面
Android 是一个开源的系统,并且拥有开放性的平台,市场上应用来源很广,因此安全性对 Android 平台而言极其重要。
传统的 IPC 接收方无法获得对方可靠的进程用户 ID/进程 ID(UID/PID),无法鉴别对方身份。Android 为每个安装好的 APP 分配了自己的 UID,通过进程的 UID 来鉴别进程身份。另外,Android 系统中的服务端会判断 UID/PID 是否满足访问权限,而对外只暴露客户端,加强了系统的安全性。
1.7.4 语言方面
Linux 是基于 C 语言的,C 语言是面向过程的,Android 应用层和 Java Framework 是基于 Java 语言的,Java 语言是面向对象的。Binder 本身符合面向对象的思想,因此作为 Android 的通信机制更合适。
从这四个方面来看,Linux 提供的大部分 IPC 机制无法和 Binder 相比较,而共享内存只在性能方面优于 Binder,其他方面都劣于 Binder,这些就是 Android 要使用 Binder 来进行进程间通信的原因,当然系统中并不是所有进程通信都采用了 Binder,而是根据场景选择最合适,比如 Zygote 进程与 AMS 通信使用的是 Socket,Kill Process采用的是信号。