Binder系列1 Binder总体设计思想

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),驱动是路由器。

Binder系列1 Binder总体设计思想_第1张图片

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 通信的所有角色,将在以后章节中一一提到。

binder_overview

有必要对上图做以下说明:

  • 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) 函数实现用户空间和内核空间的交互。命令由参数 cmd 承载,数据由参数 arg 承载,数据随 cmd 不同而不同。下表列举了所有命令及其所对应的数据:

命令

含义

arg

BINDER_WRITE_READ

该命令用于向 Binder 驱动写入数据或从 Binder 驱动读取数据。参数为 binder_write_read数据结构,在用户空间和内核空间都有定义,这个数据结构设计的很巧妙,分为两段:写部分和读部分。如果 write_size不为 0 就先将 write_buffer 里的数据写入 Binder 驱动;如果 read_size 不为 0,再从 Binder 驱动中读取数据,然后存入 read_buffer 中。write_consumed 和 read_consumed 表示操作完成时Binder驱动实际写入或读出的数据个数。

struct binder_write_read {

binder_size_t    write_size;

binder_size_t    write_consumed;

binder_uintptr_r  write_buffer;

binder_size_t    read_size;

binder_size_t    read_consumed;

binder_uintptr_r  read_buffer;

};

BINDER_SET_MAX_THREADS

该命令告知 Binder 驱动,接收方(通常是 Server 端)线程池中最大的线程数。由于 Client 是并发向 Server 端发送请求的,Server 端必须开辟线程池为这些并发请求提供服务。告知驱动,线程池的最大值是为了让驱动发现线程数达到该值时不要再命令接收端启动新的线程。

size_t max_threads;

BINDER_SET_CONTEXT_MGR

将当前进程注册为 SMgr。系统中同时只能存在一个 SMgr。只要当前的 SMgr 没有调用 close() 关闭 Binder 驱动,就不能有别的进程可以成为 SMgr。这个命令主要是提供给 SMgr 进程来使用的,把自己注册为系统中唯一的 SMgr。

0

BINDER_THREAD_EXIT

通知 Binder 驱动当前线程退出了。Binder 驱动会为所有参与 Binder 通信的线程(包括 Server 线程池中的线程和 Client 发出请求的线程)建立相应的数据结构 binder_thread。这些线程在退出时必须通知驱动释放相应的数据结构。

0

BINDER_VERSION

获得 Binder 驱动的版本号

BINDER_GET_NODE_DEBUG_INFO 获取 binder_node debug 信息

这其中最常用的命令是 BINDER_WRITE_READ。该命令的参数类型为 binder_write_read,包括两部分数据:一部分是向 Binder 驱动写入的数据,一部分是要从 Binder 驱动读出的数据,驱动程序先处理写部分,再处理读部分。这样安排的好处是应用程序可以很灵活地处理命令的同步或异步。例如:若要发送异步命令可以只填入写部分而将 read_size 置成 0;若要只从 Binder 获得数据可以将写部分置空即 write_size 置成 0;若要发送请求并同步等待返回数据可以将两部分都置上。

4.1 BINDER_WRITE_READ 写操作

BINDER_WRITE_READ 写操作的数据格式同样也是(命令+数据)。这时候命令和数据都存放在 binder_write_read 结构的 write_buffer 域指向的内存空间里,多条命令可以连续存放,数据紧接着存放在命令后面,同样数据的格式根据命令不同而不同。下表列举了 Binder 写操作支持的命令:

cmd

含义

arg

BC_TRANSACTION
BC_REPLY

BC_TRANSACTION 用于 Client 向 Server 发送请求数据;BC_REPLY 用于 Server 向 Client 发送回复(应答)数据。其后面紧接着一个binder_transaction_data 结构体表明要写入的数据。

struct binder_transaction_data

BC_ACQUIRE_RESULT
BC_ATTEMPT_ACQUIRE

暂未实现

---

BC_FREE_BUFFER

释放一块映射的内存。Binder 接收方进程通过 mmap() 映射一块较大的内存空间,Binder 驱动基于这片内存,采用最佳匹配算法,实现接收数据缓存的动态分配和释放,满足并发请求对接收缓存区的需求。应用程序处理完这片数据后必须尽快使用该命令释放缓存区,否则会因为缓存区耗尽而无法接收新数据。

指向需要释放的缓存区的指针;该指针位于收到的 Binder 数据包中

BC_INCREFS
BC_ACQUIRE
BC_RELEASE
BC_DECREFS

这组命令用来增加或减少 Binder 的引用计数,用以实现强指针或弱指针的功能。

int32_t 类型 Binder 引用号

BC_INCREFS_DONE
BC_ACQUIRE_DONE

第一次增加 Binder 实体引用计数时,驱动向 Binder 实体所在的进程发送 BR_INCREFS, BR_ACQUIRE 消息;Binder 实体所在的进程处理完毕反馈 BC_INCREFS_DONE,BC_ACQUIRE_DONE

void *ptr:Binder 实体在用户空间中的指针

void *cookie:与该实体相关的附加数据

BC_REGISTER_LOOPER
BC_ENTER_LOOPER
BC_EXIT_LOOPER

这组命令同 BINDER_SET_MAX_THREADS 一道实现 Binder 驱动对接收方线程池的管理。BC_REGISTER_LOOPER 通知 Binder 驱动,线程池中一个线程已经创建了;BC_ENTER_LOOPER 通知 Binder 驱动该线程已经进入主循环,可以接收数据了;BC_EXIT_LOOPER 通知 Binder 驱动,该线程退出主循环,不再接收数据。详细内容请参考 Binder系列10 Binder线程池管理

BC_REQUEST_DEATH_NOTIFICATION

BC_CLEAR_DEATH_NOTIFICATION

获得 Binder 引用的进程,通过该命令要求 Binder 驱动在 Binder 实体销毁的时候,发通知回来,通知自己。虽说强指针可以确保只要有引用就不会销毁实体,但这毕竟是个跨进程的引用,谁也无法保证 Binder 实体所在的 Server 端进程不会因为内部错误,或其它异常而关闭,这种情况,Client 端所能做的就是要求 Server 在此刻给出通知,通知自己:

BC_REQUEST_DEATH_NOTIFICATION  死亡通知的注册

BC_CLEAR_DEATH_NOTIFICATION 死亡通知的注销

这个详细细节在 Binder系列11 死亡通知机制 中有详细介绍。

int32_t handle; 注册死亡通知的 Binder 引用号

uintptr_t proxy: 注册死亡通知的 Binder 代理对象的地址

BC_DEAD_BINDER_DONE

