Android的系统设计特别围绕进程隔离,不但在应用程序之间,而且在系统本身的不同部分之间隔离进程。Android的Binder进程通信机制是一个丰富的通用IPC设施,Android系统的大部分就建立在该设施之上。
Binder体系结构分为三个层次,如下所示:

Binder内核模块
Binder没有使用管道这样的现有Linux IPC设施,它包含一个特别的内核模块来实现其自己的IPC机制。Binder使用的IPC模型是发送的进程向内核提交操作,该提交在接收的进程中被执行,当执行时发送者会处于阻塞,等待结果的返回。因此Binder IPC是基于消息的,类似System V消息队列,而不是基于流的如Linux管道。Binder中的消息称为事务,用户空间提交给内核的每个事务是一个完整的操作:它标识操作的目标和发送者的标识符,以及交付的完整数据;内核决定适当的进程来接收该事务,将其交付给进程中等待的线程。其模型如下图所示:

发送的进程中任何线程都可能创建标识其目标的事务,并且将该事务提交给内核。内核制作事务的副本,将发送者的标识符添加到其中。内核确定由哪个进程负责事务的目标,并且唤醒接收事务的进程中的一个线程。一旦接收的进程执行起来,它要确定适当的事务目标并且交付。
图中每个进程拥有一个“线程池”,线程池是由用户空间创建的一个或多个线程,用以处理到来的事务。内核将每个到来的事务分配给进程中的线程中当前正处于等待工作的线程。然而从发送进程发出的对内核的调用不必来自线程池,任何线程都可以自由的发起一个事务,例如图中的Ta线程。
内核如何查找对应的进程对象?
如下图所示:

内核中备份各个进程中对象,在进程中创建句柄建立与对象的关联。例如进程1中句柄2关联到进程2的对象2a。因此进程1可以提交一个事务(目标为句柄2)给内核,内核据此判断发送给进程2的,并且是进程2中的对象2a。
注意:一个进程中句柄的值与其他进程中的值相同并不意味着相同的事物。例如在进程1中句柄2标识对象2a,然而在进程2中,相同的句柄值2标识对象1a。如果内核没有分配句柄给某个进程,那么其它进程将无法访问该进程中的对象。如进程2中的对象2b,在进程1中并没有为它分配句柄。因此对于进程1而言,不存在访问该对象的路径。
句柄到对象的关联是如何建立?
讨论进程2到进程1中对象1b如何建立联系,如下图所:

1> 进程1创建一个初始的事务结构,其中包含对象1b的本地地址;
2> 进程1提交事务到内核;
3> 内核查看事务中的数据,找到地址对象1b,并且创建一个针对它的新条目,因为它以前并不知道该地址;
4> 内核利用事务的目标句柄2来确定它意在进程2中的对象2a;
5> 内核现在将事务头重写,使其适合进程2,改变其目标为地址对象2a;
6> 内核同样为目标进程重写事务数据,此处它发现对象1b还不被进程2所知,所以为它创建一个新的句柄3;
7> 重写的事务被交付给进程2来执行;
8> 一旦接收到事务,进程会发现新的句柄3,并且将其添加到可用句柄表中;
注意:发送相同的对象到一个进程很多次,总是会得到相同的标识,这与Linux文件描述符不同,在Linux中打开相同的文件多次,每次会分配不同的文件描述符。当对象在进程之间传递时,Binder IPC系统将维护唯一的对象标识。
Binder体系结构本质上为Linux引入了一个基于能力的安全模型,每个Binder对象是一个能力,发送一个对象到另一个进程就是将能力授予该进程,于是接收进程可以使用对象提供的一切功能。
Binder用户空间API
大多数用户空间代码不直接与Binder内核模块交互,存在一个用户空间的面向对象的库,它提供了更加简单地API。
1> IBinder是Binder对象的抽象接口。其关键方法是transact,它将一个事务提交给对象,接收事务的实现可能是本地进程中的一个对象,或者是另一个进程中的对象。如果它在另一个进程中,则将会通过前面讨论的Binder内核模块交付给它。
2> Binder是一个具体的Binder对象,其关键方法是onTransact,它接收发送给它的一个事务,并且执行适当的操作。
3> Parcel是一个容器,用于读和写Binder事务中的数据,它拥有用于读和写类型化数据的方法,同时提供对IBinder引用对象的读写操作,使用适当的数据结构供内核跨进程理解和传输该引用。
用户空间的API调用过程如下图所示:

说明:Binder1b和Binder2a是具体Binder子类的实例。
1> 创建包含期望数据的Parcel,通过BinderProxy将其发送,具体是使用transact方法为调用创建适当的事务并将其提交给内核。
2> 事务流经过程为:目标由BinderProxy代表,并且其数据保存在Parcel中,事务流过内核,不断改变头信息;
3> 接收进程中利用目标确定适当的接收Binder对象,将Parcel交付给对象的onTransact方法;
上面工作的重头戏是解组和编组代码,这些代码具有固定的模式,写起来乏味,且容易出错,于是产生AIDL做自动生成工作。
Binder接口和AIDL
AIDL(Android Interface Definition Language, Android接口定义语言),该工具是一个接口编译器,它以接口的抽象描述为输入,生成定义接口所必须的源代码,并且实现适当的编组和解组代码。
package com.example
interface IExample {
void print(String msg);
}
如上的接口描述由AIDL进行编译,生成三个Java类,如下所示:

1)IExample提供Java语言接口定义;
2)IExample.Proxy是IExample的一个具体实现,负责将调用转换成适当的Parcel内容,并且通过与之通信的IBinder上的transact调用将其发送出去;
3)IExample.Stub是实现该接口的基类,是IExample接口的实现,负责onTransact调用转换成IExample的适当的方法调用。
在IExample接口上的简单地print调用过程如下图所示:

1)将方法调用编组成一个parcel,调用底层BinderProxy上的transact;
2)BinderProxy构造一个内核事务,通过ioctl调用将其交付给内核;
3)内核将事务传递给特定的进程,将其交付给一个正在其自己的ioctl调用中等待的线程;
4)事务解码回到一个Parcel,并且在适当的本地对象上调用onTransact,在这里本地对象是ExampleImpl,它是IExample.Stub的一个子类;
5)IExample.Stub将Parcel解码成适当的方法和参数以便进行调用,这里调用的是print;
6)ExampleImpl中print的具体实现最终会执行;
Android的IPC的主体是使用这一机制编写的,Android中的大多数服务通过AIDL定义,并且使用这里讨论的方式来实现。
参考资料:
[1] 现代操作系统
网友评论