文章目录
在Linux系统里面,进程之间是相互隔离的,也就是说进程之间的各个数据是互相独立,互不影响,而如果一个进程崩溃了,也不会影响到另一个进程。
Android系统其底层是采用Linux作为基底,上层采用包含虚拟机的Java层以及Native层,通过系统调用(Syscall)连通系统的内核空间与用户空间。用户空间主要采用C++和Java代码,通过JNI技术打通用户空间的Java层和Native层(C++/C)。
每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。真正通信的核心环节还是在Binder Driver。
用户空间/内核空间:
Linux Kernel是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
对于Kernel这么一个高安全级别的东西,显然是不容许其它的应用程序随便调用或访问的,所以需要对Kernel提供一定的保护机制,这个保护机制用来告诉那些应用程序,你只可以访问某些许可的资源,不许可的资源是拒绝被访问的,于是就把Kernel和上层的应用程序抽像的隔离开,分别称之为Kernel Space和User Space。
用户空间访问内核空间的唯一方式就是系统调用;通过这个统一入口接口,所有的资源访问都是在内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。
为什么要使用Binder
Linux现有的所有进程间IPC方式:
- 管道:在创建时分配一个page大小的内存,缓存区大小比较有限;
- 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
- 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
4.套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信; - 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;
Android使用的Linux内核拥有着非常多的跨进程通信机制,比如管道,System V,Socket等;为什么还需要单独搞一个Binder出来呢?主要有两点,性能和安全。在移动设备上,广泛地使用跨进程通信肯定对通信机制本身提出了严格的要求;Binder相对出传统的Socket方式,更加高效;另外,传统的进程通信方式对于通信双方的身份并没有做出严格的验证,只有在上层协议上进行架设;比如Socket通信ip地址是客户端手动填入的,都可以进行伪造;而Binder机制从协议本身就支持对通信双方做身份校检,因而大大提升了安全性。这个也是Android权限模型的基础。
为什么 Android 要采用 Binder 作为 IPC 机制?
Binder是什么
- 通常意义下,Binder指的是一种通信机制;我们说AIDL使用Binder进行通信,指的就是Binder这种IPC机制。
- 对于Server进程来说,Binder指的是Binder本地对象
- 对于Client来说,Binder指的是Binder代理对象,它只是Binder本地对象的一个远程代理;对这个Binder代理对象的操作,会通过驱动最终转发到Binder本地对象上去完成;对于一个拥有Binder对象的使用者而言,它无须关心这是一个Binder代理对象还是Binder本地对象;对于代理对象的操作和对本地对象的操作对它来说没有区别。
- 对于传输过程而言,Binder是可以进行跨进程传递的对象;Binder驱动会对具有跨进程传递能力的对象做特殊处理:自动完成代理对象和本地对象的转换。在驱动中,Binder本地对象的代表是一个叫做binder_node的数据结构,Binder代理对象是用binder_ref代表的
Binder架构
binder在framework层,采用JNI技术来调用native(C/C++)层的binder架构,从而为上层应用程序提供服务。在native层中,binder是C/S架构,分为Bp端(BpBinder - Client)和Bn端(BBinder - Server)。
- 图中红色代表整个framework层binder架构(C/S架构);Binder类代表Server端,BinderProxy类代码Client端;
- 图中蓝色代表Native层Binder架构相关组件,分为Bp端(BpBinder - Client)和Bn端(BBinder - Server);
- Binder 在 framework 层进行了封装,通过 JNI 技术调用 Native(C/C++)层的 Binder 架构。
- Binder 在 Native 层以 ioctl 的方式与 Binder 驱动通讯。
上层framework层的Binder逻辑是建立在Native层架构基础之上的,核心逻辑都是交予Native层方法来处理。也只有真正理解了Native层Binder架构,才能算掌握的Binder。
Client进程只不过是持有了Server端的代理;代理对象协助驱动完成了跨进程通信。
native层的Binder通信采用C/S架构,包含client、server、serviceManager、Binder驱动,client / server / serviceManager 位于用户空间,Binder驱动位于内核空间。
-
Native层的ServiceManager(C++),用于管理系统中的各种服务,是整个Binder通信机制的大管家。
-
BpBinder(客户端) 和BBinder(服务端) 都是从IBinder类中派生而来的务。
client端:BpBinder.transact()来发送事务请求; server端:BBinder.onTransact()会接收到相应事务。
Binder原理
Native层的ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程, 要掌握Binder机制,首先需要了解系统是如何首次启动ServiceManager。当Service Manager启动之后,Client端和Server端通信时都需要先获取Service Manager接口,才能开始通信服务。
图中的Client、Server、ServiceManager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder Driver进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client、Server、ServiceManager位于用户空间。
Client、Server、ServiceManager都位于不同的进程之中,是基于Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。这3大过程每一次都是一个完整的Binder IPC过程。
1、注册服务:首先AMS注册到ServiceManager。该过程:AMS所在进程(system_server)是客户端,ServiceManager是服务端。
2、获取服务:Client进程使用AMS前,须先向ServiceManager中获取AMS的代理类AMP。该过程:AMP所在进程(app process)是客户端,ServiceManager是服务端。
3、使用服务: app进程根据得到的代理类AMP,便可以直接与AMS所在进程交互。该过程:AMP所在进程(app process)是客户端,AMS所在进程(system_server)是服务端。
-
首先需要注册服务端,只有注册了服务端,客户端才有通讯的目标,服务端通过 ServiceManager 注册服务,注册的过程就是向 Binder 驱动的全局链表 binder_procs 中插入服务端的信息(binder_proc 结构体,每个 binder_proc 结构体中都有 todo 任务队列),然后向 ServiceManager 的 svcinfo 列表中缓存一下注册的服务。
-
有了服务端,客户端就可以跟服务端通讯了,通讯之前需要先获取到服务,拿到服务的代理,也可以理解为引用。比如下面的代码://获取WindowManager服务引用
WindowManager wm = (WindowManager)getSystemService(getApplication().WINDOW_SERVICE);
获取服务端的方式就是通过 ServiceManager 向 svcinfo 列表中查询一下返回服务端的代理,svcinfo 列表就是所有已注册服务的通讯录,保存了所有注册的服务信息。
-
有了服务端的引用我们就可以向服务端发送请求了,通过 BinderProxy 将我们的请求参数发送给 ServiceManager,通过共享内存的方式使用内核方法 copy_from_user() 将我们的参数先拷贝到内核空间,这时我们的客户端进入等待状态,然后 Binder 驱动向服务端的 todo 队列里面插入一条事务,执行完之后把执行结果通过 copy_to_user() 将内核的结果拷贝到用户空间(这里只是执行了拷贝命令,并没有拷贝数据,binder只进行一次拷贝),唤醒等待的客户端并把结果响应回来,这样就完成了一次通讯。
Binder通信简述
前面说过Binder机制的3大步骤(注册服务、获取服务、使用服务)都有相应的client端和server端。
Client进程通过RPC与Server通信,可以简单地划分为三层,驱动层、IPC层、业务层。demo()便是Client端和Server共同协商好的统一方法;handle、RPC数据、代码、协议这4项组成了IPC层的数据,通过IPC层进行数据传输;而真正在Client和Server两端建立通信的基础设施便是Binder Driver。
例如,当名为BatteryStatsService的Server向ServiceManager注册服务的过程中,IPC层的数据组成为:Handle=0,RPC代码为ADD_SERVICE_TRANSACTION,RPC数据为BatteryStatsService,Binder协议为BC_TRANSACTION。
Binder通信协议
Binder协议包含在IPC数据中,分为两类:
-
BINDER_COMMAND_PROTOCOL:binder请求码,以”BC_“开头,简称BC码,用于从IPC层传递到Binder Driver层;
-
BINDER_RETURN_PROTOCOL :binder响应码,以”BR_“开头,简称BR码,用于从Binder Driver层传递到IPC层;
Binder IPC通信至少是两个进程的交互:
-
client进程执行binder_thread_write,根据BC_XXX命令,生成相应的binder_work;
-
server进程执行binder_thread_read,根据binder_work.type类型,生成BR_XXX,发送到用户空间处理。
Binder通信过程
其中binder_work.type共有6种类型:
- BINDER_WORK_TRANSACTION //最常见类型
- BINDER_WORK_TRANSACTION_COMPLETE
- BINDER_WORK_NODE
- BINDER_WORK_DEAD_BINDER
- BINDER_WORK_DEAD_BINDER_AND_CLEAR
- BINDER_WORK_CLEAR_DEATH_NOTIFICATION
一次完整的通讯模型如下:
- BC码,用于从IPC层传递到Binder Driver层;
- BR码,用于从Binder Driver层传递到IPC层;
oneway与非oneway: 都是需要等待Binder Driver的回应消息BR_TRANSACTION_COMPLETE。 主要区别在于oneway的通信收到BR_TRANSACTION_COMPLETE则返回,而不会再等待BR_REPLY消息的到来。另外,oneway的binder IPC则接收端无法获取对方的pid。
Binder内存模型
虚拟进程地址空间(vm_area_struct)和虚拟内核地址空间(vm_struct)都映射到同一块物理内存空间。当Client端与Server端发送数据时,Client(作为数据发送端)先从自己的进程空间把IPC通信数据copy_from_user拷贝到内核空间,而Server端(作为数据接收端)与内核共享数据,不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。一般地做法,需要Client端进程空间拷贝到内核空间,再由内核空间拷贝到Server进程空间,会发生两次拷贝。
对于进程和内核虚拟地址映射到同一个物理内存的操作是发生在数据接收端,而数据发送端还是需要将用户态的数据复制到内核态。到此,可能有读者会好奇,为何不直接让发送端和接收端直接映射到同一个物理空间,那样就连一次复制的操作都不需要了,0次复制操作那就与Linux标准内核的共享内存的IPC机制没有区别了,对于共享内存虽然效率高,但是对于多进程的同步问题比较复杂,而管道/消息队列等IPC需要复制2两次,效率较低。这里就不先展开讨论Linux现有的各种IPC机制跟Binder的详细对比,总之Android选择Binder的基于速度和安全性的考虑。
下面这图是从Binder在进程间数据通信的流程图,从图中更能明了Binder的内存转移关系。
Binder线程池创建
Android系统启动完成后,ActivityManager、PackageManager等各大服务都运行在system_server进程里,app进程如果需要使用系统服务需要通过binder,来完成进程之间的通信。但不管是system_server进程,还是app进程,都是通过zygote fork 出来的,而这个fork的过程,也就是创建新进程时会执行onZygoteInit(),以启动binder线程池。
Binder线程的创建:在其所在进程的创建过程中,Binder线程也随之产生。Java层进程的创建都是通过Process.start()方法,向Zygote进程发出创建进程的socket消息,Zygote收到消息后会调用Zygote.forkAndSpecialize()来fork出新进程,在新进程中会调用到RuntimeInit.nativeZygoteInit方法,该方法经过jni映射,最终会调用到app_main.cpp中的onZygoteInit()中。
每个应用进程只允许启动一个binder线程池,且本次创建的是binder主线程(isMain=true),并且主线程是不会退出的;其余binder线程池中的线程都是由Binder驱动根据IPC通信需求来控制创建的,当线程执行binder_thread_read的过程中,发现当前没有空闲线程,且没有达到上限,则创建新的binder线程。
每次由Zygote fork出新进程的过程中,伴随着创建binder线程池,调用spawnPooledThread来创建binder主线程。当线程执行binder_thread_read的过程中,发现当前没有空闲线程,没有请求创建线程,且没有达到上限,则创建新的binder线程。
Binder进程与线程
对于底层Binder驱动,通过binder_procs链表记录所有创建的binder_proc结构体,binder驱动层的每一个binder_proc结构体都与用户空间的一个用于binder通信的进程一一对应,且每个进程有且只有一个ProcessState对象,这是通过单例模式来保证的。在每个进程中可以有很多个线程,每个线程对应一个IPCThreadState对象,IPCThreadState对象也是单例模式,即一个线程对应一个IPCThreadState对象,在Binder驱动层也有与之相对应的结构,那就是Binder_thread结构体。在binder_proc结构体中通过成员变量rb_root threads,来记录当前进程内所有的binder_thread。
Binder线程池:每个Server进程在启动时会创建一个binder线程池,并向其中注册一个Binder线程;之后Server进程也可以向binder线程池注册新的线程,或者Binder驱动在探测到没有空闲binder线程时会主动向Server进程注册新的的binder线程。对于一个Server进程有一个最大Binder线程数限制,默认为16个binder线程。对于所有Client端进程的binder请求都是交由Server端进程的binder线程来处理的。
Binder传输过程
Binder IPC机制,就是指在进程间传输数据(binder_transaction_data),一次数据的传输,称为事务(binder_transaction)。对于多个不同进程向同一个进程发送事务时,这个同一个进程或线程的事务需要串行执行,在Binder驱动中为binder_proc和binder_thread都有todo队列。
也就是说对于进程间的通信,就是发送端把binder_transaction节点,插入到目标进程或其子线程的todo队列中,等目标进程或线程不断循环地从todo队列中取出数据并进行相应的操作。
在Binder驱动层,每个接收端进程都有一个todo队列,用于保存发送端进程发送过来的binder请求,这类请求可以由接收端进程的任意一个空闲的binder线程处理;接收端进程存在一个或多个binder线程,在每个binder线程里都有一个todo队列,也是用于保存发送端进程发送过来的binder请求,这类请求只能由当前binder线程来处理。binder线程在空闲时进入可中断的休眠状态,当自己的todo队列或所属进程的todo队列有新的请求到来时便会唤醒,如果是由所需进程唤醒的,那么进程会让其中一个线程处理响应的请求,其他线程再次进入休眠状态。
Binder死亡回调
死亡通知是为了让Bp端(客户端进程)进能知晓Bn端(服务端进程)的生死情况,当Bn端进程死亡后能通知到Bp端。
Binder驱动
Binder Driver是Android专有的,但底层的驱动架构与Linux驱动是一样。运行在内核空间的,负责各个用户进程通过Binder进行通信的内核模块。
- 将misc设备(字符驱动设备)注册在设备目录/dev下,作为虚拟字符设备,用户通过dev/binder访问它;
- 但它并没有直接操作硬件,只是对设备内存的处理。
- 工作在内核态,提供了驱动设备的初始化(binder_init)、打开(binder_open)、映射(binder_mmap)、数据操作(binder_ioctl)等标准文件操作。
用户态的程序调用Kernel层驱动是需要陷入内核态,进行系统调用(syscall),比如打开Binder驱动方法的调用链为: open-> __open() -> binder_open()。当用户空间调用open()/mmap()/ioctl()方法,最终会调用binder驱动的binder_open()/binder_mmap()/binder_ioctl(),从用户态进入内核态。
binder核心方法:
-
binder_init :注册misc设备,创建dev/binder设备节点
-
binder_open *:打开binder驱动设备,获取Binder Driver的文件描述符。创建binder_proc对象,并把当前进程等信息保存到binder_proc对象,该对象管理IPC所需的各种信息并拥有其他结构体的根结构体;再把binder_proc对象保存到文件指针filp,以及把binder_proc加入到全局链表binder_procs。
-
binder_mmap :首先在内核虚拟地址空间,申请一块与用户虚拟内存相同大小的内存;然后再申请1个page大小的物理内存,再将同一块物理内存分别映射到内核虚拟地址空间和用户虚拟内存空间,从而实现了用户空间的Buffer和内核空间的Buffer同步操作的功能。
-
binder_ioctl :负责在两个进程间收发IPC数据和IPC reply数据。
serviceManager
ServiceManager是Binder IPC通信过程中的守护进程,是由init进程通过解析init.rc文件而创建的,其所对应的可执行程序/system/bin/servicemanager,进程名为/system/bin/servicemanager。servicemanager的核心工作就是注册服务和查询服务。
ServiceManger集中管理系统内的所有服务,通过权限控制进程是否有权注册服务,通过字符串名称来查找对应的Service; 由于ServiceManger进程跟所有向其注册的服务建立死亡通知, 那么当服务所在进程死亡后, 都将告知ServiceManager. 每个Client通过查询ServiceManager可获取Server进程的情况,降低所有Client进程直接检测会导致负载过重。
- ServiceManager 分为 framework 层和 native 层,framework 层只是对 native 层进行了封装方便调用,图上展示的是 native 层的 ServiceManager 启动过程。
ServiceManager的启动流程
ServiceManager#main()
1、通过系统调用陷入内核,打开Binder设备驱动,在Binder驱动层创建一个binder_proc对象,同时放入全局链表binder_procs,再通过ioctl()检验当前binder版本与Binder驱动层的版本是否一致;调用mmap()进行内存映射,同理mmap()方法经过系统调用,对应于Binder驱动层的binder_mmap()方法,该方法会在Binder驱动层创建Binder_buffer对象,并放入当前binder_proc的proc->buffers链表。
2、注册成为binder服务的大管家,通知binder驱动使其成为守护进程:binder_become_context_manager;
3、验证selinux权限,判断进程是否有权注册或查看指定服务;
4、进入无限循环,处理client端发来的请求:binder_loop;
ServiceManager的获取过程
当进程注册服务(addService)或 获取服务(getService)的过程之前,都需要先调用defaultServiceManager()方法来获取gDefaultServiceManager对象。对于gDefaultServiceManager对象(单例),如果存在则直接返回;如果不存在则创建该对象。
gDefaultServiceManager的创建过程,可分解为以下3个步骤:
defaultServiceManager 等价于 new BpServiceManager(new BpBinder(0));defaultServiceManager()返回的是BpServiceManager对象,用于跟servicemanager进程通信;
1、 ProcessState::self():用于获取ProcessState对象(单例),每个进程有且只有一个ProcessState对象(一个进程只能打开binder设备一次,其中ProcessState的成员变量mDriverFD记录binder驱动的fd,用于访问binder设备。),如果存在则直接返回,不存在则创建,ProcessState的创建过程:
-
调用open(),打开/dev/binder驱动设备;
-
再利用mmap(),创建大小为1M-8K的内存地址空间,映射内核的地址空间,用来接收事物;
-
设定当前进程最大的最大并发Binder线程个数为16。
2、getContextObject(): 获取BpBinder对象,对于handle=0的BpBinder对象(在整个Binder系统中handle=0代表ServiceManager所对应的BBinder),如果存在则直接返回,不存在才创建。BpBinder通过handler来指向所对应BBinder, 。
3、interface_cast():用于获取BpServiceManager对象,
注册服务
-
注册服务: do_add_service():先检查权限,检查selinux权限是否满足;服务检索,根据服务名来查询匹配的服务;当查询到已存在同名的服务,则先清理该服务信息,再将当前的服务加入到服务列表svclist(记录服务名和handle信息)
-
注册 MediaPlayerService 服务端,我们通过 ServiceManager 的 addService() 方法来注册服务。
-
首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令(ioctl 的命令,BC 可以理解为 binder client 客户端发过来的请求命令)携带 ADD_SERVICE_TRANSACTION 命令,同时注册服务的线程进入等待状态 waitForResponse()。 Binder 驱动收到请求命令向 ServiceManager 的 todo 队列里面添加一条注册服务的事务。事务的任务就是创建服务端进程 binder_node 信息并插入到 binder_procs 链表中。
-
事务处理完之后发送 BR_TRANSACTION 命令,ServiceManager 收到命令后向 svcinfo 列表中添加已经注册的服务。最后发送 BR_REPLY 命令唤醒等待的线程,通知注册成功。
获取服务
-
查询服务: do_find_service():查询到目标服务,并返回该服务所对应的handle。从svclist服务列表中,根据服务名遍历查找是否已经注册。当服务已存在svclist,则返回相应的服务名,否则返回NULL。当找到服务的handle, 则调用bio_put_ref(reply, handle),将handle封装到reply
-
获取服务的过程与注册类似,相反的过程。通过 ServiceManager 的 getService() 方法来注册服务。
-
首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令携带 CHECK_SERVICE_TRANSACTION 命令,同时获取服务的线程进入等待状态 waitForResponse()。
-
Binder 驱动收到请求命令向 ServiceManager 的发送 BC_TRANSACTION 查询已注册的服务,查询到直接响应 BR_REPLY 唤醒等待的线程。若查询不到将与 binder_procs 链表中的服务进行一次通讯再响应。
使用服务
-
我们在使用 Binder 时基本都是调用 framework 层封装好的方法,AIDL 就是 framework 层提供的傻瓜式是使用方式。假设服务已经注册完,我们来看看客户端怎么执行服务端的方法。
-
首先我们通过 ServiceManager 获取到服务端的 BinderProxy 代理对象,通过调用 BinderProxy 将参数,方法标识(例如:TRANSACTION_test,AIDL中自动生成)传给 ServiceManager,同时客户端线程进入等待状态。
-
ServiceManager 将用户空间的参数等请求数据复制到内核空间,并向服务端插入一条执行执行方法的事务。事务执行完通知 ServiceManager 将执行结果从内核空间复制到用户空间,并唤醒等待的线程,响应结果,通讯结束。
参考:
Gityuan的 Binder系列 (基于 Android 6.0)