收到 Binder 实体死亡通知书的进程,在删除引用后用此命令告知 Binder 驱动

uintptr_t proxy 注册死亡通知的 Binder 代理对象的地址

BC_TRANSACTION_SG

BC_REPLY_SG

没看到发送的场景 binder_transaction_data_sg

在这些命令中,最常用的是 BC_TRANSACTION / BC_REPLY 命令对,Binder 的请求和应答数据就是通过这对命令发送给接收方的。这对命令所承载的数据包由结构体 binder_transaction_data 定义。Binder 交互有同步和异步之分,利用 binder_transaction_data 中的 flags 域进行区分。如果 flags 域的 TF_ONE_WAY 位为 1,则为异步交互,即 Client 端发送完请求,交互即结束, Server 端不再返回 BC_REPLY 数据包;否则 Server 会返回 BC_REPLY 数据包,Client 端必须等待接收完该数据包方才完成一次交互。 

4.2 BINDER_WRITE_READ 读操作

从 Binder 驱动里读出的数据格式和向 Binder 中写入的数据格式一样,采用(命令+数据)形式,并且多条命令可以连续存放。下表列举了从 Binder 驱动读出的命令及其相应的参数:

cmd

含义

参数

BR_ERROR

发生内部错误(如内存分配失败)

---

BR_OK
BR_NOOP

操作完成

---

BR_SPAWN_LOOPER

该消息用于 Binder 接收方线程池管理。当 Binder 驱动发现接收方所有线程都处于忙碌状态且线程池里的线程总数没有超过 BINDER_SET_MAX_THREADS 设置的最大线程数时,会向接收方发送该命令要求创建更多线程以备接收数据。Binder系列10 Binder线程池管理

---

BR_TRANSACTION
BR_REPLY

这两条消息分别对应发送方的 BC_TRANSACTION 和 BC_REPLY,表示当前接收的数据是请求还是回复

binder_transaction_data

BR_ACQUIRE_RESULT
BR_ATTEMPT_ACQUIRE
BR_FINISHED

尚未实现

---

BR_DEAD_REPLY

交互过程中如果发现对方进程或线程已经死亡则返回该消息

---

BR_TRANSACTION_COMPLETE

发送方通过 BC_TRANSACTION 或 BC_REPLY 发送完一个数据包后,都能收到从 Binder 驱动发送过来的该消息,做为成功发送的反馈。这和 BR_REPLY 不一样,是驱动告知发送方已经发送成功,而不是 Server 端返回请求数据。所以不管同步还是异步交互接收方都能获得本消息。

---

BR_INCREFS
BR_ACQUIRE
BR_RELEASE
BR_DECREFS

这一组消息用于管理强 / 弱指针的引用计数。只有提供 Binder 实体的进程才能收到这组消息

void *ptr:Binder 实体在用户空间中的指针

void *cookie:与该实体相关的附加数据

BR_DEAD_BINDER
BR_CLEAR_DEATH_NOTIFICATION_DONE

向获得 Binder 引用的进程发送 Binder 实体死亡通知书;收到死亡通知书的进程接下来会返回 BC_DEAD_BINDER_DONE 做确认

void **cookie:在使用 BC_REQUEST_DEATH_NOTIFICATION注册死亡通知时的附加参数。

BR_FAILED_REPLY

如果发送非法引用号则返回该消息

---

和写数据一样,其中最重要的命令是 BR_TRANSACTION 和 BR_REPLY,表明收到了一个格式为 binder_transaction_data 的请求数据包(BR_TRANSACTION)或返回数据包(BR_REPLY)。 

4.3 binder_transaction_data 数据包

我们知道承载 BC_TRANSACTION / BC_REPLY 与 BR_TRANSACTION / BR_REPLY 命令对的关键数据是 binder_transaction_data,这个数据结构里边提供了通信传输的关键信息,如下:

struct binder_transaction_data {
	/* The first two are only used for bcTRANSACTION and brTRANSACTION,
	 * identifying the target and contents of the transaction.
	 */
	union {
		/* target descriptor of command transaction */
		__u32	handle;
		/* target descriptor of return transaction */
		binder_uintptr_t ptr;
	} target;
	binder_uintptr_t	cookie;	/* target object cookie */
	__u32		code;		/* transaction command */

	/* General information about the transaction. */
	__u32	        flags;
	pid_t		sender_pid;
	uid_t		sender_euid;
	binder_size_t	data_size;	/* number of bytes of data */
	binder_size_t	offsets_size;	/* number of bytes of offsets */

	/* If this transaction is inline, the data immediately
	 * follows here; otherwise, it ends with a pointer to
	 * the data buffer.
	 */
	union {
		struct {
			/* transaction data */
			binder_uintptr_t	buffer;
			/* offsets from buffer to flat_binder_object structs */
			binder_uintptr_t	offsets;
		} ptr;
		__u8	buf[8];
	} data;
};

详细说明如下: 

成员

含义

union {

__u32 handle;

binder_uintptr_t ptr;

} target;

target,顾名思义,这个成员用来描述发送目的地的信息。由于目的地是在远端,所以这里填入的是对远端 Binder 实体的引用,由发送方填入,存放在 target.handle 中。如前述,Binder 的引用在代码中也叫句柄(handle),是由 Binder 驱动生成的,并且是按照次序递增的一个整型数值,驱动会根据这个 handle,找到对应的 Server 端的 Binder 实体。

当数据包 binder_transaction_data 经过 Binder 驱动的时候,驱动会根据其中的 target.handle 来找到对应的远端 Binder 实体的指针,并将这个指针赋值给 ptr 域。驱动是怎么知道远端的 Binder 实体的指针的呢?原因是该指针是 Server 接收方在将 Binder 实体传输给其它进程时(比如向 SMgr 注册)提交给驱动的,驱动程序已经把这个指针记录并保存了下来,具体细节以后会介绍。总之 Binder 驱动能够根据发送方填入的 Binder 引用(handle)找到对应的接收方 Binder 实体对象的指针,故接收方可以直接将其当做对象指针来使用(通常是将其 reinterpret_cast 成相应类)

binder_uintptr_t cookie;

发送方忽略该成员;Binder 驱动会在把数据包返回给接收方用户空间之前,给该成员赋值,该成员存放的是接收方 Binder 实体对象的地址

__u32 code;

该成员存放收发双方约定的命令码,驱动完全不关心该成员的内容。通常是 Server 端定义的公共接口函数的编号

__u32 flags;

