移步系列Android跨进程通信IPC系列
1 Android整体架构Android系统架构及系统源码目录
每一个系统服务在应用框架层都有一个Manager与之对应
从进程的角度来看IPC机制
- 1 是一种跨进程通信的方式(IPC)
- 2 是一种远程过程调用方式(PRC)
而从实现的角度来说,Binder核心被实现成一个Linux驱动程序,并运行于内核态。这样它才能具有强大的跨进程访问能力。
- 性能:目前Linux支持的IPC包括传统的管道,System V IPC(包括消息队列/共享内存/信号量)以及socket,但是只有socket支持Client/Server的通信方式,由于socket是一套通用当初网络通信方式,其效率低下,且消耗比较大(socket建立连接过程和中断连接过程都有一定的开销),明显在手机上不适合大面积使用socket。而消息队列和管道采用”存储-转发” 方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存中拷贝到接收方缓存中,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。
- 安全性:在安全性方面,Android作为一个开放式,拥有众多开发者的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽的情况。传统IPC没有任何安全措施,完全依赖上层协议来去报。首先传统IPC的接受方无法获取对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程的身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道、system V的键值,socket的ip地址或者文件名都是开放的,只要知道这些接入点的程序都可以对端建立连接,不管怎样都无法阻止恶意程序通过接收方地址获得连接。
基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,所以就有了Binder。
Binder基于Client/Server通信模式,传输过程只需要一次拷贝,为发送发添加UID/PID身份,即支持实名Binder也支持匿名Binder,安全性高。
Binder通信过程示例
通信双方必须要处理线程同步,内存管理等问题,工作量大,而且问题多,
介绍的传统IPC 命名管道(FIFO) 信号量(semaphore) 消息队列已经从Android中去掉了,同其他IPC相比,Socket是一种比较成熟的通信手段了,同步控制也很容易实现。Socket用于网络通信非常合适,但是用于进程间通信就效率很低。
Android在架构上一直希望模糊进程的概念,取而代之以组件的概念。应用也不需要关心组件存放的位置、组件运行在那个进程中、组件的生命周期等问题。随时随地的,只要拥有Binder对象,就能使用组件的功能。Binder就像一张网,将整个系统的组件,跨进程和线程的组织在一起。
Binder是整个系统的运行的中枢。Android在进程间传递数据使用共享内存的方式,这样数据只需要复制一次就能从一个进程到达另一个进程了(前面文章说了,一般IPC都需要两步,第一步用户进程复制到内核,第二步再从内核复制到服务进程。)
- PS: 整个Androdi系统架构中,虽然大量采用了Binder机制作为IPC(进程间通信)方式,但是也存在部分其他的IPC方式,比如Zygote通信就是采用socket。
在Android中,有很多Service都是通过Binder来通信的,比如MediaService名下的众多Service:
- AudioFlinger音频核心服务
- AudioPolicyService:音频策略相关的重要服务
- MediaPlayerService:多媒体系统中的重要服务
- CarmeraService:有关摄像/照相的重要服务
整体架构
从图中可以看出,Binder的实现分为这几层,按照大的框架理解是
- framewor层
— —java 层
— —jni 层
— —native/ C++层- linux驱动层 c语言
其中Linux驱动层位于Linux内核中,它提供了最底层的数据传递,对象标示,线程管理,通过调用过程控制等功能。驱动层其实是Binder机制的核心。
Framework层以Linux驱动层为基础,提供了应用开发的基础设施。Framework层既包含了C++部分的实现,也包含了Java基础部分的实现。为了能将C++ 的实现复用到Java端,中间通过JNI进行衔接。
Binder框架是典型的C/S架构。
我们把服务的请求方称为Client,服务的实现方称之Server。Clinet对于Server的请求会经由Binder驱动框架由上至下传递到内核的Binder驱动中,请求中包含了Client将要调用的命令和参数。
请求到了Binder驱动以后,在确定了服务的提供方之后,再讲从下至上将请求传递给具体的服务。
Android内部采用C/S架构。而Binder通信也是采用C/S架构。那我们来看下Binder在C/S的中的流程。
具体流程如下:
- 相应的Service需要注册服务。Service作为很多Service的拥有者,当它想向Client提供服务时,得先去Service Manager(以后缩写成SM)那儿注册自己的服务。Server可以向SM注册一个或者多个服务。
- Client申请服务。Client作为Service的使用者,当他想使用服务时,得向SM申请自己所需要的服务。Client可以申请一个或者多个服务。
- 当Client申请服务成功后,Client就可以使用服务了。
PS:注意这里的ServiceManager是指Nativie层的ServiceManager(C++),并非是framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制的守护进程。
后面我们会不断的提及两个概念,一个是Server,还有一个是Service,我这里先强调下,Server是Server,Service是Service,大家不要混淆,一个Server下面可能有很多Service,但是一个Servcie也只能隶属于一个Server。
Binder通信模型如下:
我们可以发现:
- Client和Server是存在于用户空间
- Client和Server通信实现是由Binder驱动在内核的实现
- SM作为守护进程,处理客户端请求,管理所有服务
我们可以把SM理解成为DNS服务器,那么Binder Driver就相当于路由的功能。
如下图:(注意图片的右边)
- Binder IPC 属于 C/S 结构,Client 部分是用户代码,用户代码最终会调用 Binder Driver 的 transact 接口,Binder Driver 会调用 Server,这里的 Server 与 service 不同,可以理解为 Service 中 onBind 返回的 Binder 对象,请注意区分下。
切记,这里 Server 的实现是线程池的方式,而不是单线程队列的方式,区别在于,单线程队列的话,Server 的代码是线程安全的,线程池的话,Server 的代码则不是线程安全的,需要开发者自己做好多线程同步。
- 首先 XXServer(XXX代表某个)在自己的进程中向Binder驱动申请创建一个XXXService的Binder实体。
- Binder驱动为这个XXXService创建位于内核中的Binder实体节点以及Binder的引用,注意,是将名字和新建的引用打包传递给SM(实体没有传给SM),通知SM注册一个名叫XXX的Service。
- SM收到数据包后,从中取出XXXService名字和引用,填入一张查找表中
- 4、此时,如果有Client向SM发送申请服务XXXService的请求,那么SM就可以查找表中该Service的Binder引用,并把BInder引用(XXXBpBinder返回给Client)
在进一步了解Binder通信机制之前,我们先弄清楚几个概念。
为了大家在后面更好的理解,这里补充几个概念
- Binder实体对象 :Binder实体对象就是Binder服务的提供者。一个提供Binder服务的类必须继承BBinder类,因此,有时为了强调对象类型,也用”BBinder对象”来代替”Binder实体对象”。
- Binder引用对象 :Binder引用对象是Binder实体对象在客户进程的代表,每个引用对象的类型都是BpBiner类,同样可以用名称”BpBinder对象”来代替”Binder引用对象”。
- Binder代理对象 :代理对象也成为接口对象,它主要是为了客户端的上层应用提供接口服务,从IInterface类派生。它实现了Binder服务的函数接口,当然只是一个转调的空壳。通过代理对象,应用能像使用本地对象一样使用远端实体对象提供服务,将Binder代理对象和Binder引用对应(BpBinder对象)分开的好处是代理对象可以有很多实例,但是它们包含的是同一个引用对象,这样方便了应用层的使用。。
- IBiner对象 :BBinder和BpBinder类是从IBinder类中继承来。在很多场合,不需要刻意地去区分实体对象和引用对象,这时候也可以统一使用”IBinder对象”来统一称呼他们。
PS:BpBinder(Binder引用对象,在客户端)和BBinder(Binder实体,在服务端)都是Android中Binder通信相关的代表,它们都是从IBiner类中派生而来(BpBinder和BBinder在Native层,不在framework层),
client端:BpBinder通过调用transact()来发送事物请求
server端:BBinder通过onTransact()会接受到相应的事物
这时候再来看下这个图,然后大家思考一下,就会明白很多事情。
如果你足够细心,你会发现这里有一个问题:
SM和Server都是进程,Server向SM注册Binder需要进程间通信,当前实现的是进程间通信,却又用到进程间通信。有点晕是不,就好像先有鸡还是先有蛋这个问题。
其实Binder是这么解决这个问题的:
- 针对Binder的通信机制,Server端拥有的是Binder的实体(BBinder);Client拥有的是Binder的引用(BpBinder)。
- 如果把SM看做Server端,让它在Binder驱动一起运行起来时就有自己的实体(BBinder)(代码中设置ServiceManager的Binder其handle的值恒为0)。这个Binder实体没有名字也不需要注册,所有的Client都认为handle值为0的binder引用(BpBinder)是用来与SM通信的。那么这个问题就解决了。
- Client和Server中handle的值为0(值为0的引用是专门与SM通信用的),还不行,还需要让SM的handle值为0的实体(BBinder)为0才算大功告成。怎么实现的? 当一个进程调用Binder驱动时,使用** “BINDER_SET_CONTEXXT_MGR” ** 命名(在binder_ioctl中)将自己注册成SM时,Binder驱动会自动为她创建Binder实体。这个Binder的引用对所有Client都为0。
- Server向SM注册了Binder实体及其名字后,Client就可以Service的名字在SM在查找表中获得了该Binder的引用(BpBinder)了
- Client也利用了保留的handle值为0的引用向SM请求访问某个Service:当申请访问XXXService的引用。
- SM就会从请求数据包中获得XXXService的名字,在查找表中找到名字对应的条目,取出Binder的引用打包回复给Client。
- Client就可以利用XXXService的引用使用XXXService的服务了。如果有更多的Client请求该Service,系统中就会有更多的Client获得这个引用。
如下图
首先要明白一个事情:
Client要拥有自己的自己的Binder实体,以及Server的Binder的应用;Server有用自己的Binder的实体,以及Client的Binder引用。
我们也可以按照网络请求的方式来分析:
- 从Client向Server发送数据:Client为发送方,拥有Binder实体;Server为接收方,拥有Binder引用。
- 从Server向Client发送数据:Server为发送方,拥有Binder实体:Client为接收方,拥有Binder引用。
其实,在我们建立C/S连接后,无需考虑谁是Client,谁是Server。只要理清谁是发送方,谁是接收方,就能知道Binder的实体和应用在那边。
那我们看下建立C/S连接后的,具体流程,如下图:
那我们说下具体的流程:
- 第一步,发送方通过Binder实体请求发送操作
- 第二步,Binder驱动会处理这个操作请求,把发送方的数据放入写缓存(binder_write_read.write_buffer)(对于接受方来说为读缓存区),并把read_size(接收方读数据)置为数据大小。
- 第三步,接收方之前一直在阻塞状态中,当写缓存有数据,则会读取数据,执行命令操作
- 第四步,接收方执行完后,会把返回结果同样采用binder_transaction_data结构体封装,写入缓冲区(对于发送方,为读缓冲区)
如下图:
从代码上看,Binder设计的类可以分成4个层级,如下图所示
6 Binder协议
- 最上层的是位于Framewok中的各种Binder服务类和它们的接口类。这一层的类非常多,比如常见的ActivityManagerService(缩写叫AMS)、WindowManagerService(缩写叫WMS)、PackageManagerService(缩写是PMS)等,它们为应用程序提供了各种各样的服务。
- 中间则分为为两层,上面是用于服务类和接口开发的基础,比如IBinder、BBinder、BpBinder等。下层是和驱动交互的IPCThreadState和ProcessState类。
- 这里刻意把中间的libbinder中的类划分为两个层次的原因,是在这4层中,第一层的和第二层联系很紧密,第二层中的 各种Binder类用来支撑服务类和代理类的开发。但是第三层的IPCThread和第四层之间耦合得很厉害,单独理解IPCThread或者是驱动都是一件很难的事,必须把它们结合起来理解,这一点正是Binder架构被人诟病的地方,驱动和应用层之间过于耦合,违反了Linux驱动设计的原则,因此,主流的Linux并不愿意接纳Binder
Biner协议格式基本是”命令+数据”,使用ioctl(fd,cmd,arg)函数实现交互。命令由参数cmd承载,数据由参数arg,随着cmd不同而不动。
命令 | 含义 | 参数(arg) |
---|---|---|
BINDER_WRITE_READ | 该命令向Binder写入或读取数据。参数分为两段:写部分和读部分。如果write_size不为0,就将write_buffer里的数据写入Binder;如果read_ size不为0再从Binder中读取数据存入read_buffer中。write_consumered和read_consumered表示操作完成时Binder驱动实际写入或者读出数据的个数 | struct binder_write_read{ singed long write_size;singed 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 | 当前进程注册为SM。系统中只能存在一个SM,只要当前的SM没有调用close()关闭,Binder驱动就不能有别的进程变成SM | 无 |
BINDER_TREAD_EXIT | 通知Binder驱动当前线程退出了。Binder会为所有参与的通信线程(包括Server线程池中的线程和Client发出的请求的线程) 建立相应的数据结构。这些线程在退出时必须通知驱动释放相应的数据结构 | 无 |
BINDER_VERSION | 获取Binder驱动的版本号 | 无 |
这其中最常用的命令是 BINDER_WRITE_READ。该命令的参数包括两个部分:
驱动程序先处理写部分再处理读部分。这样安排的好处是应用程序可以很灵活的地处理命令的同步或者异步。
例如若要发送异步命令可以只填入写部分而将read_size设置为0,若要只从Binder获得的数据可以将写部分置空,即write_size置0。如果想要发送请求并同步等待返回数据可以将两部分都置上。
我提供两套,一套是图片,方便手机用户,一部分是文字,方便PC用户
上面图片,下面是文字
命令 | 含义 | 参数(arg) |
---|---|---|
BC_TRANSACTION BC_REPLY | BC_TRANSACTION用于Client向Server发送请求数据;BC_REPLY用于Server向Client发送回复(应答)数据。其后面紧接着一个binder_transaction_data结构体表明要写入的数据。 | struct binder_transaction_data |
BC_ACQUIRE_RESULT | 暂未实现 | |
BC_FREE_BUFFER | 释放一块映射内存。Binder接受方通过mmap()映射一块较大的内存空间,Binder驱动基于这片内存采用最佳匹配算法实现接受数据缓存的动态分配和释放,满足并发请求对接受缓存区的需求。应用程序处理完这篇数据后必须尽快使用费改命令释放缓存区,否则会因为缓存区耗尽而无法接受新数据 | 指向需要释放的缓存区的指针;该指针位于收到的Binder数据包中 |
BC_INCREFS BC_ACQUIRE BC_RELEASE BC_DECREFS | 这组命令增加或减少Binder的引用计数,用以实现强指针或弱指针的功能 | 32位Binder引用号 |
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关闭Binder驱动或异常退出而消失,引用者能做的就是要求Server在此刻给出通知 | uint32 *ptr;需要得到死亡的通知Binder引用 |
BC_DEAD_BINDER | 收到实体死亡通知书的进程在删除引用后用本命令告知驱动 | void * COOKIE |
在Binder里读出数据格式和向Binder中写入数据格式一样,采用(消息ID+数据)形式,并且多条消息可以连续存放。
Binder读操作消息ID
消息 | 含义 | 参数(arg) |
---|---|---|
BR_ERROR | 发生内部错误(如内存分配失败) | —- |
BR_OK BR_NOOP | 操作完成 | —- |
BR_SPAWN_LOOPER | 消息用于接受方线程池管理。当驱动发现接收方所有线程都处于忙碌状态且线程池中的线程总数没有超过BINDER_SET_MAX_THREADS设置的最大线程时,向接收方发送该命令要求创建更多的线程以备接受数据 | —- |
BR_TRANSCATION BR_REPLY | 这两条消息分别对应发送方的 BC_TRANSACTION 和BC_REPLY,表示当前接受的数据是请求还是回复 | binder_transaction_data |
BR_ACQUIRE_RESULT BR_ATTEMPT_ACQUIRE BR_FINISHED | 尚未实现 | —- |
BR_DEAD_REPLY | 交互过程中如果发现对方进程或线程已经死亡则返回该消息 | —- |
BR_TRANSACTION_COMPLETE | 发送方通过BC_TRRANSACTION或BC_REPLY发送完一个数据包后,都能收到该消息作为成功发送的反馈。这和BR_REPLY不一样,是驱动告知发送方已经发送成功,而不是Server端返回数据。所以不管同步还是异步交互接收方都能获得本消息。 | —- |
BR_INCREFS BR_ACQUIRE BR_RFLEASE 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))
该结构是Binder接收/发送数据包的标准格式,每个成员定义如下:
成员 | 含义 |
---|---|
union{ size_t handle; void *ptr;} target; | 对于发送数据包的一方,该成员指明发送目的地。由于目的地是远端,所以在这里填入的是对Binder实体的引用,存放在target.handle中。如前述,Binder的引用在代码中也叫句柄(handle)。 当数据包到达接收方时,驱动已将该成员修改成Binder实体,即指向 Binder对象内存的指针,使用target.ptr来获取。该指针是接受方在将Binder实体传输给其他进程时提交给驱动的,驱动程序能够自动将发送方填入的引用转换成接收方的Binder对象的指针,故接收方可以直接将其当对象指针来使用(通常是将其reinpterpret_cast相应类) |
void *COOKIE; | 发送方忽略该成员;接收方收到数据包时,该成员存放的是创建Binder实体时由该接收方自定义的任意数值,做为与Binder指针相关的额外信息存放在驱动中。驱动基本上不关心该成员 |
unsigned int code ; | 该成员存放收发双方约定的命令码,驱动完全不关心该成员的内容。通常是Server端的定义的公共接口函数的编号 |
unsigned int code; | 与交互相关的标志位,其中最重要的是TF_ONE_WAY位。如果该位置上表明这次交互是异步的,Server端不会返回任何数据。驱动利用该位决定是否构建与返回有关的数据结构。另外一位TF_ACCEPT_FDS是处于安全考虑,如果发起请求的一方不希望在收到回复中接收文件的Binder可以将位置上。因为收到一个文件形式的Binder会自动为接收方打开一个文件,使用该位可以防止打开文件过多 |
pid_t send_pid uid_t sender_euid | 该成员存放发送方的进程ID和用户ID,由驱动负责填入,接收方可以读取该成员获取发送方的身份。 |
size_t data_size | 驱动一般情况下不关心data.buffer里存放了什么数据。但如果有Binder在其中传输则需要将其对应data.buffer的偏移位置指出来让驱动知道。有可能存在多个Binder同时在数据中传递,所以须用数组表示所有偏移位置。本成员表示该数组的大小。 |
union{ struct{ const void *buffer; const void * offset; } ptr; uint8_t buf[8];} data; | data.buffer存放要发送或接收到的数据;data.offsets指向Binder偏移位置数组,该数组可以位于data.buffer中,也可以在另外的内存空间中,并无限制。buf[8]是为了无论保证32位还是64位平台,成员data的大小都是8字节。 |
- offsets_size和data.offsets两个成员,这是Binder通信有别于其他IPC的地方。
- Binder采用面向对象的设计思想,一个Binder实体可以发送给其他进程从而建立许多跨进程的引用;另外这些引用也可以在进程之间传递,就像java将一个引用赋值给另外一个引用一样。为Binder在不同进程中创建引用必须有驱动参与,由驱动在内核创建并注册相关的数据结构后接收方才能使用该引用。而且这些引用可以是强类型的,需要驱动为其维护引用计数。
Android跨进程通信IPC之6——Binder框架