Android进阶笔记-5. IPC机制 & Binder 原理
2022-01-12 本文已影响0人
今阳说
IPC机制
一次进程间通信至少包含两个进程,由于进程隔离机制的存在,通信双方必然需要借助 IPC(进程间通信,inter-Process Communication)来实现;
Linux中的IPC机制种类
管道(pipe)
- 继承自Unix,半双工通信方式(数据只能在一个方向上流动);
- 原理:通信双方利用内存的共享文件来传递信息;
信号(sinal)
- 异步通信方式,软件层对中断机制的一种模拟,例如内核通知用户空间进程发生了哪些系统事件
- 不适用于信息交换,适用于进程中断控制;
信号量(semophore)
- 是一个计数器,控制多个进程对共享资源的访问,常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源;
- 主要作为进程间以及同一进程内不同线程之间的同步手段;
消息队列(Message)
- 具有特定格式的消息链表,存放在内存中并由消息队列标识符标识,并且允许一个或多个进程向它写入与读取消息;
- 信息会复制两次,因此对于频繁或者信息量大的通信不宜使用消息队列;
共享内存(Share Memory)
- 多个进程读写一块内存空间,由需要访问的进程将其映射到自己的私有地址空间,不需要进行数据的拷贝,效率较高;
套接字(Socket)
- 可用于不同机器之间的进程间通信
Android中的IPC机制
序列化
- Serializable: java提供,空接口,开销大,大量的IO操作;
- Parcelable: android提供,通过intent,binder传递,效率要更高,我们常用的Bundle就是其实现类;
Serializable与Parcelable区别
- Serializable:Java 序列化接口,在硬盘上读写,读写过程中有大量临时变量的生成,内部执行大量的i/o操作,效率很低;
- Parcelable:Android 序列化接口,效率高, 使用麻烦, 在内存中读写(AS有相关插件 一键生成所需方法),对象不能保存到磁盘中;
AIDL
服务端:
- 创建要操作的实体类,实现 Parcelable 接口
- 新建aidl文件夹,在其中创建接口aidl文件以及实体类的映射aidl文件
- Make project
- 服务service中实现AidlInterface.Stub对象, 并在onBind方法中返回
客户端:
- copy服务端提供的aidl文件夹和实体类
- Make project
- ServiceConnection.onServiceConnected方法中调用AidlInterface.Stub.asInterface(IBinder)创建AidlInterface实例,并调用其方法
Bundle
- 实现了Parcelable接口,所以它可以方便的在不同的进程间传输。
- 四大组件中的Activity,Service,Receiver都支持使用Bundle传递数据,所以当我们启动不同进程的组件时,就可以用Intent+bundle进行数据传递
共享文件和SharedPreferences
- 两个进程通过读写同一个文件来交换数据
- 对象可以通过序列化和反序列化进行读写
- SharedPreferences本质就是读写文件
Messenger(信使,轻量级的IPC方案)
- 通过它可以在不同进程间传递Message对象
- 底层其实就是AIDL, 只是进一步进行了封装, 以方便使用,从下面这个构造方法就可以看出
public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}
Socket
- 分为 流式套接字 和 用户数据报套接字 两种, 分别对应于网络的传输控制层中的TCP和UDP协议
- TCP: 面向连接的协议,提供稳定的双向通信功能,连接的建立需要经过三次握手才能完成,
为了提供稳定的数据传输功能,其本身提供了超时重传机制 - UDP: 无连接,提供不稳定的单向通信功能(也可以实现双向通信),具有更好的效率,但不能保证数据一定能
正确传输,尤其是在网络拥塞的情况下
ContentProvider
- Android中提供的专门用于不同应用间进行数据共享的方式,天生就适合进程间通信
- 底层实现同样也是Binder,而且比AIDL要简单
- 需要注意的: CRUD操作,防止SQL注入,权限控制
- 系统中预制了许多ContentProvider,如通讯录,日程表信息等
- query,insert,delete,update是运行在Binder线程中的, 存在多线程并发访问,方法内部要做好线程同步
,例如BookProvider中采用了SQLite,并且只有一个SQLiteDatabase的链接, 所以可以正确的应对多线程情况,
因为SQLiteDatabase内部对数据库的操作是有同步处理的 - onCreate运行在main线程(UI线程),不能进行耗时操作
Binder
- 基于Binder的: AIDL,Messenger,ContentProvider
- 从Android Framework角度讲,Binder是ServiceManager链接各种Manager(ActivityManager,WindowManager等)和ManagerService的桥梁
- 从Android 应用层来说,Binder是客户端和服务端进行通信的媒介(bindService)
- Binder主要用在Service中,包括普通的Service,AIDL和Messenger
- 普通的Service中的Binder不涉及进程间通信,没有触及到Binder的核心
Binder 跨进程通信原理
为什么要使用Binder
1. 性能
- 主要影响的因素是拷贝次数:
- 管道、消息队列、Socket的拷贝次书都是两次,性能不是很好;
- 共享内存不需要拷贝,性能最好;
- Binder拷贝1次,性能仅次于共享内存;
Linux 下传统的进程间通信原理与不足
- 内核程序在内核空间分配内存并开辟一块内核缓存区,发送进程通过copy_from_user函数将数据拷贝到到内核空间的缓冲区中。同样的,接收进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程。这样数据发送进程和数据接收进程完成了一次数据传输,也就是一次进程间通信;其有两点不足之处:
- 一次数据传递需要经历:用户空间 –> 内核缓存区 –> 用户空间,需要2次数据拷贝,效率不高。
- 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用API接收消息头来获取消息体的大小,浪费了空间或者时间。
2. 稳定性
- Binder是基于C/S架构的,技术上已经很成熟,稳定;
- 共享内存没有分层,难以控制,并发同步访问临界资源时,可能还会产生死锁;
- 从稳定性的角度讲,Binder是优于共享内存的。
3. 安全
- Android是一个开源的系统,并且拥有开放性的平台,市场上应用来源很广,因此安全性对于Android 平台而言极其重要。
传统的IPC接收方无法获得对方可靠的进程用户ID/进程ID(UID/PID),无法鉴别对方身份。Android 为每个安装好的APP分配了自己的UID,
通过进程的UID来鉴别进程身份。另外,Android系统中的Server端会判断UID/PID是否满足访问权限,而对外只暴露Client端,加强了系统的安全性。
4. 语言
- Linux是基于C语言,C语言是面向过程的,Android应用层和Java Framework是基于Java语言,Java语言是面向对象的。
- Binder本身符合面向对象的思想,因此作为Android的通信机制更合适。
Android中Binder的来源
- Binder是基于开源的OpenBinder实现的,OpenBinder最早并不是由Google公司开发的,而是Be Inc公司开发的,接着由Palm, Inc.公司负责开发。后来OpenBinder的作者Dianne Hackborn加入了Google公司,并负责Android平台的开发工作,顺便把这项技术也带进了Android。
Linux的动态内核可加载模块
- 跨进程通信是需要内核空间做支持的, 传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?
- 这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
Binder 驱动
- 在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。
- Binder驱动是Android专用的,但底层的驱动架构与Linux驱动一样。binder驱动在以misc设备进行注册,作为虚拟字符设备,没有直接操作硬件,只是对设备内存的处理。主要是驱动设备的初始化(binder_init),打开 (binder_open),映射(binder_mmap),数据操作(binder_ioctl)。
- Binder是基于内存映射来实现的,内存映射通常是用在有物理介质的文件系统上的,而 Binder 并不存在物理介质,因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间, 是为了跨进程传递数据。
内核空间(KernelSpace)和用户空间(UserSpace)
- 为了保证内核的安全,不能让用户进程直接操作内核,操作系统从逻辑上将虚拟空间划分为用户空间和内核空间,即使用户的程序崩溃了,内核也不会受到影响, 内核空间的数据是可以进程间共享的;
进程隔离
- 一个进程不能直接操作或者访问另一个进程
系统调用
- 系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。
- copy_from_user:将用户空间的数据拷贝到内核空间。
- copy_to_user:将内核空间的数据拷贝到用户空间。
内存映射(Memory Map)
- 由于应用程序不能直接操作设备硬件地址,所以操作系统提供了一种机制:内存映射,把设备地址映射到进程虚拟内存区
- 例如用户空间读取磁盘文件,如果不采用内存映射,那么就需要在内核空间建立一个页缓存,页缓存去拷贝磁盘上的文件,然后用户空间拷贝页缓存的文件,这就需要两次拷贝。 如果采用内存映射,由于新建了虚拟内存区,那么磁盘文件和虚拟内存区域就可以直接映射,少了一次拷贝。
- 在Linux中通过系统调用函数mmap来实现内存映射。将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间,反之亦然。内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。
Binder驱动通过内存映射进行跨进程通信的步骤
- Binder驱动在内核空间创建一个数据接收缓存区。
- 在内核空间开辟一块内核缓存区,建立内核缓存区和数据接收缓存区之间的映射关系,以及数据接收缓存区和接收进程用户空间地址的映射关系。
- 发送方进程通过copy_from_user()函数将数据拷贝 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,
因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信,整个过程只使用了1次拷贝,效率更高。
- 系统中并不是所有的进程通信都是采用了Binder,而是根据场景选择最合适的,比如Zygote进程与AMS通信使用的是Socket,Kill Process采用的是信号。
基于Binder通信的C/S架构
- 系统在启动时,SystemServer进程启动后会创建Binder线程池,目的是通过Binder,使得在SystemServer进程中的服务(如AMS、PMS)可以和其他进程进行通信;
- 我们常说的AMS、PMS都是基于Binder来实现的,再比如Client端的MediaPlayer和Server端的MeidaPlayerService不是运行在一个进程中的,同样需要Binder来实现通信。
[图片上传失败...(image-18e90-1641984042595)]
- Binder 是基于 C/S 架构的,其中 Client进程、Server进程、Service Manager进程 运行在用户空间,Binder驱动 运行在内核空间。Client、Server 和 ServiceManager 之间的交互通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder(Binder驱动,在内核空间通过mmap实现跨进程通信),间接的实现跨进程通信。
- Binder驱动,Service Manager进程 属于 Android基础架构(系统已经实现好了);
- Client 进程 和 Server 进程 属于Android应用层(需要开发者自己实现)
Binder机制 在Android中的具体实现
- Binder机制在 Android中的实现主要依靠 Binder类,其实现了IBinder 接口
Binder 通信过程
1. 启动ServiceManager
- ServiceManager是init进程负责启动的,具体是在解析init.rc配置文件时启动的,init进程是在系统启动时启动的;
- servicemanager的入口函数在service_manager.c中
int main(int argc, char** argv)
{
//binder_state结构体用来存储binder的三个信息:
//int fd; //binder设备的文件描述符
//void *mapped; //binder设备文件映射到进程的地址空间
//size_t mapsize; //内存映射后,系统分配的地址空间的大小,默认为128KB
struct binder_state *bs;
union selinux_callback cb;
char *driver;
if (argc > 1) {
driver = argv[1];
} else {
driver = "/dev/binder";
}
//binder_open:打开binder设备文件,调用mmap函数进行内存映射,申请128k字节大小的内存空间
bs = binder_open(driver, 128*1024);
...
//binder_become_context_manager:将servicemanager注册成为Binder机制的上下文管理者,这个管理者在整个系统只有一个
if (binder_become_context_manager(bs)) {
ALOGE("cannot become context manager (%s)\n", strerror(errno));
return -1;
}
...
if (getcon(&service_manager_context) != 0) {
ALOGE("SELinux: Failed to acquire service_manager context. Aborting.\n");
abort();
}
//servicemanager成功注册成为Binder机制的上下文管理者后,servicemanager就是Binder机制的“总管”了,
//它需要在系统运行期间处理client端的请求,由于client端的请求不确定何时发送,因此需要通过无限循环来实现
binder_loop(bs, svcmgr_handler);
return 0;
}
- 上面代码可以看出,servicemanager的入口函数主要做了三件事
- 打开binder设备文件,调用mmap函数进行内存映射,申请128k字节大小的内存空间
- 将servicemanager注册成为Binder机制的上下文管理者,这个管理者在整个系统只有一个
- servicemanager成功注册成为Binder机制的上下文管理者后,servicemanager就是Binder机制的“总管”了,它需要在系统运行期间通过无限循环处理client端的请求
用户态和内核态
- 当一个进程在执行用户自己的代码时处于用户态,比如open函数,它运行在用户空间,当前的进程处于用户态。
- 当一个进程因为系统调用进入内核代码中执行时就处于内核态,比如open函数通过系统调用(__open()函数),查找到了open函数在Kernel Binder对应的函数为binder_open,这时binder_open运行在内核空间,当前的进程由用户态切换到内核态。
2. 注册服务
- Server通过Binder驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。Binder 驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
- 例如MediaPlayerService是Server端,MediaPlayerService是系统多媒体服务的一种,系统多媒体服务是由一个叫做MediaServer的服务进程提供的;
- 在Android系统启动时,MediaServer也被启动,入口函数如下
int main(int argc __unused, char **argv __unused)
{
signal(SIGPIPE, SIG_IGN);
//获取ProcessState实例,打开/dev/binder设备,设定Binder最大的支持线程数
//并使用mmap为Binder驱动分配一个虚拟地址空间用来接收数据。
sp<ProcessState> proc(ProcessState::self());
//获取ServiceManager,其他进程就可以和当前的ServiceManager进行交互
//ServiceManager中使用了Binder通信(BpBinder),它自身也是属于Binder体系的
//这里得到的实例是BpServiceManager,它实现了IServiceManager,并且通过BpBinder来实现通信
sp<IServiceManager> sm(defaultServiceManager());
ALOGI("ServiceManager: %p", sm.get());
InitializeIcuOrDie();
//注册MediaPlayerService,
//其中调用了defaultServiceManager()->addService将数据打包发送给BpBinder来进行处理
//BpBinder新建一个IPCThreadState对象,并将通信的任务交给IPCThreadState
//IPCThreadState的writeTransactionData函数用于将命令协议和数据写入到mOut中
//IPCThreadState的waitForResponse函数主要做了两件事,
//一件事是通过ioctl函数操作mOut和mIn来与Binder驱动进行数据交互,另一件事是处理各种命令协议。
MediaPlayerService::instantiate();
ResourceManagerService::instantiate();
registerExtensions();
//启动Binder线程池
ProcessState::self()->startThreadPool();
//当前线程加入到线程池
IPCThreadState::self()->joinThreadPool();
}
- BpBinder,BBinder是Binder通信的“双子星”,都继承了IBinder;
- BpBinder是Client端与Server交互的代理类,而BBinder则代表了Server端。
- BpBinder和BBinder是一一对应的,BpBinder会通过handle来找到对应的BBinder
- BpBinder和BBinder负责Binder的通信,而IServiceManager用于处理ServiceManager的业务
- MediaServer通过Binder驱动,使用mmap接收数据;
- 获取一个IServiceManager的实例BpServiceManager,其并且通过BpBinder来实现通信;
- 通过defaultServiceManager注册MediaPlayerService,这里MediaPlayerService是Client端,用于请求添加系统服务。而Server端则是指的是ServiceManager,用于完成系统服务的添加,两端通过向Binder驱动发送命令协议来完成系统服务的添加;
3. 获取服务
- Client进程通过Binder驱动 向 ServiceManager进程 获取相应的Service信息(Binder代理对象,具有和 Binder 一样的方法,但这些方法并没有 Server 进程中 Binder 对象那些方法的能力,这些方法只需要把把请求参数交给Binder驱动即可。对于 Client 进程来说和直接调用 Binder 中的方法是一样的)
- 系统服务的注册流程中,在Kernel Binder中会调用do_add_service函数,其内部会将包含服务名和handle值的svcinfo保存到svclist列表中。同样的,在获取服务的流程中,find_svc函数中会遍历svclist列表,根据服务名查找对应服务是否已经注册,如果已经注册就会返回对应的svcinfo,如果没有注册就返回NULL。
- 服务端ServiceManager处理请求:servicemanager的入口函数中调用了binder_loop函数,其在无限循环中不断的调用ioctl函数,它不断的使用BINDER_WRITE_READ指令查询Binder驱动中是否有新的请求;
- 仍旧以MediaPlayerService为例,获取MediaPlayerService需要调用getMediaPlayerService函数
IMediaDeathNotifier::getMediaPlayerService()
{
ALOGV("getMediaPlayerService");
Mutex::Autolock _l(sServiceLock);
if (sMediaPlayerService == 0) {
sp<IServiceManager> sm = defaultServiceManager();//实例为BpServiceManager
sp<IBinder> binder;
do {
//获取名为”media.player”的系统服务(MediaPlayerService),返回的值为BpBinder
binder = sm->getService(String16("media.player"));
if (binder != 0) {
break;
}
//这个时候MediaPlayerService可能还没有向ServiceManager注册
//所以休眠0.5s后继续调用getService,直到获取服务对应的为止
usleep(500000); //4
} while (true);
if (sDeathNotifier == NULL) {
sDeathNotifier = new DeathNotifier();
}
binder->linkToDeath(sDeathNotifier);
//interface_cast通过BpBinder的handle来找到对应的服务,即BpMediaPlayerService。
sMediaPlayerService = interface_cast<IMediaPlayerService>(binder);
}
ALOGE_IF(sMediaPlayerService == 0, "no media player service!?");
return sMediaPlayerService;
}
- 通过getMediaPlayerService获取名为”media.player”的系统服务(MediaPlayerService),其中调用interface_cast通过BpBinder的handle来找到对应的服务,即BpMediaPlayerService
4. 使用服务
- Client进程 根据获取到的 Service信息(Binder代理对象BinderProxy),通过Binder驱动 建立与 该Service所在Server进程通信的链路,并开始使用服务
- Binder和BinderProxy实现了IBinder接口,Binder是服务端的代表,而BinderProxy是客户端的代表; Parcel是一个数据包装器,它可以在进程间进行传递,Parcel既可以传递基本数据类型也可以传递Binder对象,Binder通信就是通过Parcel来进行客户端与服务端数据交互。
- 例如之前的Android进阶笔记-3. Service 启动过程 & 绑定过程中讲过的ContextImpl的startService方法
private ComponentName startServiceCommon(Intent service, boolean requireForeground, UserHandle user) {
try {
...
//
ComponentName cn = ActivityManager.getService().startService(
mMainThread.getApplicationThread(), service,
service.resolveTypeIfNeeded(getContentResolver()), requireForeground,
getOpPackageName(), getAttributionTag(), user.getIdentifier());
...
return cn;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
- 上面代码调用ActivityManager.getService()获取到ActivityManagerService的代理类,并调用了startService方法,而startService方法的实现是在ActivityManagerService中
- ActivityManager.getService()代码如下, 其中调用了ServiceManager.getService(Context.ACTIVITY_SERVICE)
@UnsupportedAppUsage
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
@UnsupportedAppUsage
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
从不同角度看Binder定义
- 从IPC角度:Binder是Android中的一种跨进程通信方式,该通信方式在linux中没有,是Android独有;
- 从Android Driver层:Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder;
- 从Android Framework层:Binder是各种Manager(ActivityManager、WindowManager等)和相应xxxManagerService的桥梁;
- 从Android APP层:Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的 Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务
- 从 Server 进程的角度,Binder 指的是 Server 中的 Binder 实体对象;
- 从 Client 进程的角度,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理
- 从传输过程的角度,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。