之前一直对 Binder 理解不够透彻,仅仅知道一些皮毛,所以最近抽空深入理解一下,并在这里做个小结。
Binder 是 Android 系统中实现 IPC (进程间通信)的一种机制。Binder 原意是“胶水、粘合剂”,所以可以想象它的用途就是像胶水一样把两个进程紧紧“粘”在一起,从而可以方便地实现 IPC 。
那么为什么会有进程通信呢?这是因为在 Linux 中进程之间是隔离的,也就是说 A 进程不知道有 B 进程的存在,相应的 B 进程也不知道 A 进程的存在。A 、B 两进程的内存是不共享的,所以 A 进程的数据想要传给 B 进程就需要用到 IPC 。
在这里再科普一下进程空间的知识点:进程空间可以分为用户空间和内核空间。简单的说,用户空间是用户程序运行的空间,而内核空间就是内核运行的空间了。因为像内核这么底层、至关重要的东西肯定是不会简单地让用户程序随便调用的,所以需要把内核保护起来,就创造了内核空间,让内核运行在内核空间中,这样就不会被用户空间随便干扰到了。两个进程之间的用户空间是不共享的,但是内核空间是共享的。
所以到这里,有些同学会有个大胆的想法,两个进程间的通信可以利用内核空间来实现啊,因为它们的内核空间是共享的,这样数据不就传过去了嘛。但是接着又来了一个问题:为了保证安全性,用户空间和内核空间也是隔离的。那么如何把数据从发送方的用户空间传到内核空间呢?
针对这个问题提供了系统调用来解决,可以让用户程序调用内核资源。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性(这段话来自《写给 Android 应用工程师的 Binder 原理剖析》)。我们平时的网络、I/O操作其实都是通过系统调用在内核空间中运行的(也就是内核态)。
至此,关于 IPC 我们有了一个大概的实现方案:A 进程的数据通过系统调用把数据传输到内核空间(即copy_from_user),内核空间再利用系统调用把数据传输到 B 进程(即 copy_to_user)。这也正是目前 Linux 中传统 IPC 通信的实现原理,可以看到这其中会有两次数据拷贝。
20190521235434.jpg
(图片来自于《写给 Android 应用工程师的 Binder 原理剖析》)
Linux 中的一些 IPC 方式:
通过上面的讲解我们可以知道,IPC 是需要内核空间来支持的。Linux 中的管道、socket 等都是在内核中的。但是在 Linux 系统里面是没有 Binder 的。那么 Android 中是如何利用 Binder 来实现 IPC 的呢?
这就要讲到 Linux 中的动态内核可加载模块。动态内核可加载模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。(这段话来自《写给 Android 应用工程师的 Binder 原理剖析》)在 Android 中,这个内核模块也就是 Binder 驱动。
另外,Binder IPC 原理相比较上面传统的 Linux IPC 而言,只需要一次数据拷贝就可以完成了。那么究竟是怎么做到的呢?
其实 Binder 是借助于 mmap (内存映射)来实现的。mmap 用于文件或者其它对象映射进内存,通常是用在有物理介质的文件系统上的。mmap 简单的来说就是可以把用户空间的内存区域和内核空间的内存区域之间建立映射关系,这样就减少了数据拷贝的次数,任何一方的对内存区域的改动都将被反应给另一方。
所以,Binder 的做法就是建立一个虚拟设备(设备驱动是/dev/binder),然后在内核空间创建一块数据接收的缓存区,这个缓存区会和内存缓存区以及接收数据进程的用户空间建立映射,这样发送数据进程把数据发送到内存缓存区,该数据就会被间接映射到接收进程的用户空间中,减少了一次数据拷贝。具体可以看下图理解
20190522105623.jpg
(图片来自于《写给 Android 应用工程师的 Binder 原理剖析》)
Binder 的优点
在整个 Binder 通信过程中,可以分为四个部分:
其中 Client 和 Server 是应用层实现的,而 Binder 驱动和 ServiceManager 是 Android 系统底层实现的。
具体流程如下:
20190530122134.jpg
(Binder通信过程示意图来自于《写给 Android 应用工程师的 Binder 原理剖析》)