浅谈Android之Binder原理介绍(一)

Linux的进程间通讯进制(IPC)很多,比如管道,socket,共享内存等等,但是Android为什么不直接使用这些方式,主要原因是传统的IPC方式要么效率无法满足,要么设计理念不够现代,无法满足Android系统设计的要求;


比如管道和socket,会存在内存数据多次拷贝,这如果在APP级别,或许是够用的,但是如果上升到系统级别,系统内部通信频次是极高的,如果效率不够,用户体验肯定会很差,用户体验不行?那还玩个球球,必须要有更好的方案出来。

共享内存效率不错,开辟物理地址空间,然后映射到各自进程地址空间,跟各自进程的虚拟地址页面数据绑定,效率很高,但是共享内存只是一个系统基础功能,它在安全性和扩展性上都无法满足Android的实现要求。

所以,大家都开始寻找这么一套方案,在保持高效率的同时,还能具有优秀的面向对象设计,让各个层级的程序都能快速的实现其业务相关的数据交互。找了一圈,发现开源软件OpenBinder很适合,然后改波改波,集成到Android这个开源软件大杂烩系统中,就这样,Binder诞生了。

Android Binder设计的优点:

1)  效率高,核心是Binder Driver,也就是BinderKernel,相关代码全部运行于内核空间;进程之间主要通过共享内存交互数据

2)采用C-S架构,各个Binder Server能够以组件的方式注册到BinderKernel中,然后Binder Kernel会维护一个server列表,并给每一个server分配一个唯一的ID(Handle),Client端就通过这个ID与server建立数据通路

3)支持远程过程调用(RPC)

4)在各个层都有比较易用的封装类,设计比较合理,从而使C/C++/JAVA层都能快速的开发出基于Binder的进程间通信代码。

接下去看图:

 

从这张图可以看出,如果给Binder的设计分层,总共应该有四层,最核心的,当然是最底下的Binder Kernel,Binder Kernel包含了Binder全部核心实现逻辑,至于上面三层,则是应用层的封装,方便开发者开发而已。

由于Binder是基于Client-Server来设计的,考虑到与Android系统层相关代码的命名规则保持一致,在后续描述中,对Binder Server来说,业务对象用BinderService来描述,其内部关联的binder,则统一使用Native Binder来描述,对BinderClient,业务对象使用Binder ServiceProxy,其内部关联的binder,则使用Binder Proxy。

至于上述binder service和binder service proxy与其关联的nativebinder和binder proxy的关系,前者负责实现service业务和binder transactioncode绑定,并关联native binder或者binder proxy实现数据传输。

2.1 BinderKernel

Binder Kernel其实是一个驱动,当然,它不是一个硬件驱动,而是内核功能驱动,对应的设备符为:/dev/binder。

Binder Kernel的实现非常复杂,这边做下粗略的介绍,以便于大家更好的理解后续代码介绍和相关概念:

1)  维护Native Binder数据列表,会为每个NativeBinder开辟数据类型为binder_node内存空间,然后保存Native Binder对应的进程相关数据(线程池,mmap映射的内存空间管理等等),对象地址,cookie,引用技术,然后给每个Native binder分配一个唯一的Handle做其唯一标识(代码中其实对应的是binder_ref中的desc字段);还有,在binder_node初始创建时,会把强引用计数置为1,并且发送BR_ACQUIRE让用户空间的Native Binder对象强引用计数+1,从而保证binder_node对应的Native binder不会被释放(当然越过Android的智能指针规则强行delete raw pointer的不算^_^)。

2)  线程动态调度,支持RPC

3)  管理数据接收内存,实现mmap,由于binder在android无处不在,应用程序和binderkernel的交互频次是极高的,所以假如能够减少数据拷贝的次数,哪怕每次交互减少一次,效果也会非常明显;这就是binder kernel实现mmap的原因,通过在内核空间开辟一个足够大的内存,然后让所有的进程mmap到这块内存,接着binder kernel再对这块内存做管理,根据进程的需求动态分配内存区域直接给进程使用,从而减少数据传输过程中的拷贝次数。

4)接收线程管理,每个拥有Binder的进程,都需要设置Binder 线程池(可设置最大值),当然不是一次性全部创建,一般都是启动后创建一条主的Binder接收线程到Binder Kernel,这样当进程有新的Binder请求到来时,Binder Kernel就会根据已创建线程的空闲情况,来决定是否需要向Binder进程申请创建新线程来执行请求并添加到线程池,如果无空闲线程并且线程池已经到达最大数目,则该次请求会被阻塞。

应用层与Binder Kernel之间的通信方式,当然就是IOCTL,下面是IOCTL主要命令的介绍:

命令

解释

BINDER_VERSION

获取Binder版本

BINDER_SET_MAX_THREADS

设置接收线程池的线程数目

BINDER_SET_CONTEXT_MGR

设置Binder管理进程,对应的就是ServiceManager,handle为0

BINDER_WRITE_READ

Binder写入或读取数据,对应的数据结构为binder_write_read;参数分为两段:写部分和读部分。如果write_size不为0就先将write_buffer里的数据写入Binder;如果read_size不为0再从Binder中读取数据存入read_buffer中。write_consumedread_consumed表示操作完成时Binder驱动实际写入或读出的数据个数。

如果需要返回数据,那么read_sizeread_buffer必须设置

BINDER_WRITE_READ命令关联的数据分为读写两个buffer,对应write_bufferread_buffer,按照惯例,buffer头四个字节用于数据类型描述。

这些数据类型以宏的形式定义,以BC或者BR开头:

(Writebuffer) BC_XXX,全称为Binder Driver Command Protocol 是指App –>Binder Kernel命令

(Readbuffer) BR_XXX,全称为Binder Driver Return Protocol,是指BinderKernel –> App命令 

命令

解释

BC_ENTER_LOOPER

将当前线程设置为Binder主线程,它不计入接收线程池,必须设置

BC_REGISTER_LOOP

将当前线程注册到接收线程池

BC_EXIT_LOOP

线程退出时,告知Binder将该线程从线程池中移除

BR_SPAWN_LOOPER

当线程池无空闲线程并且线程池总数还未到最大值时,Binder Kernel会发送该命令告知Binder线程,线程快不够用了,请尽快开辟新线程并注册到Binder Kernel,以便能及时接收后续数据。

BC_TRANSACTION

业务事件,包含业务id以及序列化数据,App –> Kernel,所以,只有Binder Proxy才会发出该事件命令

BR_TRANSACTION

业务事件,包含业务id以及序列化数据,Kernel –> APP,所以,只有Native Binder才会收到该事件命令

BC_ACQUIRE

请求将handle所对应的binder node的强引用计数+1

BC_RELEASE

请求将handle所对应的binder node的强引用计数-1

BC_INCREFS

请求将handle所对应的binder node的弱引用计数+1

BC_DECREFS

请求将handle所对应的binder node的弱引用计数-1

BR_ACQUIRE

将关联 native binder的强引用计数+1

BR_RELEASE

将关联 native binder的强引用计数-1

BR_ATTEMPT_ACQUIRE

尝试将关联 native binder的强引用计数+1

BR_INCREFS

将关联native binder的弱引用计数+1

BR_DECREFS

将关联native binder的弱引用计数-1

有一点说下,BC_ACQUIRE和BR_ACQUIRE以及后续类似的命令,操作的对象是不一样的,前者操作的是binder kernel里头的binder node中的引用计数,后者由于接收方是native binder所在进程(线程池),所以它直接操作native binder中的引用计数。

任何一个Native binder化身数据流从Binder Kernel穿过时,BinderKernel都会自动的将其添加到Binder 列表中,并给其分配一个唯一的Handle,同时保存Native Binder的详细信息,包括所属进程,对象地址,引用计数等等。

Handle唯一标识这个Native Binder,任何进程只要拿到这个Handle,就可以通过binder kernel找到它了,是不是跟一个IP对应一台设备很像?

Binder Kernel既然保存了Native Binder的对象信息,就对这个对象存有一个引用关系,那就需要通知对象所属进程(BC_ACQUIRE),将这个对象的强引用计数+1,这样就能保证Binder Kernel保存的这个NativeBinder不会被释放,当然这一切是基于Android的SP&WP的智能指针规则来完成的,SP&WP这里就不做过多介绍了。

上头说过,IOCTL数据传输是通过命令BINDER_WRITE_READ来完成的,它会关联读写两个buffer,那么问题来了,如何将各种各样的数据格式序列化到buffer或者从buffer里头反序列化呢?Android是通过Parcel来完成的,Parcel主要用于进程间数据序列化,还有最重要的,就是它能序列化/反序列化Binder对象数据,详细的这里就不做过多介绍,大家只需记住一点,如果简化理解,站在应用开发的角度,Native Binder对象的序列化,核心要保存的数据就是对象地址,而对于Binder Proxy,序列化时核心要保存的,就是其内部的Handle值。

接下去看下一次完整的binder数据交互图,分析Binder是如何做到减少内存拷贝次数的:

这张图只描述了从Binder Proxy到Native Binder的内存使用情况,至于NativeBinder到Binder Proxy则是一样的,把二者位置调一下就可以了。

从图中可知,对Native Binder和Binder Proxy所属进程来说,初始化的时候,一定要调用mmap完成内存映射,接着在第5步,binder kernel会调用copy fromuser将数据从App Binder Proxy拷贝到申请好的共享内存A,由于这块内存是多个进程映射的,所以在第6步收到传输数据后,App Native Binder就可以直接使用这个数据,使用好后在通知binder kernel将这块内存释放。

由于第6步,直接使用了内存A的数据,所以整个流程下来只在步骤5做了一次数据拷贝。

当然,这里指的是传输数据内容的拷贝,对于这些IOCTL控制命令来说,当然会存在用户空间和内核空间的来回拷贝,但是这个是无法避免的,不在考虑范围内。

很多人可能会说,第5步这次拷贝不是也可以优化掉?既然mmap做了内存映射,直接在App BinderProxy将数据拷贝到共享内存不就好了,在技术上当然没问题,但是安全上就有大问题了,给App赋予共享内存写的权限?什么时候来个内存越界,核心数据就全爆了,所以这样肯定不行,这也是app mmap的权限是mode read的原因,只允许读,不允许写,数据写操作统一由binder kernel来完成,效率和安全不能兼得,只能找一个平衡点。

总结下,每个Native Binder在被序列化后传送到binder kernel时,binder kernel如果检测到这是个native binder并且还没有在binder 列表登记过,就会将其添加到binder列表,并给其分配一个唯一的handle值,然后其他app如果要访问这个native binder,只需要拿到其对应的handle就可以了。

那么问题来了,在初始阶段,如何将nativebinder传送到binder kernel?就算传过去并登记了,app如何得到想要的native binder的handle?就算拿到了,handle就是一个赤裸裸的数字,如何区分?

所以,必须存在这么一个服务,来解决上面所提的问题,它就是servicemanager


发布了46 篇原创文章 · 获赞 25 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/zhejiang9/article/details/55095791