与交互相关的标志位,其中最重要的是 TF_ONE_WAY 位。如果该位置为 1,表明这次交互是异步的,Server 端不会返回任何数据。驱动利用该位来决定是否构建与返回有关的数据结构。另外一位 TF_ACCEPT_FDS 是出于安全考虑,如果发起请求的一方不希望在收到的回复中接收文件形式的 Binder,可以将该位置置为 0。因为收到一个文件形式的 Binder,会自动为数据接收方打开一个文件,使用该位可以防止打开文件过多

pid_t sender_pid;

uid_t sender_euid;

该成员存放发送方的进程 ID 和用户 ID,由驱动负责填入,接收方可以读取该成员获知发送方的身份

binder_size_t data_size;

该成员表示 data.ptr.buffer 指向的缓冲区存放的数据长度。发送数据时由发送方填入,表示即将发送的数据长度;在接收方用来告知接收到数据的长度

binder_size_t offsets_size;

驱动一般情况下不关心 data.ptr.buffer 里存放什么数据,但如果有 Binder 对象(驱动中的数据结构为 flat_binder_object) 在其中传输则需要将其相对 data.ptr.buffer 的偏移位置,指出来让驱动知道。有可能存在多个 Binder 对象同时在数据中传递,所以须用数组表示所有偏移位置。本成员表示该数组的大小

union {

struct {

binder_uintptr_t buffer;

binder_uintptr_t offsets;

} ptr;

_u8 buf[8];

} data;

data.ptr.buffer 指向要发送或接收的数据;data.ptr.offsets 指向 Binder 对象(flat_binder_object) 相对于 data.ptr.buffer 偏移位置的数组

这里有必要再强调一下 offsets_size 和 data.ptr.offsets 两个成员,这是 Binder 通信有别于其它 IPC 的地方。如前述,Binder 采用面向对象的设计思想,一个 Binder 实体可以发送给其它进程,从而建立许多跨进程的引用;另外这些引用也可以在进程之间传递,就象 java 里将一个引用赋给另一个引用一样。

Binder 在不同进程中建立引用必须有驱动的参与,由驱动在内核创建并注册相关的数据结构后,发送方才能使用该引用。而且这些引用可以是强类型,需要驱动为其维护引用计数。然而这些跨进程传递的 Binder 混杂在应用程序发送的数据包里,数据格式由用户定义,如果不把它们一一标记出来告知驱动,驱动将无法从数据中将它们提取出来。于是就使用数组 data.ptr.offsets 存放用户数据中每个 Binder 相对 data.ptr.buffer 的偏移量,用 offsets_size 表示这个数组的大小。驱动在发送数据包时会根据 data.ptr.offsets 和 offset_size 将散落于 data.ptr.buffer 中的 Binder 找出来并一一为它们创建相关的数据结构。在数据包中传输的 Binder 是类型为 flat_binder_object 的结构体,详见后文。

对于接收方来说,该结构只相当于一个定长的消息头,真正的用户数据存放在 data.ptr.buffer 所指向的缓存区中。如果发送方在数据中内嵌了一个或多个 Binder,接收到的数据包中同样会用 data.offsets 和 offset_size 指出每个 Binder 的位置和总个数。不过通常接收方可以忽略这些信息,因为接收方是知道数据格式的,参考双方约定的格式定义就能知道这些 Binder 在什么位置。

下图直观反馈了 BINDER_WRITE_READ 协议执行写操作的时候,write_buffer 中的数据存储情况,可以看到写命令和数据是顺序存放的,并且可以存放多条命令和数据,其中重点关注最常用的传输命令 BC_TRANSACTION,它携带的数据为 binder_transaction_data 数据包,Binder 对象是以 flat_binder_object 的形式散落存在 data.ptr.buffer 中,offsets 就是可以标记这些 Binder 对象相对于 buffer 偏移位置的数组,驱动就是根据 offsets 和 offsets_size,来找到 data.ptr.buffer 中的 Binder 对象,并一一为其生成对应的数据结构。

binder_proto

五 Binder 的表述

分析 Binder 通信的一次全过程会发现,Binder 存在于系统的以下几个部分中:

  • 应用程序进程:分别位于 Server 进程和 Client 进程中
  • 传输数据:由于 Binder 可以进行跨进程传递,所以 Binder 需要在传输数据中予以表述
  • Binder 驱动:管理各个进程中存在的 Binder 实体和 Binder 引用

在系统的不同部分,Binder 实现的功能不同,表现形式也不一样。接下来逐一探讨 Binder 在系统中各部分所扮演的角色和使用何种数据结构来表述。

5.1 Binder 在应用程序中的表述

虽然 Binder 的设计用到了面向对象的思想,但并不意味着应用程序一定要使用面向对象的语言。无论是 C 语言,还是 C++ 语言都可以很容易的使用 Binder 来通信。例如尽管 Android 主要使用 java 和 C++,但是像 SMgr 这么重要的进程,就是用 C 语言来实现的。

Binder 本质上只是一种底层通信方式,和具体的服务没有关系。为了提供具体服务,Server 必须提供一套接口函数,以便 Client 通过远程访问使用 Server 提供的这些服务。这时通常采用 Proxy 设计模式:将接口函数定义在一个抽象类中,Server 和 Client 都会以该抽象类为基类实现所有接口函数,所不同的是 Server 端是真正的功能实现,而 Client 端是对这些函数远程调用请求的包装。如何将 Binder 和 Proxy 设计模式结合起来是应用程序实现 Binder 通信的根本问题。

5.1.1 Binder 在 Server 端的表述 - Binder 实体

做为 Proxy 设计模式的基础,首先定义一个抽象接口类封装 Server 所有的功能,其中包含一系列纯虚函数留待 Server 和 Proxy 各自实现。由于这些函数需要跨进程调用,须为其一一编号,从而 Server 可以根据收到的编号决定调用哪个函数。其次就要引入 Binder 了。Server 端定义另一个 Binder 抽象类处理来自 Client 的 Binder 请求数据包,其中最重要的成员是虚函数 onTransact()。该函数分析收到的数据包,调用相应的接口函数处理请求。

接下来采用继承方式以接口类和 Binder 抽象类为基类构建 Binder 在 Server 中的实体,实现基类里所有的虚函数,包括公共接口函数以及数据包处理函数:onTransact()。这个函数的输入是来自 Client 的 binder_transaction_data 结构的数据包。前面提到,该结构里有个成员 code,表示这次请求的接口函数的编号。onTransact() 将 case-by-case 地解析 code 值,从数据包里取出函数参数,调用接口类中相应的,已经实现的接口函数。函数执行完毕,如果需要返回数据,就再构建一个 binder_transaction_data 数据包,并将返回数据填入其中。

