Binder 是 Android 系统进程间通信(IPC)最主要的一种方式。Linux 已经拥有管道,system V IPC,socket 等 IPC 手段,Android 却还要使用专门的 Binder 来实现进程间通信,说明 Binder 具有无可比拟的优势。深入了解 Binder 并将之与传统 IPC 做对比,有助于我们深入领会进程间通信的实现和性能优化。本文将对 Binder 的总体设计思想和关键设计细节做一个全面的阐述,首先介绍 Android 系统为什么使用 Binder 来进行 IPC,然后通过介绍 Binder 的通信模型和通信协议了解 Binder 的设计需求;接着阐述 Binder 在系统不同部分的表述方式和起的作用;最后说明 Binder 在数据接收端的设计考虑,包括内存映射,线程池管理和等待队列管理等。通过本文对 Binder 的大体介绍以及与其它 IPC 通信方式的对比,读者将对 Binder 的总体设计思想和优势有深入了解。
一 为什么是Binder
1.1 稳定性考虑
基于 C/S 的通信方式广泛应用于从互联网和数据库访问到嵌入式手持设备内部通信等各个领域。智能手机平台特别是 Android 系统中,为了向应用开发者提供丰富多样的功能,这种通信方式更是无处不在,从媒体播放,视频音频捕获,到各种让手机更智能的传感器(加速度,方位,温度,光亮度等)都由不同的 Server 负责管理,应用程序只需作为 Client 与这些 Server 建立连接,便可以使用这些服务,花很少的时间和精力就能开发出令人眩目的功能. C/S 方式的广泛采用对进程间通信(IPC)机制是一个挑战。C/S 架构最主要的优点就是,架构清晰明朗,Server 端与 Client 端相对独立,稳定性好.目前 Linux 支持的 IPC 包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及 socket 中只有 socket 支持 C/S 的通信方式。当然也可以在这些底层机制上架设一套协议来实现 C/S 通信,但这样增加了系统的复杂性,在手机这种条件复杂,资源稀缺的环境下可靠性也难以保证。
1.2 传输性能考虑
socket 作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题,这些问题需要靠各进程利用同步工具来解决,难以使用,缺乏稳定性。
表 1 各种IPC方式数据拷贝次数
IPC |
数据拷贝次数 |
共享内存 |
0 |
Binder |
1 |
Socket/管道/消息队列 |
2 |
1.3 安全性考虑
Android 作为一个开放式,拥有众多开发者的的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统 IPC 没有任何安全措施,完全依赖上层协议来确保。首先传统 IPC 的接收方无法获得对方进程可靠的 UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android 为每个安装好的应用程序分配了自己的 UID,故进程的 UID 是鉴别进程身份的重要标志。Android 系统中对外只暴露 Client 端,Client 端将任务发送给 Server 端,Server 端会根据权限控制策略,判断 UID/PID 是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行.使用传统 IPC 只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只能由 IPC 机制本身在内核中添加。其次传统 IPC 访问接入点是开放的,无法建立私有通道。比如命名管道的名称,system V的键值,socket 的 ip 地址或文件名等都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。
综合以上分析,Android 需要建立一套新的 IPC 机制来满足系统对通信方式,传输性能和安全性的要求,这就是 Binder。Binder基于 C/S 通信架构,传输过程只需一次拷贝,并且为发送方添加 UID/PID 身份标记,既支持实名 Binder 也支持匿名 Binder,安全性高。这些特点把稳定性,传输性能与安全性结合在了一起,这个就是 Android 选择 Binder 的原因.
二 面向对象的Binder
Binder 使用 C/S 通信方式:一个进程作为 Server 提供诸如视频/音频解码,视频捕获,地址查询,网络连接等服务;多个进程作为 Client 向 Server 发起服务请求,获得所需要的服务。要想实现 C/S 通信架构,必须实现以下两点:
- Server 必须有确定的访问接入点或者说地址来接受 Client 的请求,并且 Client 可以通过某种途径获知这个接入点或地址.
- 制定 Command-Reply 协议来传输数据.
例如在网络通信中 Server 的访问接入点就是 Server 主机的IP地址+端口号,传输协议为 TCP 协议.同理在 Binder 机制中,Server 端提供了一个 Binder 实体对象,这个 Binder 对象可以看成 Server 端提供的实现某个特定服务的访问接入点, Client 会通过某种途徑获取这个在 Server 端存在的 Binder 对象的"地址",也可以说是引用,然后通过这个引用,向 Server 发送请求来使用该服务;对 Client 而言,Binder 可以看成是通向 Server 的管道入口,或引用,要想和某个 Server 通信首先必须建立这个管道并获得管道入口。
与其它 IPC 不同,Binder 使用了面向对象的思想来描述作为访问接入点的 Binder 及其在 Client 中的入口:Binder 是一个实体位于 Server 端中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。遍布于不同 Client 中的对这个 Binder 的入口可以看成是指向这个 Binder 对象的"指针",一旦获得了这个"指针"就可以调用该 Binder 对象的方法访问 Server。在 Client 看来,通过 Binder "指针"调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别,尽管前者的实体位于远端 Server 中,而后者实体位于本地内存中。"指针"是 C++ 的术语,而更通常的说法是引用,即 Client 通过 Binder 的引用访问 Server。而软件领域另一个术语"句柄"也可以用来表述 Binder 在 Client 中的存在形式。从通信的角度看,Client 中的 Binder 也可以看作是 Server 端中 Binder 实体对象的"代理",在本地代表远端 Server 为 Client 提供服务。本文中会使用"引用"或"句柄"这个两广泛使用的术语。
面向对象思想的引入将进程间通信转化为通过对某个 Binder 实体对象的引用,调用该对象的方法,而其独特之处在于 Binder 对象是一个可以跨进程引用的对象,它的实体位于一个 Server 端进程中,而它的引用却遍布于系统的各个进程之中。最诱人的是,这个引用和 java 里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给其它进程,让大家都能访问同一个Server,就象将一个对象的引用赋值给另一个引用一样。Binder 模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。形形色色的 Binder 对象以及星罗棋布的引用就像胶水一样粘接着各个处于不同进程的应用程序,并实现了各个应用程序间有条不紊的通信.胶水,这也是 Binder 在英文里的原意。
当然面向对象只是针对应用程序而言,在内核驱动层,Binder 驱动和内核其它模块一样使用C语言实现,没有类和对象的概念。Binder 驱动为面向对象的进程间通信提供底层支持。
三 Binder的通信模型
Binder 框架定义了四个角色:Server,Client,ServiceManager(以后简称SMgr)以及 Binder 驱动。其中Server,Client,SMgr运行于用户空间,Binder 驱动运行于内核空间。其中 SMgr 和 Binder 驱动是由 Android 系统在平台中实现并提供支持,Server与Client 由开发者来实现.这四个角色的关系和互联网类似:Server 是服务器,Client 是客户端,SMgr 是域名服务器(DNS),驱动是路由器。
3.1 Binder驱动
和路由器一样,Binder 驱动虽然默默无闻,却是通信的核心。尽管名叫"驱动",实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供 open(),mmap(),poll(),ioctl() 等标准文件操作函数,以字符驱动设备中的misc 设备注册在设备目录"/dev"下,用户通过"/dev/binder"访问该它。作为虚拟字符设备,没有直接操作硬件,只是对设备内存的处理,驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数的管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由 ioctl() 接口实现,不提供 read(),write()接口,因为 ioctl() 灵活方便,且能够一次调用,实现先写后读以满足同步交互,而不必分别调用 write() 和 read()。Binder 驱动的代码位于 kernel 目录的 staging/android/binder.c 中。
3.2 ServiceManager
Client 需要通过某种途经拿到 Binder 的引用,然后通过这个引用来使用 Server 端提供的服务,那么 Client 是通过什么途经拿到Binder 引用的,答案就是通过请求 ServiceManager.和 DNS 类似,SMgr 的作用是将字符形式的 Binder 名字转化成 Client 中能够使用的 Binder 的引用,使得 Client 能够通过 Binder 名字获得对 Server 中 Binder 实体的引用,并使用相关服务.
注册了名字的 Binder 对象叫实名 Binder,就象每个网站除了有IP地址外还提供了自己的网址,用来方便记忆。Server 端创建了Binder 实体,为其取一个字符形式,可读易记的名字,将这个 Binder 对象连同名字一起以数据包的形式通过 Binder 驱动发送给SMgr,通知 SMgr 注册一个名叫张三的 Binder,它位于某个 Server 进程中。Binder 驱动开始在内核中大显身手了,驱动先是为这个穿越进程边界的 Binder 对象创建位于内核中的 Binder 实体节点 binder_node(这个以后在介绍进程的四颗红黑树的时候会细说),接着找到目标进程 SMgr,并创建 binder_ref 引用,这个 binder_ref 引用可以理解为,像指针一样指向位于 Server 端进程的 binder_node 节点,然后把这个 binder_ref 引用添加到 SMgr 进程的引用树中以便记录.最后将名字及新建的引用打包传递给用户空间的 SMgr。SMgr 收到数据包后,从中取出名字和引用填入一张查找表 svcinfo 中,以后只要有 Client 向 SMgr 请求某一个字符形式的 Binder,SMgr 就会查找这个 svcinfo 表,找到对应的 Binder 引用,然后返回给 Client 使用.注意 SMgr 中的存储在 svcinfo 表中的 Binder 引用其实是一个类型为 uint32_t 的整型句柄值 handle,这个 handle 是由 Binder 驱动生成的,并且按照次序递增的一个数字.这个 handle 和驱动层的 binder_ref 一一对应,也就是通过这个 handle 就能找到对应的 binder_ref 引用,进而找到 Server 进程的 binder_node,从而最后找到服务端的 Binder 实体.
细心的读者可能会发现其中的蹊跷:SMgr 是一个进程,Server 是另一个进程,Client 也是另外一个进程,Server 向 SMgr 注册Binder,以及 Client 通过 SMgr 获取 Binder 引用,都会涉及到进程间通信。当前讨论的是怎样实现进程间通信,却又要用到进程间通信,这个怎么解决?Binder 的实现比较巧妙:SMgr 和其它进程同样采用 Binder 通信,无论是对 Client,还是对 Server 来说,SMgr 都是 Server 端,有自己的 Binder 实体对象,其它进程都是Client,需要通过获取这个 Binder 的引用来实现 Binder 对象的注册,以及 Binder 引用的查询和获取。SMgr 提供的 Binder 实体对象比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR 命令将自己注册成 SMgr 时,Binder 驱动会自动为它创建 Binder 实体。其次这个 Binder 的引用或者说句柄 handle 在所有其它进程(Client 或 Server 进程)中都固定为0,而无须通过其它手段获得。也就是说,一个 Server若要向 SMgr 注册自己的 Binder 对象,就必需先通过0这个引用号来和 SMgr 进行通信。类比网络通信,0号引用就好比域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的 Client 是相对 SMgr 而言的,一个应用程序可能是个提供服务的Server,但对 SMgr 来说它仍然是个 Client。
3.3 Client
Server 向 SMgr 注册了 Binder 实体对象及其名字后,Client 就可以通过名字获得该 Binder 的引用了。Client 是利用保留的0号引用向 SMgr 请求获得某个 Binder 的引用:我申请获得名字叫张三的 Binder 的引用。SMgr 收到这个连接请求,从请求数据包里获得 Binder 的名字,在查找表 svcinfo 里找到该名字对应的条目,从条目中取出 Binder 的引用(句柄handle),将该引用作为回复发送给发起请求的 Client。从面向对象的角度,这个 Binder 对象现在有了两个引用:一个位于 SMgr 中,一个位于发起请求的Client 中。如果接下来有更多的 Client 请求该 Binder,系统中就会有更多的引用指向该 Binder,就象 java 里一个对象存在多个引用一样。而且类似的这些指向 Binder 的引用是强类型,从而确保只要有引用,Binder 实体就不会被释放掉。通过以上过程可以看出,SMgr 像个火车票代售点,收集并记录了所有火车车次的车票,可以通过它购买到乘坐各趟火车的票也就是得到某个 Binder 的引用。
3.4 Server
Server 毋庸置疑就是为 Client 提供各种类型的服务,比如上面说的视频/音频解码,视频捕获,地址查询,网络连接等,在ServiceManager 中已经说明了,他通过0号引用获得 SMgr,然后通过 SMgr 来注册自己,把自己的 Binder 信息注册到 SMgr中,让 Client 来查询获取并调用自己.
匿名Binder
这里还需要说明下匿名 Binder,并不是所有 Binder 实体对象都需要注册给 SMgr 广而告之的。Server 端可以通过已经建立的Binder 连接将创建的 Binder 实体对象传给 Client,当然这条已经建立好的 Binder 连接,必须是通过实名 Binder 实现的。由于这个 Binder 实体对象没有向 SMgr 注册名字,所以是个匿名的 Binder。Client 将会收到这个匿名 Binder 的引用,通过这个引用向位于 Server 中的 Binder 实体发送请求。匿名 Binder 为通信双方建立一条私密通道,只要 Server 没有把匿名 Binder 发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该 Binder 的引用,向该 Binder 发送请求。
下图展示了参与 Binder 通信的所有角色,将在以后章节中一一提到。
有必要对上图做以下说明:
- Server 端中的每一个 Binder 实体都会在 Binder 驱动中有与其一一对应对应的 Binder实体(binder_node)
- Client 端中的每一个 Binder 引用同样都会在 Binder 驱动中有与其一一对应的 Binder 引用(binder_ref)
- 每个涉及到 Binder 通信的进程,包括 Server 与 Client 中都会有一个0号引用,用来获取 SMgr,同样在驱动中也有与其一一对应的0号引用
- 不管是 Server 端的 Binder 实体或 Binder 引用(Server 端也可以存在 Binder引用,用来调用其它 Server),或者是 Client端的 Binder 引用,Binder 驱动都会在驱动中为其一一生成对应的数据结构,在驱动中 Binder 实体用 binder_node 表示,Binder 引用用 binder_ref 表示
- 存在多个不同的 Binder 引用指向同一个 Binder 实体,比如所有的进程中的0号引用都指向同一个 SMgr 实体
四 Binder的通信协议
Binder 协议的基本格式是命令+数据,使用 ioctl(fd, cmd, arg) 函数实现用户空间和