《Android Bander设计与实现 - 设计篇》——校注
本篇旨在对作者universus在《Android Bander设计与实现 - 设计篇》中表述错误的地方做修正。
Android Binder设计与实现 - 设计篇
摘要
Binder是Android系统提供的进程间通信(IPC)方式之一。Linux已经拥有管道、system V IPC消息队列/共享内存/信号量)和socket等IPC手段,确还是依赖Binder实现进程间通信,说明Binder具有无可比拟的优势。深入了解Binder并将之与传统IPC做对比有助于我们深入领会进程间通信的实现和性能优化。本文将对Binder的设计细节做一个全面的阐述,首先我们通过介绍Binder通信模型和Binder通信协议了解Binder的设计,然后分别阐述Binder在系统不同部分的表述方式和起的作用,最后还会解释Binder在数据接收端的设计考虑,包括线程池管理、内存映射和等待队列管理等。
1 引言
基于Client-Server的通信方式广泛的应用于互联网和数据库访问。Android系统中为了向应用开发者提供丰富的功能,这种通信方式也是无处不在,从媒体播放、音视频捕捉,到监听传感器都由不同的Server负责管理,应用程序只需作为Client与这些Server建立连接遍可以使用这些服务。因为Client和Server分处不同进程,所以Client-Server方式的广泛使用对进程间通信(IPC)机制的可靠性是一个挑战。目前Linux支持的IPC方式中只有socket支持Client-Server的通信方式。当然也可以在其他底层机制上架设一套协议来实现Client-Server通信,但这样增加了系统的复杂性,在手机这种条件复杂、资源稀缺的环境下通信的可靠性也难以保证。
另一方面是传输性能。socket作为一款通用接口,其传输效率低、开销大,主要用于跨网络的进程间通信和本机进程间的低速通信。消息队列和管道采用的存储-转发机制,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少要两次拷贝过程。共享内存虽然没有拷贝过程,但是因为其控制复杂并且需要其他IPC机制保证进程间同步,所以难以使用。
表 1 各种IPC方式拷贝次数
IPC | 数据拷贝次数 |
---|---|
共享内存 | 0 |
Binder | 1 |
Socket/管道/消息队列 | 2 |
还有一点是出于安全性考虑。Android作为一个开放式,拥有众多开发者的平台,应用程序的来源广泛,确保终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据、连接无线网络以及长期操作底层设备导致电池很快耗尽等等。传统IPC没有任何安全措施,完全依赖上层协议确保。
首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从未无法鉴别对方身份。Android为每个应用程序分配自己的UID,同时也为应用程序的每个进程分配PID,所以进程的UID和PID是鉴别进程身份的重要标志。传统IPC只能由用户填写UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标示只能由IPC机制本身在内核添加。
其次传统IPC访问的接入点是开放的,无法建立私有通道。比如命名管道的名称、System V 的键值或者socket的ip地址和端口号都是开放的,只要知道了这些接入点的程序都可以对端建立连接,不管怎样都无法阻止程序通过猜测接收端地址获得连接。(注:Binder机制的IPC过程中,Server可以通过对外提供API文档也是可以为满足条件的Client提供开放式接入点的,比如Google官方以及其他第三方平台提供的服务,此处作者想说明的是Client在不知道Server对外提供的接入点的情况下是无法通过穷举来建立与Server的连接的。)
基于以上原因,Android需要建立一套新的IPC机制满足系统对通信方式传输性能和安全的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需要一次拷贝,为发送方添加UID/PID ,既支持实名Binder也支持匿名Binder,安全性高。
2 面向对象的Binder IPC
Binder使用Client-Server通信方式:一个进程作为Server提供服务,多个进程作为Client想Server发起服务请求,获得所需的服务。
想实现Client-Server通信就必须实现以下两点:
- Server必须有确定的访问接入点或者说地址来接收Client的请求,并且Client可以通过某种途径获知Server的地址;
- 指定通信协议来传输数据。
以上两点可以用网络通信比喻,网络通信中Server的访问接入点就是Server主机的IP地址+端口号,传输协议为TCP/UDP协议。对Binder机制而言,Binder就可以看做Server提供给外部访问的接入点,Client通过这个接入点向Server发送请求来使用服务。
与其他IPC不同,Binder使用了面向对象的思想来描述访问的接入点——Binder,及其在Client中的入口。Binder是一个位于Server的实体对象,该对象提供了一套方法来实现Client对Server的请求,这就是类的成员函数。Client中持有的访问Server的入口是指向这个Binder对象的指针,一旦Client获得了这个指针就可以调用它所指向的Binder对象的方法从而访问Server。在Client看来,它通过Binder指针调用远程Server的方法和通过指针调用本地进程的方法并没有区别,尽管前者指向的是存在于远程Server的实体对象,而后者是的实体在本地内存中。“指针(Pointer)”是C/C++的术语,Java中的说法是“引用(Reference)”,即Client通过Binder的引用访问Server,而软件领域的另一个术语“句柄(Handle)”也可以用来表述Binder在Client中的存在方式。从通信的角度看,Client中的Binder也可以看做是Server Binder的“代理(Proxy)”,在本地代表远端Server为Client提供服务。本文中会使用“引用(Reference)”或“句柄(Handle)”这两个被广泛使用的术语。
面向对象思想的引入将进程间通信的过程转化为Client通过Binder对象的引用来调用该对象中成员函数的过程,而其独特之处在于Binder对象是一个可以跨进程引用的对象,他的实体位于一个进程,而它的引用却遍布于各个进程中。最诱人的是,这个引用和Java里的一样既可以是强类型,也可以是弱类型(Weak Reference),而且可以从一个进程传给其它进程,让大家都能访问同一个Server,就像将一个对象的引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行在同一个面向对象的程序之中。形形色色的Binder对象以及星罗棋布的Binder引用仿佛粘结各个进程的胶水,这也是Binder在英语里的原意。
当然面向对象只是针对同样面向对象的应用程序而言,Binder驱动和内核其它模块一样也是使用C语言实现的,没有类和对象的概念,Binder驱动为面向对象的进程间通信提供了底层支持。
3 Binder通信模式
Binder框架定义了四个角色:Server、Client、ServiceManager(以下简称SMgr)以及Binder驱动。其中Server、Client和SMgr运行于用户空间,Binder驱动运行于内核空间。这四个角色的关系和互联网类似: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驱动的代码位于linux目录的drivers/misc/binder.c中。(注:Android是基于Linux内核开发的,Binder并不是Linux内核的一部分,得益于Linux的Loadable Kernel Module——动态内核可加载模块简称LKM机制,Linux中模块是具有独立功能的程序,它可以被单独编译但不能独立运行,想要运行它需要将它链接到内核作为内核的一部分运行。Android系统通过动态添加一个模块到内核空间,用户进程通过这个内核模块实现通信,这个运行在内核空间负责各个用户进程通过Binder通信的内核模块就叫做Binder驱动。)
3.2 ServiceManager 与实名Binder
和DNS类似,SMgr的作用是将字符形式的Binder名字转换成Client中对该Binder的引用,是的Client能通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就像每个网站除了有IP地址外还有自己的网址(域名)。Server创建了Binder实体,为其取了一个字符形式可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SMgr,驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及一个对这个实体的引用,将名字及新建的引用打包传递给SMgr,SMgr收到数据包后,从中取出名字和引用填入一张表中。
细心的读者可能会发现其中的蹊跷:SMgr是一个进程,Server是另一个进程,Server向SMgr注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好像蛋生鸡的前提是要有只鸡生蛋。Binder的实现比较巧妙,SMgr和其它进程同样采用Binder通信,SMgr是Server端,有自己的Binder对象或者说Binder实体,其它进程都是Client(包括待注册的为其它Client提供服务的Server在SMgr看来都是Client),Client需要通过这个Binder的引用来实现Binder的注册和查询。SMgr提供的Binder实体比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成SMgr时Binder驱动会为他创建Binder实体节点(注:这个实体节点与Server向SMgr发起注册请求时Binder驱动为它创建的实体节点一样,只是创建的过程不一样,SMgr的节点是由BINDER_SET_CONTEXT_MGR命令创造,其它Server的节点是发起注册时创造)。Client想得到这个Binder的引用也不需要从别处获得,所有Client中的0号引用就是SMgr中Binder实体的引用。类比网络通信,0号引用就是域名服务器DNS的服务器地址,已经预先配置好了。也就是说,一个Server若要向SMgr注册自己的Binder就必须通过0这个引用号和SMgr的Binder通信。
3.3 Client获得实名Binder的引用
Server通过0号向SMgr注册了“名字”和对应的Binder实体后,Client也利用0号引用向SMgr请求指定名字对应的Binder引用。SMgr收到这个链接请求,从请求数据包获得Binder的名字,在注册表中找到该名字对应的条目,再从条目中取出Binder的引用,将该引用作为回复发送给Client。从面向对象的角度看,这个Server的Binder对象现在有了两个引用,一个位于SMgr中,另一个位于发起请求的Client中,如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就像Java里的一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型的,从而确保只要有引用指向Binder对象,这个对象就不会被释放掉。
3.4 匿名Binder
并不是所有Binder都需要注册给SMgr广而告之的。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder建立的。由于这个后创建的Binder没有在SMgr注册名字,所以是个匿名Binder。Client将会收到这个匿名Binder的引用,通过这个匿名Binder引用向位于Server中的Binder实体发送请求。(注:匿名Binder使Server可以创建多个可被Client引用的Binder实体甚至是不同类型的多种Binder实体,相对的实名Binder一个Server只能有一个就是 onBinder 方法返回的Binder。或许这是Android出于多态的考虑,比方说Server可以提供多种服务,但是不希望只用一个Binder实体实现,因为那样意味着客户端会收到一个过度冗余的Binder引用,除了自己需要的方法外还有许多自己用不到的方法,所以Server干脆注册一个实名Binder用来给Client提供匿名Binder的选择,Client可以根据自己的需要选择获取Server端对应的那个匿名Binder使用用这个匿名Binder代理服务)。
下图展示了参与Binder通信的所有角色,将在以后章节中一一提到。
Binder通信示例.jpg
4 Binder协议
Binder协议基本格式是(命令+数据),使用ioctl(fd,cmd,arg)函数实现交互。参数cmd是命令,参数arg时数据,arg随cmd不同而不同。下表列举了所有命令及其所对应的数据:
表 2 Binder通信命令字
cmd | 含义 | arg |
---|---|---|
BINDER_WRITE_READ | 向Binder写入或读取数据,参数分为两段:写部 分和读部分。如果write_size不为0将先将 write_buffer里的数据写入Binder;如果read_size 不为0再从Binder中读取数据存入read_buffer中。 write_consumed和read_consumed表示操作完成 后Binder驱动实际写入或读出的数据个数。 |
struct binder_write_read { signed long write_size; signed long write_consumed; unsigned long write_buffer; signed long read_size; signed long read_consumed; unsigned long read_buffer; }; |
BINDER_SET_MAX_THREADS | 告知Binder驱动,接收方(通常是Server端)线程 池中最大线程数。由于Client是并发向Server端发 送请求的,Server端必须开辟线程池为这些并发请 求提供服务。告知驱动线程池的最大线程数是为了 让驱动发现线程数达到该值时不要再命令接收端启 动新的线程 |
int max_threads; |
BINDER_SET_CONTEXT_MGR | 将当前进程注册为SMgr。系统中同时只能存在一个 SMgr。只要当前的SMgr没有调用close()关闭Binder 驱动就不能有别的进程可以成为SMgr。 |
|
BINDER_THREAD_EXIT | 通知Binder驱动当前线程退出了。Binder驱动会为 所有参与Binder通信的线程(包括Server线程池中 的线程和Client发出请求的线程)建立相应的数据 结构。这些线程在退出时必须通知驱动释放相应的 数据结构。 |
|
BINDER_VERSION | 获取Binder驱动的版本号。 |
这其中最常用的命令是BINDER_WRITE_READ。该命令的参数包括两部分:一部分是向Binder(Binder驱动,确切地说是向Binder驱动为Server中Binder实体创建的位于Binder驱动的入口节点)写入数据,一部分是要从Binder读出数据,Binder驱动先处理写部分再处理读部分。这样安排的好处是应用程序可以很灵活地处理命令的同步或异步。例如若发送异步命令可以只填入写部分而将read_size置为0;若要只从Binder获取数据可以将写部分置空即write_size置为0;若要发送请求并同步等待返回的数据可以将两部分都置上。
4.1 BINDER_WRITE_READ之写操作
Binder写操作的格式也是(命令+数据)。这时命令和数据都存放在binder_write_read结构的write_buffer域指向的内从空间里,多条命令可以连续存放。数据紧接着存放在命令后面,根据命令不同而不同。下表列举了BInder写操作支持的命令:
表 3 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的应用计数,用以实现强、弱引用的功能 | 32位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通知驱动线程池中一个线程已经创建了;BC_ENTER_LOOPER通知驱动该线程已经进入主循环,可以接受数据;BC_EXIT_LOOPER通知驱动该线程退出主循环,不再接受数据。 | |
BC_REQUEST_DEATH_NOTIFICATION | 获得Binder引用的进程通过该命令要求驱动在Binder实体被销毁时得到通知。虽说强引用可以确保只要有引用就不会销毁实体,但这毕竟是个跨进程的引用,谁也无法保证Server会因为某种原因销毁这个实体。 | unit32 *ptr; 需要得到销毁通知的Binder引用 void **cookie; 与销毁通知相关的消息,驱动会在发出销毁通知时返回给发出请求的进程。 |
BC_DEAD_BINDER_DONE | 收到Binder实体销毁通知的进程删除引用后用本命令告知驱动。 | void **cookie; |
在这些命令中最常用的是BC_TRANSACTION/BC_REPLY命令对,Binder请求和应答数据就是通过这对命令发送给接收方的。这对命令承载的数据包邮结构体 struct binder_transaction_data 定义。Binder交互有同步和异步之分,利用 binder_transaction_data 的 flag 域区分。如果 flag 域的 TF_ONE_WAY 位为1则为异步交互,即 Client端发送完请求交互即结束,Server端不在返回 BC_REPLY 数据包;否则Server会返回 BC_REPLY 数据包,Client端必须等待接收完该数据包才算是完成了一次交互。
4.2 BINDER_WRITE_READ:从Binder读出数据
从Binder里读出的数据格式和想Binder中写入数据的格式一样,采用(消息ID+数据)形式,并且多条消息可以连续存放。下表列举了读出的消息及其相应的参数:
表 4 binder读操作消息ID
消息 | 含义 | arg |
---|---|---|
BR_ERROR | 发生内部错误(如内存分配失败) | |
BR_OK BR_NOOP |
操作完成 | |
BR_SPAWN_LOOPER | 该消息用于接收方线程池管理,当驱动发现接收方所有线程都处于忙碌状态且线程池里的线程总数没有超过BINDER_SET_MAX_THREADS设置的最大线程数时,向接收方发送该命令要求创建更多的线程以备接收数据。 | |
BR_TRANSACTION BR_REPLY |
这对消息分别对应发送方的BC_TRANSACTION和BC_REPLY,表示当前接收的数据是请求还是回复。 | struct binder_transaction_data |
BR_ACQUIRE_RESULT BR_ATTEMPT_ACQUIRE BR_FINISHED |
尚未实现 | |
BR_DEAD_BINDER BR_CLEAR_DEATH_NOTIFICATION_DONE |
向获得Binder引用的进程发送Binder实体销毁通知;收到销毁通知的进程接下来会返回BC_DEAD_BINDER_DONE。 | void **cookie;在使用BC_REQUEST_DEATH_NOTIFICATION注册销毁通知时的附加信息。 |
BR_FAILED_REPLY | 如果发送非法引用号则返回该消息 |
和写数据一样,其中最重要的消息是BR_TRASACTION/BR_REPLY,表明收到的格式为binder_transaction_data的到底是请求数据包(BR_TRANSACTION)还是返回数据包(BR_REPLY)。
4.3 struct binder_transaction_data:收发数据包结构
该结构是Binder接收/发送数据包的标准格式,每个成员定义如下:
表5 Binder收发数据包结构:binder_transaction_data
成员 | 含义 |
---|---|
union { size_t handle; void *ptr; } target; |
对于发送数据包的一方,该成员指明发送的目的地。由于目的是远端, 所以这里填写的是对Binder实体的引用,存放在handle中。如前所述, Binder的引用在代码中也叫做句柄(handle)。数据包经过驱动时, 驱动会根据handle找到对应的Binder实体也就是Binder对象内存的指针, 存入ptr中,该指针是接收方在将Binder实体传输给其它进程时(可以是 向SMgr注册Binder,也可以是接收双方通过已建立的连接传递Binder) 提交给驱动的,驱动程序能够将发送方填入的引用转换成接收方Binder 对象的指针,所以接收方可以直接将其当做对象指针来使用(通常将其 reinterpret_cast成相应类)。 |
void *cookie; | 发送方忽略该成员;接收方收到数据包时,该成员存放的是创建Binder 实体时由该接收方自定义的任意数值,作为与Binder指针相关的额外信 息存放在驱动中,驱动基本上不关心该成员。 |
unsigned int code; | 该成员存放收发双方约定的命令码,通常是Server端定义的公共接口函 数的编码,Server就是用这个编码确定Client调用的是哪个方法的,驱 动完全不关心该成员的内容。 |
unsigned int flags; | 与交互相关标志位,其中最重要的是 TF_ONE_WAY 位,如果该位为1 表明这次交互是异步的,Client端不关心方法的执行以及执行后的返回值 ,发送线程也不会被阻塞,驱动则不会构建与返回值相关的数据结构, 所以Server端的方法即便有返回值也不会被传递到Client端。另一位 TF_ACCEPT_FDS 是出于安全考虑,如果发起请求的一方不希望收到 回复中有文件形式的Binder则可将该位置为1,因为收到一个文件形式的 Binder会自动为接收方打开一个文件,使用该位可以防止打开过多文件。 |
pid_t sender_pid; uid_t sender_uid; |
这两个成员分别存放发送方的进程ID PID和用户ID UID,接收方可以通过 读取该成员获取发送发的身份,这两个成员是由驱动填写的,以防止应用 程序通过篡改ID伪造身份。 |
size_t data_size | 该成员表示 data.buffer 指向的缓冲区存放的数据长度。发送数据时由 发送方填入,表示即将发送的数据长度,用来告知接收方数据的长度。 |
size_t offsets_size | 驱动一般情况下不关心 data.buffer 里存放什么数据,但其中如果有Binder指针 则需要将其在 data.buffer 中的偏移位置告诉驱动。因为有可能存在多个Binder指针, 所以需要一个数组存放所有偏移位。本成员表示该数组的长度。 |
union { struct { const void *buffer; const void *offsets; } ptr; unit8_t buf[8]; } data; |
用于存放发送或者接收到的数据,data.offsets 中的元素指向Binder在 buffer内存中的偏移位,该数据组可以位于 data.buffer 中,也可以在另 外的内存空间中,并无限制。buf[8]是为了保证无论是32位机还是64位 机,成员 data 的大小都是8个字节。 |
这里必须再强调一下 offsets_size 和 data.offsets 两个成员,这是Binder通信有别于其它IPC的地方。如前所述,Binder采用面向对象的设计思路,一个Binder实体的指针可以发送给其它进程从而建立许多跨进程的引用,另外,这些引用也可以在进程之间传递,就像Java里将一个引用赋给另一个引用一样。为Binder在不同进程建立引用必须有驱动参与,由驱动在内核创建并注册相关的数据结构后接收方才能使用该引用。而且这些引用可以是强类型的,需要驱动为其维护引用计数。然而这些跨进程的Binder混杂在应用程序发送的数据包中,数据格式由用户定义,如果不把他们一一标示出来告知驱动,驱动将无法从数据中将他们提取出来。于是就使用数组 data.offsets 存放用户数据中每个Binde在 data.buffer 中的偏移量,用 offset_size 标示这个数组的长度。驱动在发送数据包时会根据 data.offsets 和 offsets_size 将散落在data.buffer 中的Binder找出来并一一为他们创建相关的数据结构。在数据包中传输的Binder是类型为 struct flat_binder_object 的结构体,详见后文。
对于接收方来书,binder_transaction_data 只相当于一个定长的信息头,真正的用户数据存放在 data.buffer 所指向的缓存区中。如果发送方在数据中内嵌了一个或多个Binder,接收到的数据包中同样会用 data.offsets 和 offsets_size 指出每个Binder的位置和总数,不过通常接收方可以忽略这些信息,因为接收方是知道数据格式的(注:序列化与反序列化),参考双方的格式定义就能知道Binder在什么位置。
5 Binder 的表述
考察一次Binder通信的全过程会发现,Binder会出现在系统的以下几部分中:
- 应用程序进程:分别位于Server进程和Client进程中;
- 传输数据:由于Binder可以跨进程传递,需要在传输数据中予以表述;
- Binder驱动:分别管理Server端的Binder实体和Client端的Binder引用。
在系统的不同部分,Binder实现的功能不同,表现形式也不一样,都要一个数据结构来承载,或者是object或者是struct。接下来逐一介绍Binder在各部分所扮演的角色和使用的数据结构。
5.1 Binder在应用程序中的表述
虽然Binder用到了面向对象的思想,但并不限制应用程序一定要使用面向对象语言,无论是C语言还是C++语言都可以很容易的使用Binder来通信。例如尽管Android主要使用Java/C++,像SMgr这么重要的进程就是使用C语言实现的。不过面向对象的表述方式更方便,所以本文假设应用程序是用面向对象语言实现的。
Binder本质上只是一种通信方式,和具体服务没有关系。为了提供具体服务,Server必须提供一套接口函数以便Client通过远程调用方式使用各种服务。这时通常采用Proxy设计模式:将接口函数定义在一个抽象类中,Server和Client都会以该抽象类为基类实现所有接口函数,所不同的是Server端是真正功能实现类,而Client只是对这些函数的远程调用请求的包装。如何将Binder和Proxy设计模式结合起来是应用程序实现面向对象Binder通信需要解决的根本问题。
(注:研究Binder在Server/Client端的表述最好的办法还是研究由IDE根据AIDL文件生成的 .java类,研究这个类还可以帮助我们理解Binder在Server/Client端的工作过程。)
5.1.1 Binder在Server端的表述——Binder实体
作为Proxy设计模式的基础,首先定义一个接口类封装Server所有功能,其中包含一系列纯虚函数留待Server和Proxy各自实现。定义了接口类后就要引入Binder了。为Server端定义一个Binder抽象类处理来自Client的Binder请求数据包。由于上述接口中的那些函数需要跨进程调用,所以需要为其一一编号,以便Server可以根据收到的编号决定调用哪个函数。Binder抽象类中最重要的成员是虚函数onTransact(),该函数分析收到的数据包,调用相应的接口函数处理请求。其次是asBinder(),这个方法用于返回Server端的Binder引用,如果Client与Server在相同进程直接返回Server端的引用,如果在不同进程则返回Server端引用的代理。
onTransact()方法需要特别介绍一下,方法的原型是public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
这是个参数都源于驱动对binder_transaction_data数据包的解析,code就是上面所讲的编号,用来区分Client端调用的是Server端的哪个方法,数值由双方约定;data是Client需要传递给Server的数据(如果有的话);reply是Server端方法执行完成后的返回值(如果有的话);flags是一个标记位,在 4.3一节介绍binder_transaction_data包结构时有介绍。在onTransact()方法中会 case-by-case 的比对code,发现与code匹配的方法时就会调用Server端的这个方法并传入由data反序列化得到的参数,onTransact()会接收Server端方法执行完成后的返回值并在序列化之后存入reply中。
那么各个Binder实体的onTransact()方法又是什么时候调用的呢?这就需要驱动参与了。前面说过,Binder实体需要以Binder传输结构flat_binder_object形式发送给其它进程才能建立Binder通信,而Binder实体指针就存放在该结构体的handle域中。驱动根据Binder位置数组从传输数据中获取该Binder的传输结构,为它创建位于内核中的Binder节点,将Binder实体指针记录在该节点中(注:这里也可以提前预知,驱动中的“Binder实体”、“节点”、“实体节点”都是指这个Server端Binder实体的指针)。如果接下来有其他进程向该Binder发送数据,驱动会根据节点记录的信息将Binder实体指针填入 binder_transaction_data 的 target.ptr 域中发送给接收进程,接收进程从数据包中取出该指针,reinterpret_cast(C++提供的类型转化运算符)成Binder抽象类型的引用并调用onTransact()方法,由于这是个虚函数,不同的Binder实现类有各自的实现,所以可以调用到不同Binder实体提供的onTransact()方法。
5.1.2 Binder在Client端的表述-Binder引用
作为Proxy设计模式的一部分,Client端的Binder同样要实现Server端提供的公共接口类并实现接口方法,但这并不是真正的实现,而是对远程方法的包装:将方法参数序列化打包,再将打包好的数据通过Binder引用发送给Server端并等待返回值(如果是同步方法且有返回值的话)。为此Client端还要知道Server端Binder实体的相关信息,即对Binder实体的引用。该引用由SMgr返回或者从以建立的链接中索取(匿名Binder)。
由于实现了同样的公共接口,Client Binder提供了与Server Binder一样的函数原型(注:这里需要特别注意,Client端的Binder并没继承android.os.Binder类,它只是实现了根据Server对外提供的服务而定义的公共接口,所以确切地说Client端这个类并不是Binder类,作者为了行文方便使用Client Binder这个名词不可以理解为Client端也实现了一个Binder类,而Server Binder确实继承自android.os.Binder它是一个名副其实的Binder类),使用户感觉不出Server是运行在本地还是远端。Client Binder中公共接口方法的包装方式是:调用 transact() 方法,传入code、data、reply以及flags,如果是远程调用方法会进入Binder驱动中,驱动会创建一个 binder_transaction_data 数据包,将调用该方法所需的参数填入 data.buffer指向的缓存中并指明数据包的目的地,那就是Client已经获得的对Server端Binder实体的引用,填入数据包的 taget.handle 中,注意这里和Server的区别:target 域有两个成员 ptr 和 handle,前者指针指向的是Server端Binder实体所在的内存空间,Server通过它就可以找到Binder实体然后执行远程需要调用的方法;后者存放Client端持有的Binder引用,用于告知驱动将数据包路由给哪个Binder实体。数据包准备好后驱动就可以根据 taget.handle 将数据包发送至Server端,经过 BC_TRANSACTION/BC_REPLY 回合完成方法的远程调用并得到返回值。
5.2 Binder在传输数据中的表述
Binder可以塞在数据包中跨进程传递,这些传输中的Binder用结构 flat_binder_object 表示,其结构如下所示:
表 6 Binder传输结构:flat_binder_object
成员 | 含义 |
---|---|
unsigned long type | 表明Binder的类型,包括以下几种: BINDER_TYPE_BINDER:表示传递的是Binder实体,并且指向该实体的引用都是强类型; BINDER_TYPE_WEAK_BINDER:表示传递的是BInder实体,并且指向该实体的引用都 是弱类型的; BINDER_TYPE_HANDLE:表示传递的是Binder强类型的引用; BINDER_TYPE_WEAK_HANDLE:表示传递的是Binder弱类型的引用; BINDER_TYPE_FD:表示传递的是文件形式的Binder,详见 5.2.1节。 |
unsigned long flags | 该域只对第一次传递Binder实体时有效,因为此时驱动需要在内核中创建相应的实体节点 ,有些参数需要从该域取出: 第0-7位,表示处理本实体请求数据包的线程的最低优先级,当一个应用程序提供多个实体 时,可以通过该参数调整分配给各个实体的处理能力; 第8位置1表示该实体可以接收其他线程发过来的文件形式的Binder,由于接收文件形式的 Binder会在接收方进程中自动打开文件,有些接收方可以用该位标示禁止此功能,以防止 打开过多文件。 |
union { void *binder; signed long handle; }; |
当传递的是Binder实体时使用binder域,指向Binder实体在应用程序中的地址; 当传递的是Binder引用时使用handle域,存放Binder在进程中的引用号(注意 这里不是指针,而是 signed long型,说明这是个编号,所以作者用“引用号”来表述)。 |
void *cookie | 该域只对Binder实体有效,存放与该实体有关的附加信息。 |
无论是Binder实体还是对实体的引用都从属于某个进程,所以该结构不能透明地在进程间传递,必须经过驱动翻译。例如当Server想把Binder实体传递给Client时,在发送数据流中,flat_binder_object 中的 type 是 BINDER_TYPE_BINDER,binder域指向Server进程用户空间地址,对Client来说是毫无用处的(进程隔离),驱动必须对数据流中的这个Binder做修改:先将 type 改为 BINDER_TYPE_HANDLE,然后为这个Binder的接收进程创建位于内核中的引用并将引用号填入handle中。
这样做也是出于安全考虑的:应用程序不能通过编造引用的方式向Server请求服务了。因为引用是由内核创建且在内核中是有记录的,用户猜测的引用在内核中是找不到的,必定会被驱动拒绝。唯有经过身份认证确认合法的、由Binder驱动亲自授予的Binder引用才能使用。
下表总结了当 flat_binder_object 结构穿过驱动时驱动所做的操作:
表 7 驱动对 flat_binder_object 的操作
Binder类型(type域) | 对发送方数据的操作 | 对接收方数据的操作 |
---|---|---|
BINDER_TYPE_BINDER BINDER_TYPE_WEAK_BINDER |
只有发送方所在进程才能发送该类型的Binder。如果是第一次发送,驱动将为实体创建位于内核的节点并保存binder、cookie、flags域 | 如果是第一次接收该Binder则创建实体在内核中的引用;将handle域替换为新建的引用;将type替换为BINDER_TYPE_(WEAK_)HANDLE。 |
BINDER_TYPE_HANDLE BINDER_TYPE_WEAK_HANDLE |
获得Binder引用的进程都能发送该类型的引用。驱动根据handle域在内核以注册的引用中检索,如果找到了说明合法,否则拒绝请求。 | 如果引用对应的Binder实体位于接收进程则将binder域替换为保存在节点中的binder值;cookie、flags也替换为节点中保存的;type替换为BINDER_TYPE_(WEAK_)BINDER。 如果Binder实体不在接收进程并且是第一次接收则在内核中新建一个引用并填入handle中。 |
BINDER_TYPE_FD | 验证handle域中提供的文件号是否有效,无效则拒绝发送。 | 在接受方创建一个打开文件号将它与发送端提供的打开文件描述结构绑定。- |
(注:如何判断Binder引用对应的Binder实体是否在接收进程中将在 5.3节中介绍。)
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的进程空间创建一个新的打开文件号,将它与已有的打开文件描述结构 struct file 勾连上,该Binder实体(文件)就多了一个引用(打开文件号)。新建的打开文件号覆盖 flat_binder_object 中原来的文件号交给接收进程。接收进程利用它可以执行 read()、write()等文件操作。
传个文件为啥要这么麻烦?直接将文件名用Binder传过去,接收方用 open() 打开不就行了吗?其实这还是有区别的。首先对同一个打开文件共享的层次不同:使用文件Binder打开的文件共享Linux VFS中的 struct file、struct dentry、struct inode结构,这意味着进程使用 read()、write()、seek()改变了文件指针,另一个进程的文件指针也会改,而如果两个进程分别使用同一个文件名打开文件则有各自的 struct file 结构,从而各自独立维护文件指针,接收方不能在发送方操作基础上继续操作。其次是一些特殊设备文件要求在 struct file 一级共享才能使用,例如Android的另一个驱动ashmem,它和Binder一样是misc设备,用以实现进程间的共享内存,一个进程打开的ashmem文件只有通过文件Binder发送到另一个进程才能实现内存共享,这大大提高了内存共享的安全性,道理和Binder增强了IPC的安全性是一样的。
5.2.2 内存共享与Binder (注:这一节为补充内容)
由于需要传输的数据需要先拷贝到内核空间,所以Binder传输数据的大小是有限制的,每个进程分配的空间不足1M:
#define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))
但是实际使用时限制会更小,因为Binder通信是可以并发的,多个线程共用这个不足1M的空间,所以分配给每个线程的空间会更小且不确定,一旦传输的数据超出了剩余空间大小系统就会抛出 android.os.TransactionTooLargeException 异常。所以当需要传输较大数据时(比如传递一个较大的Bitmap)可以考虑使用共享内存的方式。
下面给出示例代码:
发送方:
try {
// MemoryFile是对native的封装,构造函数的第一个参数是虚拟文件的名字可以为null,第二个参数是文件的大小
MemoryFile memoryFile = new MemoryFile("share_memory", 1024);
memoryFile.getOutputStream().write(new String("hello share memory").getBytes());
// 由于MemoryFile的getFileDescriptor方法是@hide的,所以需要用反射的方式获得FileDescriptor
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
FileDescriptor fd = (FileDescriptor) method.invoke(memoryFile);
memoryFile.getOutputStream().close();
// mBinder是已建立的Binder连接;shareMemory是在AIDL接口中定义的用于传递ParcelFileDescriptor的方法
// 使用dup方法是为了将FileDescriptor序列化
mBinder.shareMemory(ParcelFileDescriptor.dup(fd));
} catch (IOException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
接收方:
// sf 是发送方穿过来的ParcelFileDescriptor
FileDescriptor fileDescriptor = sf.getFileDescriptor();
FileInputStream fileInputStream = new FileInputStream(fileDescriptor);
byte[] buf = new byte[32];
try {
fileInputStream.read(buf);
fileInputStream.close();
Log.d("shareMemory", new String(buf));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
由示例可以看出用Binder共享内存与共享文件使用的方法类似,二者都是通过文件描述符操作数据的,不同点在于共享内存方式传递的文件描述符是通过MemoryFile得到的。MemoryFile是对底层ashmem(Anonymous Shared Memory匿名共享内存)的封装,ashmem开辟一段共享内存后使用mmap建立用户空间与共享内存的映射并返回与映射关联的文件描述符,通信双方就可以通过对这个文件描述符的读写完成数据传递了。
5.3 Binder在驱动中的表述
驱动是Binder通信的核心,系统中所有Binder实体及每个实体在各个进程中的引用都登记在驱动中;驱动需要记录Binder引用和实体间多对一的关系;为Binder实体创建位于内核空间的节点;记录Binder实体位于哪个进程中;为引用找到对应的实体;为Binder实体创建或查找对应的引用;通过管理Binder的强/弱引用创建/销毁Binder实体等等。
驱动里的Binder(实体节点和引用)是什么时候创建的呢?前面提到过,为了实现实名Binder的注册,系统必须创建SMgr用于注册实名Binder的Binder实体,负责实名Binder注册过程中的进程间通信。既然创建了实体就要有对应的引用:驱动将所有进程中的0号引用都预留给SMgr的Binder实体,无须特殊操作即可使用0号引用注册实名Binder。接下来随着应用程序不断地注册实名Binder,不断向SMgr索要Binder引用,不断将Binder引用从一个进程传递给另一个进程,越来越多的Binder以 flat_binder_object 结构穿越驱动做跨进程的迁徙。由于 binder_transaction_data 中 data.offset 数组的存在,所有流经驱动的Binder都逃不过驱动的眼睛。Binder将对这些穿越进程边界的Binder进行操作,操作详解请见 表7。随着越来越多的Binder实体或引用在进程间传递,驱动会在内核中创建越来越多的节点或引用。
5.3.1 Binder实体在驱动中的表述
驱动中的Binder实体也叫节点,隶属于提供实体的进程,由 struct binder_node表示。
表 8 Binder节点描述结构:binder_node
成员 | 含义 |
---|---|
int debug_id; | 用于调试 |
struct binder_work work; | 当节点引用数发生变化,需要通知节点所属进程,将该成员加入所属进程的 todo队列里,唤醒所属进程执行引用计数修改。 |
union { struct rb_node rb_node; struct hlist_node dead_node; }; |
驱动为每个进程都维护一颗红黑树,用于根据Binder实体在用户空间的指针 查找节点,rb_node是红黑树上的一个节点。 Binder实体已销毁,如果还有引用没有切断,就用dead_node将该节点放到 另一个哈西表中。 |
struct binder_proc *proc; | 指向所属的进程,即提供该节点的Binder实体所属进程。 |
struct hlist_head refs; | 该成员是队列头,所有指向该节点的引用都连接在该队列中。这些引用 可能隶属不同的进程。通过该队列可以遍历指向该节点的所有引用。 |
int internal_strong_refs; | 用以实现强指针的计数器:每产生一个指向本节点的强引用该计数加1. |
int local_weak_refs; | 驱动为传输中的Binder设置的弱引用计数。如果一个Binder打包在数据包 中从一个进程发送到另一个进程,驱动会为该Binder增加引用计数,直到 接收进程通过BC_FREE_BUFFER通知驱动释放该数据包的数据区为止。 |
int local_strong_refs; | 驱动为传输中的Binder设置的强引用计数。操作如上 |
void __user *ptr; | Binder实体在用户空间的指针,来自于flat_binder_object的binder成员。 |
void __user *cookie; | 附加信息在用户空间的指针,来自于flat_binder_object的cookie成员。 |
unsigned has_strong_ref; unsigned pending_strong_ref; unsigned has_weak_ref; unsigned pending_eak_ref; |
这组标志用于控制驱动与Binder实体艘在进程交互式修改引用计数。 |
unsigned has_async_transaction; | 该成员表明该节点的todo队列中有异步交互尚未完成。驱动将所有发往 接收端的数据包暂存在接收进程开辟的todo队列中。对于异步交互驱动 做了适当的流控:如果todo队列里有异步交互尚待处理则该成员置1, 这将导致新发送来的异步交互被存放在本结构的 asynch_todo队列中, 而不直接发送到todo队列,目的是为同步交互让路,避免长时间阻塞 同步交互的发送端。(注:引入这个标志位也是为了让异步交互得以执行 ,因为异步交互要执行就必然要被放到todo队列中。) |
unsigned accept_fds; | 表明节点是否同意接收文件形式的Binder,来自flat_binder_object的flags 成员的FLAT_BINDER_FLAG_ACCEPTS_FDS位。由于接收文件Binder会 为进程自动打开一个文件,占用有限的文件描述符(一般是1024个), 节点可以根据该成员的值判断否以拒绝这种行为。 |
int min_priority | 设置处理Binder请求的线程的最低优先级,发送线程将数据提交给接收线程 处理时,驱动会将发送线程的优先级也赋予给接收线程,使得数据即使跨 进程也能以同样优先级得到处理。不过如果发送线程优先级过低,接收线程 将以预设的这个最小值运行。该域的值来自于flat_binder_object的flags成员。 |
struct list_head async_todo | 异步交互等待队列,用于分流。 |
(注:内核为每个进程创建了一个描述Binder上下文信息的结构体binder_proc存放诸如:用于处理用户请求的线程所构成的红黑树-threads;binder实体红黑树-nodes;binder引用红黑树-refs_by_node;进程映射的物理内存在内核空间的起始位置-*buffer;该进程的待处理事件队列-todo;等待队列-wait,以及最大线程数-max_thread等信息。struct rb_root nodes;
就是其中用于存储Binder实体节点binder_node的红黑树的根节点,struct rb_root refs_by_node;
就是维护下一节要介绍的的用于描述Binder引用的结构体binder_ref的红黑树的根节点。binder_proc详解)
每个进程都有一棵红黑树用于存放创建好的节点。每当驱动在传输数据中侦测到一个代表Binder实体的flat_binder_object,先以该结构体中的binder域为索引在红黑树上查找有没有与哪个节点的ptr域相同,如果没有就创建一个新的节点并添加到树上。由于同一个进程的内存地址是唯一的,所以不会因为重复创建造成混乱。
5.3.2 Binder引用在驱动中的表述
和实体一样,Binder的引用也是驱动根据传输数据中的flat_binder_object创建的,隶属于获得该引用的进程,用 struct binder_ref 表示:
表 9 Binder引用描述结构体:binder_ref
成员 | 含义 |
---|---|
int debug_id; | 用于调试 |
struct rb_node rb_node_desc; | 用于关联到refs_by_desc红黑树中;这棵树用引用号排序。 |
struct rb_node rb_node_node; | 用于关联到refs_by_node红黑树中;这棵树用Binder实体地址排序。 |
struct hlist_node node_entry; | 该域将本引用作为节点链入binder_node中的refs队列。 |
struct binder_proc *proc; | 本引用所属进程。 |
struct binder_node *node; | 本引用指向Binder实体在驱动中的节点,binder_node |
unit32_t desc; | 本引用的引用号 |
int strong; | 强引用计数。 |
int weak; | 弱引用计数。 |
struct binder_ref_death *death; | 应用程序向驱动发送BC_REQUEST_DEATH_NOTIFICATION或 BC_CLEAR_DEATH_NOTIFICATION命令从而能在Binder实体 销毁时收到来自驱动的提醒。该域不为空表明用户订阅了对应实体 销毁的通知。 |
就像一个对象有很多指针一样,同一个Binder实体可以有很多引用,不同的是这些引用分布在不同进程。Client端想通过自己持有的Binder引用发起远程调用时,驱动会先检索Client进程的binder_proc.refs_by_desc树中查找是否存在这个引用,如果不存在说明请求非法将被拒绝,如果存在则根据引用的binder_ref.node指针找到它所指向的Binder实体节点binder_node,得到的binder_node的ptr域存放的就Binder实体在其用户空间(Server)的指针了;与之相对的,Server端接到Client端发来的数据包后会根据binder_transaction_data.code判断Client调用的是哪个方法,待方法执行完后再取出包内的引用号根据引用号,根据引用号在内核中Binder实体节点binder_node的链表refs中找到对应的引用,将需要返回的数据填入reply中发送给驱动,驱动再通过引用转发给Client,经过这个流程就完成了一次远程调用。
7 Binder接收线程管理
Binder通信实际上是位于不同进程中的线程之间的通信。假定进程S是Server端,提供Binder实体,线程c1从Client进程C中通过Binder引用向进程S发送请求,S为了处理这个请求需要启动线程s1,而此时线程c1处于接收返回数据的等待状态。s1处理完请求就会将处理结果返回给c1,c1被唤醒并得到处理结果。在这个过程中,s1仿佛c1在进程S中的代理,代表c1执行远程服务,而给c1的感觉就像是穿越到进程S中执行了一段代码又回到了进程C。为了使这种穿越更加真实,驱动会将c1的一些属性赋给s1,特别是c1的线程优先级,这样s1会使用和c1类似的时间完成任务。很多资料会用“线程迁移”来形容这种现象,容易让人产生误解,一来线程根本不可能在进程之间跳来跳去,二来s1除了和c1优先级一样,其他没有相同之处,包括身份、打开文件、栈大小、信号处理和私有数据等。
由于Server进程S可能会有许多Client同时发起请求,为了提高效率往往会开辟线程池并发处理收到的请求。怎样使用线程池处理并发请求呢?这和具体的IPC机制有关。拿socket举例,Server端的socket设置为侦听模式,有一个专门的线程使用该socket侦听来自Client的连接请求,该线程阻塞在accept()方法上,这个socket就像一只会下蛋的鸡,一旦收到来自Client的请求就会生一个蛋——创建新的socket并通过accept()返回给监听线程,侦听线程从线程池中启动一个工作线程并将刚创建的socket交给线程,后续业务处理就由该线程完成并通过这个socket与Client实现交互。
可是对于Binder来书,既没有侦听模式也不会下蛋,那它怎么管理线程池呢?一种简单的做法是,不管三七二十一,先创建一堆线程,每个线程都用BINDER_WRITE_READ命令读Binder传来的数据。这些线程会阻塞在驱动为该Binder设置的等待队列(todo)上,一旦有来自Client的数据驱动就会从队列中唤醒一个线程来处理请求。这样做简单直观但一开始就创建一堆线程有点浪费资源,于是Binder协议引入了专门命令或消息帮助用户管理线程池,包括:
- BINDER_SET_MAX_THREADS
- BC_REGISTER_LOOPER
- BC_ENTER_LOOPER
- BC_EXIT_LOOPER
- BC_SPAWN_LOOPER
首先要管理线程池就要知道线程池有多大,应用程序通过BINDER_SET_MAX_THREADS告知驱动最多可以创建多少线程,之后每个线程在创建、进入主循环、退出主循环分别使用BC_REGISTER_LOOPER、BC_ENTER_LOOPER和BC_EXIT_LOOPER。每当驱动接收完数据包返回给读Binder的线程时都要检查一下是不是已经没有闲置的线程了,如果没有闲置线程了且线程总数没超出线程池最大值就会在当前读出的数据后面追加一条BR_SPAWN_LOOPER消息,告诉用户线程不够用了再启动一些。线程已启动又会发送BC_REGISTER_LOOPER消息告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,计时处理请求。
关于工作线程的启动,Binder驱动还做了一点有趣的优化。当进程P1的线程T1向进程P2发起请求时,驱动会先查看一下P2是否存在某个线程正在等待发往进程P1的请求。这种情况通常发生在两个进程都有Binder实体并互相调用时。假如驱动在进程P2中发现了这样的线程T2,就会要求T2来处理T1的这次请求。因为T2既然向T1发起了请求且尚未完成,说明T2肯定(或将会)阻塞在读取返回包的状态。这是可以让T2顺便做点事情,总比在那里闲着好。而且如果T2不是线程池中的线程还可以为线程池分担工作,减少线程池使用率。
8 数据包接收队列与(线程)等待队列管理
通常数据传输的接收端有两个队列:数据包接收队列和(线程)等待队列,用以缓解供需矛盾。就像当超市里的进的货(数据包)太多,货物会先堆积在仓库里;购物的人(线程)太多,会排队等待在收银台,道理是一样的。在驱动中,每个进程都有一个全局的接收队列——todo,存放不是发往特定线程的数据包;相应的有一个全局等待队列,线程在等待队列里等待接收数据。每个线程有自己的todo队列,存放发送给该线程的数据包;相应的每个线程都有各自私有的等待队列,专门用于本线程等待接收自己todo队列里的数据,虽然名为队列,其实只有自己在等待。
由于发送是没有特别标记,驱动怎么判断哪些数据该进入全局todo队列,哪些该送入特定线程的todo队列呢?这里有两个规则,规则1:Client发给Server的请求数据包都提交到Server进程的全局todo队列,不过有个特例,就是上一节谈到的Binder对工作线程启动的优化,经过优化,来自T1的请求不是提交给P2的全局todo队列,而是送入T2的私有todo队列;规则2:对同步请求的返回数据包(由BC_REPLY发送的包)都发送到发起请求的线程的私有todo队列中。如上面的例子,如果进程P1的线程T1发给进程P2的线程T2的是同步请求,那么T2返回的数据包将送进T1的私有todo队列而不是提交到P1的全局todo队列。
数据包进入接收队列的规则也就决定了线程进入等待队列的规则,即一个线程只要不接收返回数据包则应该在全局等待队列中等待新任务,否则就应该在其私有的等待队列中等待Server的返回数据包。
这些规则是驱动对Binder通信双方的限制条件,体现在应用程序上就是同步请求交互过程的一致性:1)Client端,等待返回数据包的线程必须是发送请求的线程,而不是由一个线程发送请求包,另一个线程等待接收数据包,否则将收不到返回数据包;2)Server端,发送对应返回数据包的线程必须是收到请求数据包的线程,否则返回的数据包将无法送交发送请求的线程。这是因为返回数据包的目的Binder不是发送返回数据包的进程指定的而是驱动记录在收到请求数据包的线程里,如果发送返回包的线程不是收到请求包的线程驱动将无从知晓返回包将送往何处。
接下来探讨一下Binder驱动是如何递交同步交互和异步交互的。我们知道,同步交互和异步交互的区别是同步交互的请求端(Client)在发出请求数据包后需要等待应答端(Server)的返回数据包,而异步交互的发送端在发送请求数据包后交互即结束发送线程不会被阻塞。对于这两种交互的请求数据包驱动可以不做区分统统丢到接收端的todo队列中一个一个处理,但是驱动没有这样做,而是对异步交互做了限流,另其为同步交互让路。具体做法是:对于某个Binder实体,只要接收端todo队列中有一个异步交互没有处理完毕,那么接下来所有发给该实体的异步交互将不再投递到todo队列中,而是投递到另一条用于处理异步交互的队列async_todo中,但这期间同步交互依然不受限制的直接进入todo队列,一直到todo队列中的异步交互执行完毕,下一个异步交互才能脱离async_todo队列进入todo队列。之所以这么做是因为同步交互的请求端需要等待返回包,必须迅速处理完以免影响请求端的响应速度,而异步交互属于“发射后不管”,稍微延迟一会儿不会有什么影响,所以用async_todo队列将除已经在todo队列内的那个异步交互以外的异步交互请求暂存起来,以免突发大量异步交互挤占Server端的处理能力或消耗线程池里的线程,进而阻塞同步交互。
9 总结
Binder使用Client-Server通信方式,相比于Linux传统的IPC方式来说安全性更好,更简单高效。再加上其面向对象的设计思想,独特的接收缓存管理和线程池管理方式,成为Android进程间通信的中流砥柱。
10 校注者总结
这篇博客是对原作者博客中存在的一些错漏进行校正和注解,虽然因为我自身能力的限制不能对所有错漏一一注解,但力求尽我所能让原博客的内容更加精到,同时也避免在校注过程中引入其它错误。Binder驱动的源码我只看了一部分,所以后续我还会给这篇博客继续增加内容,希望对大家都有帮助。
这里为大家再推荐另一篇或者说另一系列的博客Android Binder机制系列第一篇作者是wangkuiwu,他对Binder的讲解也很深入用心,大家可以对照两篇博客一起学习。