那么 Server 端的各个 Binder 实体的 onTransact() 又是什么时候调用呢?这就需要 Binder 驱动参与了。前面说过,Binder 实体需要在传输数据中以结构 flat_binder_object 的形式发送给其它进程才能建立 Binder 通信,而 Binder 实体的指针就存放在该结构的 binder 域和 cookie 域中。驱动会分析传输数据包 binder_transaction_data,并根据其中的 Binder 位置数组 offsets 和offsets_size,从传输数据中获取 flat_binder_object 形式的 Binder,为它创建位于内核中的 Binder 节点 binder_node,并将 Binder 实体的指针记录在该节点中。

如果接下来有其它进程向该 Binder 实体发送数据,驱动会根据 binder_node 节点中记录的信息,将 Binder 实体指针填入 binder_transaction_data 的 target.ptr 和 cookie 中,然后返回给接收线程。接收线程从数据包 binder_transaction_data 中取出该指针,reinterpret_cast 成 Binder 抽象类并调用 onTransact() 函数。由于这是个虚函数,不同的 Binder 实体中有各自的实现,从而可以调用到不同 Binder 实体提供的 onTransact()。

5.1.2 Binder 在 Client 端的表述 - Binder 引用

做为 Proxy 设计模式的一部分,Client 端的 Binder 同样要继承 Server 提供的公共接口类并实现公共函数。但这不是真正的实现,而是对远程函数调用的包装:将函数参数打包,通过 Binder 向 Server 发送请求并等待返回值。为此 Client 端的 Binder 还要知道 Binder 实体的相关信息,即对 Binder 实体的引用。该引用或是由请求 SMgr 得到的,这个叫实名 Binder 的引用;或是由另一个进程直接发送过来的,也叫匿名 Binder 的引用。

由于继承了同样的公共接口类,Client 端的 Binder 提供了与 Server 端的 Binder 一样的函数原型,使用户感觉不出 Server 是运行在本地还是远端。

Client 端的 Binder 中,公共接口函数的包装方式是:创建一个 binder_transaction_data 数据包,将其对应的函数编码填入 code 域,将调用该函数所需的参数填入 data.ptr.buffer 指向的缓存中,并指明数据包的目的地,就是已经获得的对 Binder 实体的引用,填入数据包的 target.handle 中。

注意这里和 Server 的区别:实际上 target 域是个联合体,包括 ptr 和 handle 两个成员,前者的有效使用方,是接收数据包的 Server,指向 Binder 实体对应的内存空间;后者的有效使用方,为请求方的 Client,存放 Binder 实体的引用,告知 Binder 驱动,数据包将路由给哪个 Server 进程的哪个 Binder 实体。数据包准备好后,通过调用 Binder 驱动接口函数 ioctl 发送出去。经过 BC_TRANSACTION / BC_REPLY 回合,完成函数的远程调用并得到返回值。

5.2 Binder 在传输数据中的表述

Binder 可以塞在数据包 binder_transaction_data 的有效数据中,跨越进程边界,从一个进程传递给另一个进程,这些传输中的 Binder 用结构体 flat_binder_object 来表示。

struct flat_binder_object {
	struct binder_object_header	hdr;
	__u32				flags;

	/* 8 bytes of data. */
	union {
		binder_uintptr_t	binder;	/* local object */
		__u32			handle;	/* remote object */
	};

	/* extra data associated with local object */
	binder_uintptr_t	cookie;
};
struct binder_object_header {
	__u32        type;
};

具体含义如下表所示:

成员

含义

binder_object_header hdr;

主要表明该 Binder 的类型,包括以下主要几种类型:

BINDER_TYPE_BINDER:表示传递的是 Binder 实体,并且指向该实体的引用都是强类型;

BINDER_TYPE_WEAK_BINDER:表示传递的是 Binder 实体,并且指向该实体的引用都是弱类型;

BINDER_TYPE_HANDLE:表示传递的是 Binder 引用,为强类型

BINDER_TYPE_WEAK_HANDLE:表示传递的是 Binder 引用,为弱类型

BINDER_TYPE_FD:表示传递的是文件形式的 Binder,详见下节

_u32 flags

该域只对第一次传递 Binder 实体时有效,因为此刻,驱动需要在内核中创建相应的实体节点,有些参数需要从该域取出:

第0-7位:代码中用 FLAT_BINDER_FLAG_PRIORITY_MASK 取得,表示处理本实体请求数据包的线程的最低优先级。当一个应用程序提供多个实体时,可以通过该参数调整分配给各个实体的处理能力。

第8位:代码中用 FLAT_BINDER_FLAG_ACCEPTS_FDS 取得,置 1 表示该实体可以接收其它进程发过来的文件形式的 Binder。由于接收文件形式的 Binder 会在本进程中自动打开文件,有些 Server 可以用该标志禁止该功能,以防打开过多文件

union {

binder_uintptr_t binder;

_u32 handle;

};

当传递的是 Binder 实体时使用 binder 域,指向 Binder 实体在应用程序中的地址;

当传递的是 Binder 引用时使用 handle 域,存放 Binder 在进程中的引用号

binder_uintptr_t cookie;

该域只对 Binder 实体有效,存放 Binder 实体在 Server 端的地址

无论是 Binder 实体还是 Binder 引用都从属于某个进程,所以该结构不能透明地在进程之间传输,必须经过驱动加工处理后路由。

例如当 Server 把 Binder 实体传递给 Client 时,在发送数据包中,flat_binder_object 中的 type 是 BINDER_TYPE_BINDER,binder 和cookie 指向 Server 进程 Binder 实体对象在用户空间的地址。如果透传给 Client 端将毫无用处,驱动必须对数据包中的这个 Binder 做修改:将 type 改成 BINDER_TYPE_HANDLE;为这个 Binder 在 Client 进程中创建位于内核中的 Binder 引用,并将引用号填入 handle 中。如果数据包中是引用类型的 Binder,也就是 flat_binder_object 中的 type 是 BINDER_TYPE_HANDLE,也要做类似转换。经过处理后,接收进程从数据包中取得的 Binder 引用才是有效的,才可以将其填入数据包 binder_transaction_data 的 target.handle 域,后续向 Binder 实体发送请求服务。

这样做也是出于安全性的考虑:应用程序不能随便猜测一个引用号填入 target.handle 中,就可以向 Server 请求服务了,因为驱动并没有为你在内核中创建该引用,必定会被驱动拒绝。唯有经过身份认证的,由"权威机构"(Binder 驱动)亲手授予你的 Binder 引用,才是合法的,能够使用的引用,因为这时驱动已经在内核中为你使用的 Binder 建立了合法的引用(binder_ref)。

