1 什么是Binder?
- 从IPC角度来说, Binder 是 Android 中特有的一种跨进程的通信方式
- Binder 可以理解成一种虚拟的物理设备,设备驱动是/dev/binder
- 从 Android Framework 角度来说,Binder 是 ServiceManager 连接各种 Manager(ActivityManager、WindowManager等) 和 ManagerService 的桥梁
- Android系统基本上可以看作是一个基于Binder通信的C/S架构
Binder 形象类比图
通常我们访问一个网页的步骤是这样的:首先在浏览器输入一个地址,如 www.google.com 然后按下回车键。但是并没有办法通过域名地址直接找到我们要访问的服务器,因此需要首先访问 DNS 域名服务器,域名服务器中保存了 www.google.com 对应的 ip 地址 10.249.23.13,然后通过这个 ip 地址才能放到到 www.google.com 对应的服务器。
Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之间的关系。
Binder 驱动
Binder 驱动就如同路由器一样,是整个通信的核心;驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。
ServiceManager 与实名 Binder
ServiceManager 和 DNS 类似,作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用,使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用。注册了名字的 Binder 叫实名 Binder,就像网站除了有 IP 地址外还有自己的网址一样。Server 创建了 Binder,并为它起一个字符形式,可读易记得名字,将这个 Binder 实体连同名字一起以数据包的形式通过 Binder 驱动发送给 ServiceManager ,通知 ServiceManager 注册一个名为“张三”的 Binder,它位于某个 Server 中。驱动为这个穿越进程边界的 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager。ServiceManger 收到数据后从中取出名字和引用填入查找表。
ServierManager 是一个进程,Server 是另一个进程,Server 向 ServiceManager 中注册 Binder 必然涉及到进程间通信。当前实现进程间通信又要用到进程间通信,这就好像蛋可以孵出鸡的前提却是要先找只鸡下蛋!Binder 的实现比较巧妙,就是预先创造一只鸡来下蛋。ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDER_SET_CONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡)。其次这个 Binder 实体的引用在所有 Client 中都固定为 0 而无需通过其它手段获得。也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信。类比互联网,0 号引用就好比是域名服务器的地址,你必须预先动态或者手工配置好。要注意的是,这里说的 Client 是相对于 ServiceManager 而言的,一个进程或者应用程序可能是提供服务的 Server,但对于 ServiceManager 来说它仍然是个 Client。
Client 获得实名 Binder 的引用
Server 向 ServiceManager 中注册了 Binder 以后, Client 就能通过名字获得 Binder 的引用了。Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder: 我申请访问名字叫张三的 Binder 引用。ServiceManager 收到这个请求后从请求数据包中取出 Binder 名称,在查找表里找到对应的条目,取出对应的 Binder 引用作为回复发送给发起请求的 Client。从面向对象的角度看,Server 中的 Binder 实体现在有两个引用:一个位于 ServiceManager 中,一个位于发起请求的 Client 中。如果接下来有更多的 Client 请求该 Binder,系统中就会有更多的引用指向该 Binder ,就像 Java 中一个对象有多个引用一样。
2 Binder由来
Android 系统是基于 Linux 内核的,Linux 已经提供了管道、消息队列、共享内存和 Socket 等 IPC 机制。那为什么 Android 还要提供 Binder 来实现 IPC 呢?主要是基于性能、稳定性和安全性几方面的原因。
2.1 性能
- Socket 作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。
- 消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。
- 共享内存虽然无需拷贝,但控制复杂,难以使用。
- Binder 只需要一次数据拷贝,性能上仅次于共享内存。
2.2 稳定性
- Binder 基于 C/S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。
- 共享内存虽然无需拷贝,但是控制负责,难以使用。
2.3 安全性
Android 作为一个开放性的平台,其上运行这海量的应用程序,其安全性是不言而喻的。
传统的 IPC 没有任何安全措施,完全依赖上层协议来确保。
- 传统的 IPC 接收方无法获得对方可靠的进程用户ID/进程ID(UID/PID),从而无法鉴别对方身份。Android 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志。
- 传统的 IPC 只能由用户在数据包中填入 UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标识只有由 IPC 机制在内核中添加。
- 传统的 IPC 访问接入点是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。
Binder 既支持实名 Binder,又支持匿名 Binder,安全性高。
各种IPC方式数据拷贝次数
IPC | 数据拷贝次数 |
---|---|
共享内存 | 0 |
Binder | 1 |
Socket | 2 |
管道 | 2 |
消息队列 | 2 |
基于上述原因,Android 需要建立一套新的 IPC 机制来满足系统对稳定性、传输性能和安全性方面的要求,这套新的 IPC 机制就是 Binder。
3 Linux 传统的 IPC 原理
为了更好的理解 Binder 通信原理,我们先来了解下Linux 传统的进程间通信原理
我们先来了解下 Liunx 中跨进程通信涉及到的一些基本概念
3.1 用户空间/内核空间
Kernel space 是 Linux 内核的运行空间,User space 是用户程序的运行空间。 为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。
Kernel space 可以执行任意命令,调用系统的一切资源; User space 只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称 system call)调用系统资源。
3.2 系统调用/内核态/用户态
系统调用:用户空间访问内核空间的唯一方式就是系统调用;通过这个统一入口接口,所有的资源访问都是在内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。
Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。
内核态:当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)此时处理器处于特权级最高的(0级)内核代码中执行。
用户态:当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。
系统调用主要通过如下两个函数来实现
copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间
3.3 进程隔离
简单的说就是操作系统中,进程与进程间内存是不共享的。两个进程就像两个平行的世界,A 进程没法直接访问 B 进程的数据,这就是进程隔离的通俗解释。A 进程和 B 进程之间要进行数据交互就得采用特殊的通信机制:进程间通信(IPC)。
3.4 Linux 传统 IPC 通信原理
理解了上面的几个概念,我们再来看看传统的 IPC 方式中,进程之间是如何实现通信的。
通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。
这种传统的 IPC 通信方式有两个问题:
- 一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝,性能低。
- 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
4 Binder架构
- Binder 通信采用 C/S 架构,从组件视角来说,包含 Client、 Server、 ServiceManager 以及 Binder 驱动,其中 ServiceManager 用于管理系统中的各种服务。
- Binder 在 framework 层进行了封装,通过 JNI 技术与 Native(C/C++)层的 Binder 架构通信。
- Binder 在 Native 层以 ioctl 的方式与 Binder 驱动通信。
5 Binder机制
- 首先需要注册服务端,只有注册了服务端,客户端才有通讯的目标,服务端通过 ServiceManager 注册服务,注册的过程就是向 Binder 驱动的全局链表 binder_procs 中插入服务端的信息(binder_proc 结构体,每个 binder_proc 结构体中都有 todo 任务队列),然后向 ServiceManager 的 svcinfo 列表中缓存一下注册的服务。
- 有了服务端,客户端就可以跟服务端通讯了,通讯之前需要先获取到服务,拿到服务的代理,也可以理解为引用。比如下面的代码:
//获取WindowManager服务引用
WindowManager wm = (WindowManager)getSystemService(getApplication().WINDOW_SERVICE);
获取服务端的方式就是通过 ServiceManager 向 svcinfo 列表中查询一下返回服务端的代理,svcinfo 列表就是所有已注册服务的通讯录,保存了所有注册的服务信息。
- 有了服务端的引用我们就可以向服务端发送请求了,通过 BinderProxy 将我们的请求参数发送给 ServiceManager,通过共享内存的方式使用内核方法 copy_from_user() 将我们的参数先拷贝到内核空间,并建立用户空间到内核空间的内存映射关系,这时我们的客户端进入等待状态,然后 Binder 驱动向服务端的 todo 队列里面插入一条事务,执行完之后把执行结果通过 copy_to_user() 将内核的结果通过内存映射关系映射到用户空间,唤醒等待的客户端并把结果响应回来,这样就完成了一次通讯。
6 Binder驱动
我们先来看下用户空间与内核空间的交互
通过系统调用,用户空间可以访问内核空间,那么如果一个用户空间想与另外一个用户空间进行通信怎么办呢?很自然想到的是让操作系统内核添加支持;传统的 Linux 通信机制,比如 Socket,管道等都是内核支持的;但是 Binder 并不是 Linux 内核的一部分,它是怎么做到访问内核空间的呢? Linux 的动态可加载内核模块(Loadable Kernel Module,LKM)机制解决了这个问题;模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行。这样,Android系统可以通过添加一个内核模块运行在内核空间,用户进程之间的通过这个模块作为桥梁,就可以完成通信了。
在 Android 系统中,这个运行在内核空间的,负责各个用户进程通过 Binder 通信的内核模块叫做 Binder 驱动;
熟悉了上面这些概念,我们再来看下上面的图,用户空间中 binder_open(), binder_mmap(), binder_ioctl() 这些方法通过 system call 来调用内核空间 Binder 驱动中的方法。内核空间与用户空间共享内存通过 copy_from_user(), copy_to_user() 内核方法来完成用户空间与内核空间内存的数据传输。 Binder驱动中有一个全局的 binder_procs 链表保存了服务端的进程信息。
那么在 Android 系统中用户进程之间是如何通过这个内核模块(Binder 驱动)来实现通信的呢?难道是和前面说的传统 IPC 机制一样,先将数据从发送方进程拷贝到内核缓存区,然后再将数据从内核缓存区拷贝到接收方进程,通过两次拷贝来实现吗?显然不是,否则也不会有开篇所说的 Binder 在性能方面的优势了。
Linux 下的另一个概念:内存映射
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法,该函数经过系统调用最终会调用到binder驱动的binder_mmap() 函数。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。
7 Binder 进程与线程
对于底层Binder驱动,通过 binder_procs 链表记录所有创建的 binder_proc 结构体,binder 驱动层的每一个 binder_proc 结构体都与用户空间的一个用于 binder 通信的进程一一对应,且每个进程有且只有一个 ProcessState 对象,这是通过单例模式来保证的。在每个进程中可以有很多个线程,每个线程对应一个 IPCThreadState 对象,IPCThreadState 对象也是单例模式,即一个线程对应一个 IPCThreadState 对象,在 Binder 驱动层也有与之相对应的结构,那就是 Binder_thread 结构体。在 binder_proc 结构体中通过成员变量 rb_root threads,来记录当前进程内所有的 binder_thread。
Binder 线程池:每个 Server 进程在启动时创建一个 binder 线程池,并向其中注册一个 Binder 线程;之后 Server 进程也可以向 binder 线程池注册新的线程,或者 Binder 驱动在探测到没有空闲 binder 线程时主动向 Server 进程注册新的的 binder 线程。对于一个 Server 进程有一个最大 Binder 线程数限制,默认为16个 binder 线程,例如 Android 的 system_server 进程就存在16个线程。对于所有 Client 端进程的 binder 请求都是交由 Server 端进程的 binder 线程来处理的。
8 ServiceManager 启动
ServiceManager提供了向Binder 驱动查询服务和注册服务的功能。
- ServiceManager 分为 framework 层和 native 层,framework 层只是对 native 层进行了封装方便调用,图上展示的是 native 层的 ServiceManager 启动过程。
- ServiceManager 的启动是系统在开机时,init 进程解析 init.rc 文件调用 service_manager.c 中的 main() 方法入口启动的。 native 层有一个 binder.c 封装了一些与 Binder 驱动交互的方法。
- ServiceManager 的启动分为三步,首先打开驱动创建全局链表 binder_procs,然后将自己当前进程信息保存到 binder_procs 链表,最后开启 loop 不断的处理共享内存中的数据,并处理 BR_xxx 命令(ioctl 的命令,BR 可以理解为 binder reply 驱动处理完的响应)。
9 ServiceManager 注册服务
- 注册 MediaPlayerService 服务端,我们通过 ServiceManager 的 addService() 方法来注册服务。
- 首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令(ioctl 的命令,BC 可以理解为 binder client 客户端发过来的请求命令)携带 ADD_SERVICE_TRANSACTION 命令,同时注册服务的线程进入等待状态 waitForResponse()。 Binder 驱动收到请求命令向 ServiceManager 的 todo 队列里面添加一条注册服务的事务。事务的任务就是创建服务端进程 binder_node 信息并插入到 binder_procs 链表中。
- 事务处理完之后发送 BR_TRANSACTION 命令,ServiceManager 收到命令后向 svcinfo 列表中添加已经注册的服务。最后发送 BR_REPLY 命令唤醒等待的线程,通知注册成功。
10 ServiceManager 获取服务
- 获取服务的过程与注册类似,相反的过程。通过 ServiceManager 的 getService() 方法来注册服务。
- 首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令携带 CHECK_SERVICE_TRANSACTION 命令,同时获取服务的线程进入等待状态 waitForResponse()。
- Binder 驱动收到请求命令向 ServiceManager 的发送 BR_TRANSACTION 查询已注册的服务,查询到直接响应 BR_REPLY 唤醒等待的线程。若查询不到将与 binder_procs 链表中的服务进行一次通讯再响应。
11 进行一次完整通讯
我们在使用 Binder 时基本都是调用 framework 层封装好的方法,AIDL 就是 framework 层提供的傻瓜式是使用方式。假设服务已经注册完,我们来看看客户端怎么执行服务端的方法。
- 首先我们通过 ServiceManager 获取到服务端的 BinderProxy 代理对象,通过调用 BinderProxy 将参数,方法标识(例如:TRANSACTION_test,AIDL中自动生成)传给 ServiceManager,同时客户端线程进入等待状态。
- ServiceManager 将用户空间的参数等请求数据复制到内核空间,并向服务端插入一条执行执行方法的事务。事务执行完通知 ServiceManager 将执行结果从内核空间复制到用户空间,并唤醒等待的线程,响应结果。
参考链接:
一篇文章了解相见恨晚的 Android Binder 进程间通讯机制
写给 Android 应用工程师的 Binder 原理剖析
Android Bander设计与实现 - 设计篇