一、引言:为什么要采用Binder通信
基于Client-Server的通信方式广泛应用于从互联网和数据库访问到嵌入式手持设备内部通信等各个领域。智能手机平台特别是Android 系统中,为了向应用开发者提供丰富多样的功能,这种通信方式更是无处不在,诸如媒体播放,视音频频捕获,到各种让手机更智能的传感器(加速度,方位,温度,光亮度等)都由不同的Server负责管理,应用程序只需做为Client与这些Server建立连接便可以使用这些服务,花很少的时间和精力就能开发出令人眩目的功能。
linux拥有的IPC方式:
1、内存共享:控制复杂,不是Client-Server的通信方式,难于使用。
2、管道/消息队列:采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。不是Client- Server的通信方式。
3、Socket:是Client-Server的通信方式,但其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。
二、Binder通信模型
Binder框架定义了四个角色:Server,Client,ServiceManager(以后简称SMgr)以及binder驱动。其中 Server,Client,SMgr运行于用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似:Server是服务器,Client是客户终端,SMgr是域名服务器(DNS),驱动是路由器。
情景呈现:
SMgr成为域名服务器:需要向路由器声明,告诉路由器,我是域名服务器。
当一个服务器想成为服务器时,它通过路由器,访问域名服务器,并向域名服务器注册;
当客户端想访问server时,它先通过路由器,访问域名服务器,获取server的IP地址,然后通过路由器和server的IP地址,访问server.
1、Binder 驱动
和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备类型注册在设备目录 /dev下,用户通过/dev/binder访问该它。Binder驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,不提供 read(),write()接口,因为ioctl()灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用write()和 read()。
2、ServiceManager 与实名Binder
Server要成为Server,需要先连接路由,绑定Binder,即创建Binder实体,这个Binder实体也相当于该server的IP地址,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给 SMgr,通知SMgr注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及 SMgr对实体的引用,将名字及新建的引用传递给SMgr。SMgr收数据包后,从中取出名字和引用填入一张查找表中。
3 、Client 获得实名Binder的引用
Server向SMgr注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向SMgr请求访问某个Binder:我申请获得名字叫张三的Binder的引用。SMgr收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个 Binder对象现在有了两个引用:一个位于SMgr中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用 Binder实体就不会被释放掉。
4、Binder通信示例图
三、Binder 协议
Binder协议基本格式是(命令+数据),使用ioctl(fd, cmd, arg)函数实现交互。命令由参数cmd承载,数据由参数arg承载,随cmd不同而不同。
1、主要通信命令:
一、BINDER_WRITE_READ 含义:该命令向Binder写入或读取数据。参数分为两段:写部分和读部分。如果write_size不为0就先将write_buffer里的数据写入 Binder;如果read_size不为0就从Binder中读取数据存入read_buffer中。 参数arg: 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端必须开辟线程池为这些并发请求提供服务。告知驱动线程池的最大值是为了让驱动在线程达到该值时不要再命令接收端启动新的线程。 参数arg:int max_threads; 三、BINDER_SET_CONTEXT_MGR 含义:将当前进程注册为SMgr。系统中同时只能存在一个SMgr。只要当前的SMgr没有调用close()关闭Binder驱动就不能有别的进程可以 成为SMgr。 参数arg:无; 四、BINDER_THREAD_EXIT 含义:通知Binder驱动当前线程退出了。Binder驱动会为所有参与Binder通信的线程(包括Server线程池中的线程和Client发出请求的 线程)建立相应的数据结构。这些线程在退出时必须通知驱动释放相应的数据结构。 参数arg:无; 五、BINDER_VERSION 含义:获得Binder驱动的版本号。 参数arg:无;
2、BINDER_WRITE_READ 之写操作:怎么写,如何解析写缓冲区里的数据:写操作的数据格式同样也是(命令+数据)。
在通过BINDER_WRITE_READ这个命令进行写操作时,可以把命令和数据都存放在binder_write_read 结构write_buffer域指向的内存空间里,多条命令可以连续存放。数据紧接着存放在命令后面,格式根据命令不同而不同。下表列举了Binder写 操作支持的命令:
表 Binder写操作命令字
cmd |
含义 |
arg |
BC_TRANSACTION |
BC_TRANSACTION用于写入请求数据;BC_REPLY用于写入回复数据。其后面紧接着一个 binder_transaction_data结构体表明要写入的数据。 |
struct binder_transaction_data |
BC_ACQUIRE_RESULT |
暂未实现 |
— |
BC_FREE_BUFFER |
释放一块映射的内存。Binder接收方通过mmap()映射一块较大的内存空间,Binder驱动基于这片内存采用最佳匹配算法实现接收数据缓存的动态分配和释放,满足并发请求对接收缓存区的需求。 应用程序处理完这片数据后必须尽快使用该命令释放缓存区,否则会因为缓存区耗尽而无法接收新数据。 |
指向需要释放的缓存区的指针;该指针位于收到的Binder数据包中 |
BC_INCREFS |
这组命令增加或减少Binder的引用计数,用以实现强指针或弱指针的功能。 |
32位Binder引用号 |
BC_INCREFS_DONE |
第一次增加Binder实体引用计数时,驱动向Binder实体所在的进程发送BR_INCREFS, BR_ACQUIRE消息;Binder实体所在的进程处理完毕回馈BC_INCREFS_DONE,BC_ACQUIRE_DONE |
void *ptr:Binder实体在用户空间中的指针 void *cookie:与该实体相关的附加数据 |
BC_REGISTER_LOOPER |
这组命令同BINDER_SET_MAX_THREADS一道实现Binder驱动对接收方线程池管理。 BC_REGISTER_LOOPER通知 驱动线程池中一个线程已经创建了; BC_ENTER_LOOPER通知驱动该线程已经进入主循环,可以接收数据; BC_EXIT_LOOPER通知驱动该线程退出主循环,不再接收数据。 |
— |
BC_REQUEST_DEATH_NOTIFICATION |
获得Binder引用的进程通过该命令要求驱动在Binder实体销毁得到通知。 虽说强指针可以确保只要有引用就不会销毁实体,但这毕竟是个跨进程的引用,谁也无法保证实体由于所在的Server关闭Binder驱动或异常退出而消失,引用者能做的是要求Server在此刻给出通知。 |
uint32 *ptr; 需要得到死亡通知的Binder引用 void **cookie: 与死亡通知相关的信息,驱动会在发出死亡通知时返回给发出请求的进程。 |
BC_DEAD_BINDER_DONE |
收到实体死亡通知书的进程在删除引用后用本命令告知驱动。 |
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端必须等待接收完该数据包方才完成一次交互。
3、BINDER_WRITE_READ :从Binder读出数据
从Binder里读出的数据格式和向Binder中写入的数据格式一样,采用(消息ID+数据)形式,并且多条消息可以连续存放。下表列举了从 Binder读出的命令字及其相应的参数:
表 Binder读操作消息ID
消息 |
含义 |
参数 |
BR_ERROR |
发生内部错误(如内存分配失败) |
— |
BR_OK |
操作完成 |
— |
BR_SPAWN_LOOPER |
该消息用于接收方线程池管理。当驱动发现接收方所有线程都处于忙碌状态且线程池里的线程总数没有超过BINDER_SET_MAX_THREADS 设置的最大线程数时,向接收方发送该命令要求创建更多线程以备接收数据。 |
— |
BR_TRANSACTION |
这两条消息分别对应发送方的BC_TRANSACTION和BC_REPLY,表示当前接收的数据是请求或是回复。 |
binder_transaction_data |
BR_ACQUIRE_RESULT |
尚未实现 |
— |
BR_DEAD_REPLY |
交互过程中如果发现对方进程或线程已经死亡则返回该消息。 |
— |
BR_TRANSACTION_COMPLETE |
发送方通过BC_TRANSACTION或BC_REPLY发送完一个数据包后,都能收到该消息做为成功发送的反馈。这和BR_REPLY不一样, 是驱动告知发送方已经发送成功,而不是接收方返回请求数据。所以不管同步还是异步交互接收方都能获得本消息。 |
— |
BR_INCREFS |
这一组消息用于管理强/弱指针的引用计数。只有提供Binder实体的进程才能收到这组消息。 |
void *ptr:Binder实体在用户空间中的指针 void *cookie:与该实体相关的附加数据 |
BR_DEAD_BINDER |
向获得Binder引用的进程发送Binde实体死亡通知书; 收到死亡通知书的进程接下来会返回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、struct binder_transaction_data :收发数据包结构
struct binder_transaction_data { /*对于发送数据包的一方,该成员指明发送目的地。由于目的是在远端,所以这里填入的是对Binder实体的引用,存放在target.handle 中。如前述,Binder的引用在代码中也叫句柄(handle)。 当数据包到达接收方时,驱动已将该成员修改成Binder实体,即指向Binder对象内存的指针,使用target.ptr来获得。该指针是接收方在将Binder实体传输给其它进程时提交给驱动的,驱动程序能够自动将发送方填入的引用转换成接收方Binder对象的指针,故接收方可以直接将其当做对象指针来使用 */ union { size_t handle; void *ptr; } target; /*发送方忽略该成员;接收方收到数据包时,该成员存放的是创建Binder实体时由该接收方自定义的任意数值,做为与Binder指针相关的额外信息存放在驱动中。驱动基本上不关心该成员。*/ void *cookie; //该成员存放收发双方约定的命令码,驱动完全不关心该成员的内容。 //通常是Server端定义的公共接口函数的编号。 unsigned int code; /* 与交互相关的标志位,其中最重要的是TF_ONE_WAY位。如果该位置上表明这次交互是异步的,接收方不会返回任何数据。驱动利用该位来决定是否构建与返回有关的数据结构。 另外一位TF_ACCEPT_FDS是出于安全考虑,如果发起请求的一方不希望在收到的回复中接收文件形式的Binder可以将该位置为1。因为收到一个文件形式的Binder会自动为接收方打开一个文件,使用该位可以防止打开文件过多。 */ unsigned int flags; //该成员存放发送方的进程ID和用户ID,由驱动负责填入,接收方可以读取该成员获知发送方的身份。 pid_t sender_pid; uid_t sender_euid; //该成员表示data.buffer指向的缓冲区存放的数据长度 size_t data_size; /*驱动一般情况下不关心data.buffer里存放什么数据,但如果有Binder在其中传输则需要将其相对data.buffer的偏移位置指出来让驱动知道。有可能存在多个Binder同时在数据中传递,所以须用数组表示所有偏移位置。本成员表示该数组的大小。*/ size_t offsets_size; /*data.bufer指向存放要发送或接收到的数据的缓冲区;data.offsets指向Binder偏移位置数组,该数组可以位于data.buffer 中,也可以在另外的内存空间中,并无限制。buf[8]是为了无论保证32位还是64位平台,成员data的大小都是8个字节。 */ union { struct { const void *buffer; const void *offsets; } ptr; uint8_t buf[8]; } data; };
5、BINDER_WRITE_READ数据包实例
说明:binder对象flat_binder_object只在server注册、client获取服务的引用号时,在数据包中传输,即在建立连接时传输,在client同server通信时就不用传输了,只传输通信相关的数据。
四、Binder 的表述
考察一次Binder通信的全过程会发现,Binder存在于系统以下几个部分中:
· 应用程序进程:又分为Server进程和Client进程
· Binder驱动:Server和Client有不同表述形式
· 传输数据:由于Binder可以跨进程传递,需要在传输数据中予以表述
在系统不同部分,Binder实现的功能不同,表现形式也不一样的。接下来逐一探讨Binder在各部分所扮演的角色和使用的数据结构。
1、Binder 在Server端的表述 – Binder实体
-->定义抽象接口类,类中封装server所提供的服务函数,于这些函数需要跨进程调用,须为其一一编号。
-->定义Binder抽象类,处理来自Client的Binder请求数据包,最重要的成员是虚函数onTransact()。该函数分析收到的数据包,调用相应的接口函数处理请求
-->构建Binder在Server中的实体:以接口类和binder抽象类为基类构建,类中实现基类的公共接口函数和数据包处理函数
2、Binder 在Client端的表述 – Binder引用
-->Client端的Binder引用,要继承binder抽象类和Server提供的公共接口类并实现公共函数。但这不是真正的实现, 而是对远程函数调用的包装:将函数参数打包,通过Binder向Server发送申请并等待返回值。
-->Client端的Binder引用,还要包含server端Binder实体的相关信息,该信息从SMgr获取或直接获取。
-->公共接口函数实现方式:创建一个binder_transaction_data数据包,将其对应的编码填入code域,将调用该函数所需的参数填入data.buffer指向的缓存中,并指明数据包的目的地,那就是将已经获得的对Binder实体的引用,填入数据包的 target.handle中。数据包准备好后,通过驱动接口ioctl(fd, cmd, arg)发送出去。数据包准备好后,通过驱动接口发送出去。经过BC_TRANSACTION/BC_REPLY回合完成函数的远程调用并得到返回值。
3、Binder 在传输数据中的表述 - flat_binder_object
binder对象可以在进程间传递,以flat_binder_object这个结构,放到数据包的buffer中,flat_binder_object结构如下:
struct flat_binder_object { //表明该Binder的类型,BINDER_TYPE_BINDER->实体,BINDER_TYPE_HANDLE->引用 unsigned long type; unsigned long flags; union { //当传递的是Binder实体时使用binder域,指向Binder实体在应用程序中的地址。 void *binder; //当传递的是Binder引用时使用handle域,存放Binder在进程中的引用号。 signed long handle; }; //该域只对Binder实体有效,存放与该Binder有关的附加信息。 void *cookie; };
说明:无论是Binder实体还是对实体的引用都从属与某个进程,所以该结构不能透明地在进程之间传输,必须有驱动的参与。例如当Server把 Binder实体传递给Client时,在发送数据中,flat_binder_object中的type是 BINDER_TYPE_BINDER,binder指向Server进程用户空间地址。如果直接传给接收端将毫无用处,驱动必须对数据流中的这个 Binder做转换。跨进程传递地址没有任何意思!!
4、驱动是binder通信的核心
驱动是Binder通信的核心,系统中所有的Binder实体以及每个实体在各个进程中的引用都登记在驱动中;驱动需要记录Binder引用 ->实体之间多对一的关系;为引用找到对应的实体;为某个进程中的实体创建或查找到对应的引用;记录Binder的归属地(位于哪个进程中);
随着应用程序通过不断地注册实名Binder,不断向SMgr索要Binder的引用,不断将Binder从一 个进程传递给另一个进程,越来越多的Binder以传输结构 –flat_binder_object的形式穿越驱动做跨进程的迁徙。由于binder_transaction_data中data.offset数组 的存在,所有流经驱动的Binder都逃不过驱动的眼睛。Binder将对每个穿越进程边界的Binder做如下操作:检查传输结构的type域,如果是 BINDER_TYPE_BINDER或BINDER_TYPE_WEAK_BINDER则创建内核的Binder实体;如果是 BINDER_TYPE_HANDLE或BINDER_TYPE_WEAK_HANDLE则创建Binder的引用;如果是 BINDER_TYPE_FD则为进程打开文件,无须创建任何数据结构。
5、Binder 实体在驱动中的表述 - binder_node
驱动中的Binder实体也叫‘节点’,隶属于提供实体的进程,由struct binder_node结构来表示,结构略。
每个进程都有一棵红黑树用于存放创建好的节点,以Binder实体在用户空间的指针作为索引。每当在传输数据中侦测到一个代表Binder实体的 flat_binder_object,先以该结构的binder指针为索引搜索红黑树;如果没找到就创建一个新节点添加到树中。由于对于同一个进程来说内存地址是唯一的,所以不会重复建设造成混乱。
6、Binder 引用在驱动中的表述 - binder_ref
和实体一样,Binder的引用也是驱动根据传输数据中的flat_binder_object创建的,隶属于获得该引用的进程,用struct binder_ref结构体表示:
成员 |
含义 |
int debug_id; |
调试用 |
struct rb_node rb_node_desc; |
每个进程有一棵红黑树,进程所有binder引用以引用号(即本结构的desc域)为索引添入该树中。本成员用做链接到该树的一个节点。 |
struct rb_node rb_node_node; |
每个进程又有一棵红黑树,进程所有binder引用以内核binder节点在驱动中的内存地址(即本结构的node域)为索引添入该树中。本成员用做链接到该树的一个节点。 |
struct hlist_node node_entry; |
该域将本引用做为节点链入所指向的Binder节点结构binder_node中的refs队列 |
struct binder_proc *proc; |
本引用所属的进程 |
struct binder_node *node; |
本引用所指向的节点(内核Binder实体) |
uint32_t desc; |
本结构的引用号 |
int strong; |
强引用计数 |
int weak; |
弱引用计数 |
struct binder_ref_death *death; |
应用程序向驱动发送BC_REQUEST_DEATH_NOTIFICATION或BC_CLEAR_DEATH_NOTIFICATION命令从 而当Binder实体销毁时能够收到来自驱动的提醒。该域不为空表明用户订阅了对应实体销毁的‘噩耗’。 |
就象一个对象有很多指针一样,同一个Binder实体可能有很多引用,不同的是这些引用可能分布在不同的进程中。和实体一样,每个进程使用红黑树存放所有该进程正在使用的引用。但每个进程的Binder引用可以通过两个键值索引:
· 对应binder节点在内核中的地址。注意这里指的是驱动创建于内核中的binder_node结构的地址,而不是Binder实体在用户进程中的地址。实体在内核中的地址是唯一的,用做索引不会产生二义性;但实体可能来自不同用户进程,而实体在不同用户进程中的地址可能重合,不能用来做索引。驱动利用该红黑树在一个进程中快速查找某个内核Binder节点所对应的引用(一个实体在一个进程中只建立一个引用)。
· 引用号。引用号是驱动为引用分配的一个32位标识,在一个进程内是唯一的,而在不同进程中可能会有同样的值,这和进程的打开文件号很类似。引用号将返回给 应用程序,可以看作Binder引用在用户进程中的句柄。除了0号引用在所有进程里都保留给SMgr,其它值由驱动在创建引用时动态分配。向Binder 发送数据包时,应用程序通过将引用号填入binder_transaction_data结构的target.handle域中表明该数据包的目的 Binder。驱动根据该引用号在发送方进程的红黑树中找到引用的binder_ref结构,进而通过其node域知道对应的内核binder节点,然后再从binder节点的proc域知道目标Binder实体所在的进程及其它相关信息,实现数据包的路由。
五、Binder 内存映射和接收缓存区管理-mmap
mmap():为了实现用户空间到用户空间的拷贝,mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’。
接收方执行如下操作实现内存映射:
fd = open(“/dev/binder”, O_RDWR);
mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区。mmap()的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必也不能直接访问。
接收缓存区映射好后就可以做为缓存池接收和存放数据了。前面说过,接收数据包的结构为binder_transaction_data,但这只是消息头,真正的有效负荷位于data.buffer所指向的内存中。这片内存不需要接收方提供,恰恰是来自mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是,存放 binder_transaction_data结构本身以及表4中所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。
释放映射的内存:有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放data.buffer所指向的内存区。由命令BC_FREE_BUFFER完成的。
六、Binder 接收线程管理
Binder通信实际上是位于不同进程中的线程之间的通信。假如进程S是Server端,提供Binder实体,线程T1从Client进程C1中 通过Binder的引用向进程S发送请求。S为了处理这个请求需要启动线程T2,而此时线程T1处于接收返回数据的等待状态。T2处理完请求就会将处理结果返回给T1,T1被唤醒得到处理结果。
BINDER_SET_MAX_THREADS 的使用位置此命令告知Binder驱动接收者(一般是Server端)线程池中最大的线程数。由于Client是并发向Server端发送请求的,Server端必须开辟线程池为这些并发请求提供服务。告知驱动线程池的最大值是为了让驱动发现线程数达到该值时不要再命令接收端启动新的线程。
一个Binder服务端实际上就是一个Binder类的对象,该对象一旦创建,内部就启动一个隐藏线程。该线程接下来会接收Binder驱动发送的消息,Server 端在注册服务后就创建两个binder thread
流程说明:
1、服务启动后,通过BINDER_SET_MAX_THREADS命令告诉驱动线程池最大线程数。
2、先在线程池创建1、2个线程,并通过BC_REGISTER_LOOP命令告知驱动
3、同时如果有线程退出,也通过BC_EXIT_LOOP命令告知,以便驱动收集和记录当前线程池的状态。
4、每当驱动接收完数据包返回读Binder的线程时,都要检查一下是不是已经没有闲置线程了。如果是,而且线程总数不会超出线程池最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息,告诉用户线程即将不够用了,请再启动一些,否则下一个请求可能不能及时响应。新线程一启动又会通过BC_xxx_LOOP告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,来及时处理请求。
七、数据包接收队列与(线程)等待队列管理