下面总结了当 flat_binder_object 结构穿过驱动时驱动所做的操作:

Binder 类型( type 域)

Binder驱动的操作

BINDER_TYPE_BINDER

BINDER_TYPE_WEAK_BINDER

1)只有 Binder 实体所在的进程,才能发送该类型的 Binder。如果是第一次发送,驱动将创建 Binder 实体在内核中的节点 binder_node,并保存 binder,cookie,flag 域

2)驱动接着到目标进程中创建一个 Binder 引用,指向这个 Binder 实体,然后修改 type 为 BINDER_TYPE_HANDLE,并且把生成的引用号赋值给 handle,然后重新放入数据包中传递给接收进程,这里的接收进程,一般为 SMgr

BINDER_TYPE_HANDLE

BINDER_TYPE_WEAK_HANDLE

1)获得 Binder 引用的进程都能发送该类型 Binder。驱动根据 handle 域提供的引用号,查找之前建立在内核中的 Binder 引用。如果找到说明引用号合法,否则拒绝该发送请求

2)驱动接收到这个类型的 Binder 后,会修改此 Binder 的 type 为

BINDER_TYPE_BINDER,并且把 Binder 实体的指针赋值给flat_binder_object 的 binder 和 cookie.然后重新放入数据包中继续传递给接收方,这里的接收方,一般为 Server 端

BINDER_TYPE_FD

5.2.1 文件形式的 Binder

除了通常意义上用来通信的 Binder,还有一种特殊的 Binder:文件 Binder。

这种 Binder 的基本思想是:将文件看成 Binder 实体,进程打开的文件号看成 Binder 的引用。一个进程可以将它打开文件的文件号传递给另一个进程,从而另一个进程也打开了同一个文件,就象 Binder 的引用在进程之间传递一样。

一个进程打开一个文件,就获得与该文件绑定的打开文件号。从 Binder 的角度,Linux 在内核创建的打开文件描述结构 struct file是 Binder 的实体,打开文件号是该进程对该实体的引用。既然是 Binder 那么就可以在进程之间传递,故也可以用 flat_binder_object 结构将文件 Binder 通过数据包形式发送至其它进程,只是结构中 type 域的值为 BINDER_TYPE_FD,表明该 Binder 是文件 Binder。而结构中的 handle 域,则存放文件在发送方进程中的打开文件号。我们知道打开文件号是个局限于某个进程的值,一旦跨进程就没有意义了。这一点和 Binder 实体指针或 Binder 引用号是一样的,若要跨进程,同样需要 Binder 驱动做转换。驱动在接收 Binder 的进程空间创建一个新的打开文件号,将它与已有的打开文件描述结构 struct file 勾连上,从此该 Binder 实体又多了一个引用。新建的打开文件号,覆盖 flat_binder_object 中原来的文件号交给接收进程。接收进程利用它可以执行 read(),write() 等文件操作。

传个文件为啥要这么麻烦,直接将文件名用 Binder 传过去,接收方用 open() 打开不就行了吗?其实这还是有区别的:

1)首先对同一个打开文件共享的层次不同:使用文件 Binder 打开的文件,共享 Linux VFS 中的 struct file,struct dentry,struct inode 结构,这意味着,一个进程使用 read() / write() / seek() 改变了文件指针,另一个进程的文件指针也会改变;而如果两个进程分别使用同一文件名打开文件则有各自的 struct file 结构,从而各自独立维护文件指针,互不干扰。

2)其次是一些特殊设备文件,要求在 struct file 一级共享才能使用,例如 Android 的另一个驱动 ashmem,它和 Binder 一样也是 misc 设备,用以实现进程间的共享内存。一个进程打开的 ashmem 文件,只有通过文件 Binder 发送到另一个进程才能实现内存共享,这大大提高了内存共享的安全性,道理和 Binder 增强了 IPC 的安全性是一样的。

5.3 Binder 在驱动中的表述

驱动是 Binder 通信的核心,系统中所有的 Binder 实体,以及每个实体在各个进程中的引用都登记在驱动中;驱动需要记录 Binder 引用->实体之间多对一的关系;为引用找到对应的实体;在某个进程中为实体创建或查找到对应的引用;记录 Binder 的归属地(位于哪个进程中);通过管理 Binder 的强 / 弱引用创建 / 销毁 Binder 实体等等。一句话总结:Binder 驱动是 Binder 通信的核心。

驱动里的 Binder 是什么时候创建的呢?前面提到过,为了实现 Binder 的注册,系统需要一个 SMgr 进程,用于实现对实名 Binder 的 Binder 实体的注册。

既然创建了 Binder 实体,就要有对应的 Binder 引用:驱动将所有进程中的 0 号引用,都预留给了 SMgr 的 Binder 实体,即所有进程的 0 号引用,都指向了 SMgr 对应的 Binder 实体,无须特殊操作即可以使用 0 号引用来获取 SMgr,然后通过这个 0 号引用来注册实名 Binder。

接下来随着应用程序不断地注册实名 Binder,不断向 SMgr 索要 Binder 的引用,不断将 Binder 从一个进程传递给另一个进程,越来越多的 Binder 以传输结构  flat_binder_object 的形式,穿越驱动做跨进程的迁徙。由于 binder_transaction_data 中 data.ptr.offsets 数组的存在,所有流经驱动的 Binder,都逃不过驱动的眼睛。

Binder 驱动将对这些穿越进程边界的 Binder 做如下操作:

检查传输结构 flat_binder_object 的 type 域,如果是 BINDER_TYPE_BINDER 或BINDER_TYPE_WEAK_BINDER 则创建 Binder 的实体;如果是 BINDER_TYPE_HANDLE 或BINDER_TYPE_WEAK_HANDLE 则创建 Binder 的引用;如果是 BINDER_TYPE_FD 则为进程打开文件,无须创建任何数据结构。随着越来越多的 Binder 实体或引用在进程间传递,驱动会在内核里创建越来越多的节点 (binder_node) 或引用 (binder_ref),当然这个过程对用户来说是透明的。

5.3.1 Binder 实体在驱动中的表述

驱动中的 Binder 实体也叫‘节点’,隶属于提供实体的进程,由 binder_node 结构来表示:

