深入理解Android IPC机制之Binder机制详解手机开发

Binder是Android系统进程间通信(IPC)方式之一。Linux已经拥有的进程间通信IPC手段包括(Internet Process Connection): 管道(Pipe)、信号(Signal)和跟踪(Trace)、插口(Socket)、报文队列(Message)、共享内存(Share Memory)和信号量(Semaphore)。本文详细介绍Binder作为Android主要IPC方式的优势。

Binder机制概述

基于Client-Server的通信方式广泛应用于从互联网和数据库访问到嵌入式手持设备内部通信等各个领域。智能手机平台特别是Android系统中,为了向应用开发者提供丰富多样的功能,这种通信方式更是无处不在,诸如媒体播放,视音频频捕获,到各种让手机更智能的传感器(加速度,方位,温度,光亮度等)都由不同的Server负责管理,应用程序只需做为Client与这些Server建立连接便可以使用这些服务,花很少的时间和精力就能开发出令人眩目的功能。Client-Server方式的广泛采用对进程间通信(IPC)机制是一个挑战。目前linux支持的IPC包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及socket中只有socket支持Client-Server的通信方式。当然也可以在这些底层机制上架设一套协议来实现Client-Server通信,但这样增加了系统的复杂性,在手机这种条件复杂,资源稀缺的环境下可靠性也难以保证。

另一方面是传输性能。socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。

表 1 各种IPC方式数据拷贝次数

深入理解Android IPC机制之Binder机制详解手机开发

还有一点是出于安全性考虑。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID和PID(用户ID进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID和PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,systemV的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。

面向对象的 Binder IPC

Binder使用Client-Server通信方式:一个进程作为Server提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务;多个进程作为Client向Server发起服务请求,获得所需要的服务。要想实现Client-Server通信据必须实现以下两点:一是server必须有确定的访问接入点或者说地址来接受Client的请求,并且Client可以通过某种途径获知Server的地址;二是制定Command-Reply协议来传输数据。例如在网络通信中Server的访问接入点就是Server主机的IP地址+端口号,传输协议为TCP协议。

对Binder而言,Binder可以看成Server提供的实现某个特定服务的访问接入点, Client通过这个‘地址’向Server发送请求来使用该服务;对Client而言,Binder可以看成是通向Server的管道入口,要想和某个Server通信首先必须建立这个管道并获得管道入口。

与其它IPC不同,Binder是一个实体位于Server中的对象,该对象提供了一套方法用以实现对服务的请求,就像类的成员函数。遍布于client中的入口可以看成指向这个binder对象的‘指针’,C++叫指针,其实是一种特殊的引用,一旦获得了这个‘指针’就可以调用该对象的方法访问server。从通信的角度看,Client中的Binder也可以看作是Server Binder的‘代理’,在本地代表远端Server为Client提供服务。

Binder 通信模型

前面都是讲Binder的场景以及使用背景,其实我们比较关系的就是怎么用了。

Binder框架定义了四个角色:Server,Client,ServiceManager(以后简称SMgr)以及Binder驱动。其中Server,Client,SMgr运行于用户空间,驱动运行于内核空间。

这四个角色的关系和互联网类似:Server是服务器,Client是客户终端,SMgr是域名服务器(DNS),驱动是路由器。

3.1 Binder 驱动

和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的。它工作于内核态,驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。

3.2 ServiceManager 与实名Binder

和DNS类似,SMgr的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册一个叫Binder对象,Server创建了Binder实体,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SMgr,通知SMgr注册一个Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgr。SMgr收数据包后,从中取出名字和引用填入一张查找表中。

3.3 Client 获得实名Binder的引用

Server向SMgr注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向SMgr请求访问某个Binder。SMgr收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个Binder对象现在有了两个引用:一个位于SMgr中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用Binder实体就不会被释放掉。

3.4 匿名 Binder

其实,Android系统还支持匿名BInder,并不是所有Binder都需要注册给SMgr。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder实现。由于这个Binder没有向SMgr注册名字,所以是个匿名Binder。Client将会收到这个匿名Binder的引用,通过这个引用向位于Server中的实体发送请求。匿名Binder为通信双方建立一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用,向该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.buffer所指向的内存中。这片内存不需要接收方提供,恰恰是来自mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是,存放binder_transaction_data结构本身以及表4中所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放data.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进程C1中通过Binder的引用向进程S发送请求。S为了处理这个请求需要启动线程T2,而此时线程T1处于接收返回数据的等待状态。T2处理完请求就会将处理结果返回给T1,T1被唤醒得到处理结果。

对于Server进程S,可能会有许多Client同时发起请求,为了提高效率往往开辟线程池并发处理收到的请求,就会用到兵法线程池,于是Binder协议引入了专门命令或消息帮助用户管理线程池,Android 的线程管理主要用到了几个东西:

· INDER_SET_MAX_THREADS      //最多可以创建几个线程

· BC_REGISTER_LOOP      

· BC_ENTER_LOOP      

· BC_EXIT_LOOP

· BR_SPAWN_LOOPER

分别使用BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驱动,以便驱动收集和记录当前线程池的状态。线程总数不会超出线程池最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息,告诉用户线程即将不够用了,请再启动一些,否则下一个请求可能不能及时响应。

Thead Pool

在Binder IPC中,所有进程均会启动一个thread来负责与BD来直接通信,也就是不停的读写BD,这个线程的实现主体是一个IPCThreadState对象,下面会介绍这个类型。

下面是 Pool thread的启动方式:

ProcessState::self()->startThreadPool();

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/5985.html

(0)
上一篇 2021年7月17日
下一篇 2021年7月17日

相关推荐

发表回复

登录后才能评论