struct binder_node {
	int debug_id;
	spinlock_t lock;
	struct binder_work work;
	union {
		struct rb_node rb_node;
		struct hlist_node dead_node;
	};
	struct binder_proc *proc;
	struct hlist_head refs;
	int internal_strong_refs;
	int local_weak_refs;
	int local_strong_refs;
	int tmp_refs;
	binder_uintptr_t ptr;
	binder_uintptr_t cookie;
	struct {
		/*
		 * bitfield elements protected by
		 * proc inner_lock
		 */
		u8 has_strong_ref:1;
		u8 pending_strong_ref:1;
		u8 has_weak_ref:1;
		u8 pending_weak_ref:1;
	};
	struct {
		/*
		 * invariant after initialization
		 */
		u8 sched_policy:2;
		u8 inherit_rt:1;
		u8 accept_fds:1;
		u8 min_priority;
	};
	bool has_async_transaction;
	struct list_head async_todo;
};

成员

含义

int debug_id;

用于调试

struct binder_work work;

当本节点引用计数发生改变,需要通知所属进程时,通过该成员挂入所属进程的 to-do 队列里,唤醒所属进程执行 Binder 实体引用计数的修改

union {

struct rb_node rb_node;

struct hlist_node dead_node;

};

每个进程都维护一棵链接所有 binder 节点的红黑树,以 Binder 实体在用户空间的指针,即本结构的 ptr 成员为索引存放该进程所有的 Binder 节点。这样驱动可以根据 Binder 实体在用户空间的指针很快找到其位于内核的节点。rb_node 用于将本节点链入该红黑树中;

销毁节点时须将 rb_node 从红黑树中摘除,但如果本节点还有引用没有切断,就用 dead_node 将节点隔离到另一个链表中,直到通知所有进程切断与该节点的引用后,该节点才可能被销毁

struct binder_proc *proc;

本成员指向 Binder 节点所属的进程,一般指提供 Binder 实体的 Server 端进程

struct hlist_head refs;

本成员是队列头,所有指向本 Binder 节点的引用,都链接在该队列里。这些引用可能隶属于不同的进程。通过该队列可以遍历所有指向该 Binder 节点的所有 Binder 引用

int internal_strong_refs;

用以实现强指针的计数器:产生一个指向本节点的强引用该计数就会加 1。一般指进程外引用本节点的引用

int local_weak_refs;

驱动为传输中的 Binder 设置的弱引用计数。进程内部的引用。

int local_strong_refs;

驱动为传输中的 Binder 设置的强引用计数。进程内部的引用。

int tmp_refs 临时引用

binder_uintptr_t ptr;

指向用户空间 Binder 实体的指针,来自于 flat_binder_object 的 binder 成员

binder_uintptr_t cookie;

指向用户空间的 Binder 实体指针,来自于 flat_binder_object 的 cookie 成员

u8 has_strong_ref:1;

u8 pending_strong_ref:1;

u8 has_weak_ref:1;

u8 pending_weak_ref:1;

这一组标志用于控制驱动与 Binder 实体所在进程交互式修改引用计数

u8 accept_fds

表明节点是否同意接受文件方式的 Binder,来自 flat_binder_object 中 flags 成员的 FLAT_BINDER_FLAG_ACCEPTS_FDS 位。由于接收文件 Binder 会为进程自动打开一个文件,占用有限的文件描述符,节点可以设置该位拒绝这种行为

u8 min_priority

设置处理 Binder 请求的线程的最低优先级。发送线程将数据提交给接收线程处理时,驱动会将发送线程的优先级也赋予接收线程,使得数据即使跨了进程也能以同样优先级得到处理。不过如果发送线程优先级过低,接收线程将以预设的最小值运行。

该域的值来自于 flat_binder_object 中 flags 成员

bool has_async_transaction;

该成员表明该节点在 to-do 队列中有异步交互尚未完成。驱动将所有发送往接收端的数据包,暂存在接收进程或线程开辟的 to-do 队列里。对于异步交互,驱动做了适当流控:如果进程或线程的 to-do 队列里有异步交互任务且尚未处理完毕,则该成员置为1,这将导致新到的异步交互存放在本结构成员:async_todo队列中,而不直接放到进程或线程的 to-do 队列里。目的是为同步交互让路,避免长时间阻塞发送端,因为同步交互需要等待数据的返回,所以比异步优先级要高

struct list_head async_todo

异步交互等待队列;用于分流发往本节点的异步交互包

每个进程都有一棵红黑树 nodes,用于存放创建好的 Binder 节点,以 Binder 在用户空间的指针作为索引。每当在传输数据中侦测到一个代表 Binder 实体的 flat_binder_object 对象,先以该结构的 binder 指针为索引搜索红黑树;如果没找到就创建一个新节点添加到树中。由于对于同一个进程来说内存地址是唯一的,所以不会重复建设造成混乱。

5.3.2 Binder 引用在驱动中的表述

和 Binder 节点一样,Binder 引用也是驱动根据传输数据中的 flat_binder_object 创建的,隶属于获得该引用的进程,用 binder_ref 结构体表示:

struct binder_ref {
	/* Lookups needed: */
	/*   node + proc => ref (transaction) */
	/*   desc + proc => ref (transaction, inc/dec ref) */
	/*   node => refs + procs (proc exit) */
	struct binder_ref_data data;
	struct rb_node rb_node_desc;
	struct rb_node rb_node_node;
	struct hlist_node node_entry;
	struct binder_proc *proc;
	struct binder_node *node;
	struct binder_ref_death *death;
};
struct binder_ref_data {
	int debug_id;
	uint32_t desc;
	int strong;
	int weak;
};

成员

含义

int debug_id;

调试用

uint32_t desc;

引用号也就是句柄值,进程内唯一,并且按顺序递增

int strong;

强引用计数

int weak;

弱引用计数

struct rb_node rb_node_desc;

每个进程有一棵红黑树 refs_by_desc,进程所有引用以引用号(即本结构的 desc 域)为索引添入该树中。本成员用做链接到该树的一个节点。

struct rb_node rb_node_node;

每个进程又有一棵红黑树 refs_by_node,进程所有引用以 Binder 节点在驱动中的内存地址(即本结构的 node 域)为索引添入该树中。本成员用做链接到该树的一个节点。

struct hlist_node node_entry;

该域将本引用做为节点链入所指向的 Binder 节点 binder_node 中的 refs 队列中

struct binder_proc *proc;

本引用所属的进程

struct binder_node *node;

本引用所指向的 binder_node 节点

struct binder_ref_death *death;

应用程序向驱动发送 BC_REQUEST_DEATH_NOTIFICATION 或  BC_CLEAR_DEATH_NOTIFICATION 命令,从而当 Binder 实体销毁时能够收到来自驱动的死亡通知。该域不为空表明用户订阅了对应实体销毁的死亡通知

就像一个对象有很多指针一样,同一个 Binder 实体可能有很多引用,不同的是这些引用可能分布在不同的进程中。和 Binder 节点一样,每个进程使用红黑树存放所有正在使用的引用。不同的是 Binder 的引用可以通过两个键值索引:

  • 对应 Binder 节点在内核中的地址。注意这里指的是驱动创建于内核中的 binder_node 结构的地址,而不是 Binder 实体在用户进程中的地址。Binder 节点在内核中的地址是唯一的,用做索引不会产生二义性;但实体可能来自不同用户进程,而实体在不同用户进程中的地址可能重合,不能用来做索引。驱动利用该红黑树在一个进程中快速查找某个 Binder 节点所对应的引用(一个 Binder 节点在一个进程中只建立一个引用)。
  • 引用号。引用号是驱动为引用分配的一个32位标识,在一个进程内是唯一的,而在不同进程中可能会有同样的值。引用号将返回给应用程序,可以看作 Binder 引用在用户进程中的句柄。除了0号引用,在所有进程里都固定保留给了 SMgr,其它值由驱动动态分配。向 Binder 发送数据包时,应用程序将引用号填入 binder_transaction_data 结构的 target.handle 域中,表明该数据包的目的 Binder。驱动根据该引用号在红黑树中找到引用的 binder_ref ,进而通过其 node 域知道目标 Binder 节点所在的进程及其它相关信息,实现数据包的路由。

六 Binder 内存映射和接收缓存区管理

暂且撇开 Binder,考虑一下,在传统的 IPC 方式中,数据是怎样从发送端到达接收端的呢?通常的做法是,发送方将准备好的数据存放在缓存区中,调用 API 通过系统调用进入内核中。内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中,并唤醒接收线程,完成一次数据发送。

这种"存储-转发"机制有两个缺陷:首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux 使用 copy_from_user() 和 copy_to_user() 实现这两次跨空间的拷贝,在此过程中如果使用了高端内存(high memory),这种拷贝还需要临时建立 / 取消页面映射,造成性能损失。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间,或先调用 API 请求,然后获得消息体大小,再开辟适当的空间,接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder 采用一种全新的策略:由 Binder 驱动负责管理数据接收缓存。我们注意到 Binder 驱动实现了 mmap() 系统调用,这对字符设备是比较特殊的,因为 mmap() 通常用在有物理存储介质的文件系统上,而像 Binder 这样没有物理介质,纯粹用来通信的字符设备没必要支持 mmap()。Binder 驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间。先看mmap() 是如何使用的:

fd = open("/dev/binder", O_RDWR);

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

这样 Binder 的接收方就有了一片大小为 MAP_SIZE 的接收缓存区。mmap() 的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必也不能直接访问(映射类型为PROT_READ,只读映射)。

接收缓存区映射好后就可以做为缓存池接收和存放数据了。前面说过,接收数据包的结构为 binder_transaction_data,但这只是消息头,真正的有效负荷位于 data.ptr.buffer 所指向的内存中。这片内存不需要接收方提供,恰恰是来自 mmap() 映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是存放 binder_transaction_data 结构本身,以及  BR_TRANSACTION / BR_REPLY 等所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

有分配就必然有释放。接收方在处理完数据包后,就要通知驱动释放 data.ptr.adb buffer 所指向的内存区。在介绍 Binder 协议时已经提到,这是由命令 BC_FREE_BUFFER 完成的。

通过上面的介绍可以看到,驱动为接收方分担了最为繁琐的任务:分配 / 释放大小不等,难以预测的有效负荷缓存区,而接收方只需要提供缓存来存放大小固定,最大空间可以预测的消息头即可。在效率上,由于 mmap() 分配的内存是映射在接收方用户空间里的,所以总体效果就相当于,对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,提升了一倍的性能。顺便再提一点,Linux 内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用 copy_from_user() 拷贝到内核空间,再用 copy_to_user() 拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,mmap() 分配的内存,除了映射进了接收方进程的用户空间里,还映射进了内核空间。所以调用 copy_from_user() 将数据拷贝进内核空间,也相当于拷贝进了接收方的用户空间,这就是 Binder 只需一次拷贝的秘密。

七 Binder 接收线程管理

Binder 通信,归根结底是位于不同进程中的线程之间的通信。假如进程 S 是 Server 端,提供 Binder 实体,线程 T1 从 Client 进程 C 中通过 Binder 的引用向进程 S 发送请求。S 为了处理这个请求需要启动线程 T2,而此时线程 T1 处于接收返回数据的等待状态。T2 处理完请求就会将处理结果返回给 T1,T1 被唤醒得到处理结果。在这过程中,T2 仿佛 T1 在进程 S 中的代理,代表 T1 执行远程任务,而给 T1 的感觉就像是穿越到 S 中执行一段代码又回到了 C。为了使这种穿越更加真实,驱动会将 T1 的一些属性赋给 T2,特别是 T1 的优先级,这样 T2 会使用和 T1 类似的时间完成任务。很多资料用"线程迁移"来形容这种现象,容易让人产生误解。一来线程根本不可能在进程之间跳来跳去,二来 T2 除了和 T1 优先级一样,其它没有相同之处,包括身份,打开文件,栈大小,信号处理,私有数据等。

对于 Server 进程 S,可能会有许多 Client 同时发起请求,为了提高效率往往开辟线程池并发处理收到的请求。怎样使用线程池来实现并发处理呢?这和具体的 IPC 机制有关。

拿 socket 举例,Server 端的 socket 设置为侦听模式,有一个专门的线程使用该 socket,侦听来自 Client 的连接请求,即阻塞在 accept() 上。这个 socket 就像一只会生蛋的鸡,一旦收到来自 Client 的请求就会生一个蛋 —— 创建新 socket 并从 accept() 返回。侦听线程从线程池中启动一个工作线程并将刚下的蛋交给该线程。后续业务处理就由该线程完成并通过这个线程与 Client 实现交互。

可是对于 Binder 来说,既没有侦听模式也不会下蛋,怎样管理线程池呢?一种简单的做法是,不管三七二十一,先创建一堆线程,每个线程都用 BINDER_WRITE_READ 命令读 Binder。这些线程会阻塞在驱动为该 Binder 线程设置的等待队列上,一旦有来自 Client 的数据,驱动会从等待队列中,唤醒一个线程来处理。这样做简单直观,省去了线程池,但一开始就创建一堆线程有点浪费资源。于是 Binder 协议引入了专门的命令帮助 Binder 驱动管理线程池,包括:

  • INDER_SET_MAX_THREADS
  • BC_REGISTER_LOOP
  • BC_ENTER_LOOP
  • BC_EXIT_LOOP
  • BR_SPAWN_LOOPER

首先要管理线程池就要知道池子有多大,应用程序通过 INDER_SET_MAX_THREADS 告诉驱动,最多可以创建几个线程。以后每个线程在创建,进入主循环,退出主循环时,都要分别使用 BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP 告知驱动,以便驱动标记当前线程池各个线程的状态。每当驱动接收完数据包,并且把数据包返回给读 Binder 线程的用户空间时,都要检查一下,线程池中是不是已经没有闲置线程了。如果是,并且线程总数还没有达到线程池设定的最大线程数,就会在当前读出的数据包后面再追加一条 BR_SPAWN_LOOPER 消息,告诉 Server 端,线程即将不够用了,请再启动一个新线程,否则下一个请求可能不能及时响应。新线程一启动,又会通过 BC_xxx_LOOP 告知驱动更新状态。这样确保了只要线程池的线程数量没有耗尽,总是会有空闲的线程在等待队列中随时待命,及时处理请求。

关于工作线程的启动,Binder 驱动还做了一点小小的优化。当进程 P1 的线程 T1,向进程 P2 发送请求时,驱动会先查看一下线程 T1 是否也正在处理来自 P2 某个线程的请求,但尚未完成(没有发送回复)。这种情况通常发生在两个进程都有 Binder 实体,并且互相向对方发送请求。假如驱动在进程 P2 中,发现了这样的线程,比如说 T2,就会要求 T2 来处理 T1 的这次请求。因为 T2 既然向 T1 发送了请求尚未得到返回包,说明 T2 肯定(或将会)阻塞在读取返回包的状态中。这时候可以让 T2 顺便做点事情,总比等在那里闲着好。而且如果 T2 不是线程池中的线程,还可以为线程池分担部分工作,以降低线程池的负担。

八 数据包接收队列与等待队列管理

通常数据传输的接收端有两个队列:数据包接收队列和(线程)等待队列,用以缓解供需矛盾。

当超市里的进货(数据包)太多,货物会堆积在仓库里;购物结帐的人(线程)太多,会排队等待在收银台,道理是一样的。在驱动中,每个进程有一个全局的接收队列,也叫 to-do 队列,用来存放不是发往特定线程的数据包;相应地有一个全局等待队列,所有等待从全局接收队列里接收数据包的线程在该队列里排队。每个线程有自己私有的 to-do 队列,存放发送给该线程的数据包;相应的每个线程都有各自私有等待队列,专门用于本线程等待接收自己 to-do 队列里的数据,这个队列虽然名叫队列,其实线程私有等待队列中最多只有一个线程,即它自己。

由于发送时没有特别标记,驱动怎么判断哪些数据包该送入全局 to-do 队列,哪些数据包该送入特定线程的 to-do 队列呢?这里有两条规则。规则1:Client 发给 Server 的请求数据包,驱动会判断数据包的 flags 是否是 IF_ONE_WAY,如果是异步的,那么直接提交到 Server 进程的全局 to-do 队列。如果是同步的,就会使用到之前谈到的, Binder对工作线程启动的优化。经过优化,来自 T1 的请求并不会提交给 P2 的全局 to-do 队列,而是送入了 T2 的私有 to-do 队列。规则2:对同步请求的返回数据包(由 BC_REPLY 发送的包)都会发送到发起请求的线程的私有 to-do 队列中。如上面的例子,如果进程 P1 的线程 T1,发给进程 P2 的线程 T2 的是同步请求,那么 T2 返回的数据包,将送进 T1 的私有 to-do 队列,而不会提交到 P1 的全局 to-do 队列。

数据包进入接收队列的潜规则,也就决定了,线程进入等待队列的潜规则,即一个线程只要不接收返回数据包,则应该在全局等待队列中等待新任务,否则就应该在其私有等待队列中,等待 Server 的返回数据。还是上面的例子,T1 在向 T2 发送同步请求后,就必须等待在它私有等待队列中,而不是在 P1 的全局等待队列中排队,否则将得不到 T2 的返回的数据包。

这些潜规则,是驱动对 Binder 通信双方施加的限制条件,体现在应用程序上,就是同步请求交互过程中的线程一致性:

1)Client 端:等待返回包的线程,必须是发送请求的线程,而不能由一个线程发送请求包,另一个线程等待接收包,否则将收不到返回包。

2)Server 端:发送对应返回数据包的线程,必须是收到请求数据包的线程,否则返回的数据包,将无法送交到发送请求的线程。这是因为返回数据包的目的 Binder 不是用户指定的,而是驱动记录在收到请求数据包的线程里,如果发送返回包的线程,不是收到请求包的线程,驱动将无从知晓返回包将送往何处。

接下来探讨一下 Binder 驱动,是如何递交同步交互和异步交互的。我们知道,同步交互和异步交互的区别是同步交互的请求端(Client)在发出请求数据包后,须要等待应答端(Server)的返回数据包,而异步交互的发送端发出请求数据包后,交互即结束。

对于这两种交互的请求数据包,驱动都可以不管三七二十一,统统丢到接收端的全局 to-do 队列中一个个处理。但驱动并没有这样做,而是对异步交互做了限流,令其为同步交互让路,具体做法是:

对于某个 Binder 节点,只要有一个异步交互还没有处理完毕,例如正在被某个线程处理,或还在任意一条 to-do 队列中排队,那么接下来发给该节点的异步交互包,将不再投递到本进程或本线程的 to-do 队列中,而是阻塞在驱动为该节点开辟的异步交互接收队列(Binder 节点的 async_todo 域)中,但这期间同步交互依旧不受限制直接进入进程或线程的 to-do 队列排队等待处理。一直到该异步交互处理完毕,下一个异步交互,方可脱离异步交互队列 async_todo,进入全局 to-do 队列中。之所以要这么做,是因为同步交互的请求端,需要等待返回包,必须迅速处理完毕并返回,以免影响请求端的响应速度,而异步交互属于"发送后不管",稍微延时一点也不会阻塞其它线程。所以用专门的 async_todo 队列,将过多的异步交互暂存起来,以免突发大量的异步交互任务,挤占 Server 端的处理能力或耗尽线程池里的线程,进而阻塞同步交互。

九 总结

Binder 使用 Client/Server通信方式,安全性好,简单高效,再加上其面向对象的设计思想,独特的接收缓存管理和线程池管理方式,使之成为 Android 系统进程间通信的不二选择。

你可能感兴趣的:(Android,framework)