Android Binder机制

先来看一些技术名词

基础

1、虚拟内存地址。

虚拟内存地址是操作系统为每个进程创建的独立的、连续的内存地址范围(从 0 到最大值)。进程"看到"和使用的都是这个虚拟地址。操作系统(配合 MMU 硬件)负责把虚拟地址动态翻译成真实的物理内存地址。如此,每条进程都会有一块自己的内存地址(虚拟的,当然最后会有对应的真实的内存地址),它们所使用的内存地址不重叠,一条进程的运行不会影响另一条进程,确保了进程安全,必要时操作系统还可以把硬盘也映射成虚拟内存。虚拟内存,这里面就用到了内存映射的技术。这种内存映射技术需要同时用到操作系统和MMU硬件。

2、用户态/内核态

用户态和内核态是CPU运行的两种模式,对应着用户权限、内核权限两种权限模式,由一个硬件寄存器记录着,也就是硬件寄存器里的这个值控制着CPU处于哪种状态模式。内核态(内核权限)下可以执行任何命令,用户态(用户权限)则只能执行常规操作 。当CPU通电后,启动芯片被触发,引导程序会将系统内核加载到内存,系统内核运载起来,此时CPU会默认给操作系统最高权限(即控制权限切换的权力),操作系统也会采取一系列手段让自己:独占控制权,以后我说升到内核态就升到内核态,我说降到用户态就降到用户态,其他人谁也别想染指!并且这种切换还伴随着硬件开关( 前面说的那个寄存器**)的变动!**

普通进程想要执行一些敏感操作、危险操作,必须通过调用系统接口的方式间接调用,系统收到调用后会先将CPU(对于多核CPU来说就是当前所在的核)升级到内核态,然后再执行。执行完再降回用户态。

那么:既然都通过系统接口调用了,系统接口肯定是值得信赖的,为什么非要升级到内核态呢?

因为硬件设计规定:这些敏感、危险的操作必须在内核态(寄存器里记录显示处于内核态)下才能执行 !硬件又为什么要这样设计呢?如果不这样设计,那么任何进程都可以直接执行危险命令,比如把硬件烧毁、让CPU关机等等,必然不可!但是操作系统是可值得信任的,它留出的调用接口势必不会威胁到内核安全。对于硬件来说,操作系统和普通程序没有区别,都是软件,都可以调用危险命令(能不能执行是另一回事),但是安全也是必须要保证的,那,咱们重新梳理一下就能理清了:CPU通电后引导程序把系统内核加载了进来,系统启动后夺取了最高控制权(切换内核/用户的权限),成为硬件的控制者,也是硬件的信任者,操作系统做什么,硬件都执行,此时若有其他进程(程序)想执行危险命令,硬件首先就会判断出它没有最高控制权,不值得信任,拒绝执行,甚至杀死该进程!这样就保证了安全。

如果引导程序引入的是一段恶意程序,最高控制权被这段恶意程序夺取了,那么是不是就危险了?

是的,此时原本的操作系统如果启动了也只是会被当做普通程序,而这段恶意程序就变成了新的操作系统的角色。

3、用户空间(User Space)/内核空间(Kernel Space)

两种空间都是说内存的,但是乍看是不是觉得就是真实物理内存上的两块不同的区域?它们确实都有对应的真实内存地址,但是它们其实也都是指的虚拟内存地址,而且它们之间的虚拟内存地址不重叠。用户空间是供普通进程使用的,内核空间是供系统内核使用的。每个进程有自己的独立用户空间,而所有进程共享同一个内核空间。

4、设备文件

在Windows上 /dev/binder 这样的写法会被认为是一个目录,但是在Linux上它代表一个设备,叫设备文件,你可以认为它是操作设备的一个接口,大致示意如下

int fd = open("/dev/binder", O_RDWR); //打开该设备

write(fd, data, len); // 向设备里写数据

read(fd, buf, size); // 读取设备里的数据

close(fd);

Binder

1、普通跨进程通信

在讲Binder之前,先来看一下一次普通的进程间通信大致是怎样的。比如进程1(作为客户端)有数据要发送给进程2(作为服务端):进程1先把数据从它的用户空间复制到内核空间,进程2再把数据从内核空间复制到它的用户空间,经过两次数据复制 ,数据就由1传到了2。如下所示。再次提醒:用户空间和内核空间都是虚拟内存地址。

2、Binder基本组件

Binder的机制也是类似的。Binder通信架构是典型的C/S架构,由Client、Server、ServiceManager、Binder 驱动四大组件组成。Binder驱动是整个机制的"物理基础",它运行在内核空间,是内核的一部分,随着内核的加载而一起启动,本质上是一个混合设备驱动,通过 /dev/binder 设备节点为上层提供服务。

ServiceManager是所有服务的登记中心,每条服务都要向它注册,报告自己的地址、名字等信息。

ServiceManager 靠 Binder 驱动收发消息,Client/Server 也都靠 Binder 驱动收发消息,Binder 驱动是所有人的唯一通信通道。

3、Binder背后的基础机制

当系统启动的时候,init进程会创建出一条进程叫ServiceManager,它是整个系统里的第一条服务,它通过open打开 /dev/binder 就和Binder驱动建立了连接,Binder 驱动会给 ServiceManager 创建系统的第一个Binder实体节点 ,不过这个节点会比较特殊,它的句柄是0,全系统约定只要发给句柄 0 的消息,就是发给 ServiceManager的,你可以认为全系统都知道ServiceManager家在哪以及怎样才能联系到它;此后ServiceManager 进入死循环,等待 Binder 驱动转发过来的请求。Binder实体节点(binder_node )是一种数据结构,存放于Binder驱动内部,记录着server的身份证+真身地址,凡是客户端发给Binder驱动的请求,Binder驱动都可以根据binder_node找到对应的server,然后把请求转交给server去处理。

日常接触到的几乎所有 Android 进程(APP、系统服务、system_server、ServiceManager 等),都会自动 open binder;只有底层极少数原生守护进程不会。也就是说,几乎所有进程在启动的时候,系统(Android 系统底层 libbinder 库代码)都会自动让它通过open()打开 /dev/binder 驱动文件,建立与内核Binder驱动的连接。之所以设计成这样,是因为在 Android 里,没有任何一个应用 / 系统进程能 "独善其身",全都必须频繁和其他进程、系统服务打交道;不连 Binder,进程直接就是个 "孤岛",根本跑不起来。即使你仅仅只是展示一个Activity静态页面,也需要和AMS打交道!不过这种open()建立连接的方式只在进程启动时执行一次,全程复用这条连接,不需要多次建立连接,直至下次进程重启。

连接建立后紧接着,系统就会继续让进程通过mmap()执行内存映射,Binder驱动收到mmap()后,会给进程分配一块虚拟内存地址作为进程使用的用户空间数据缓冲区(暂称user_buffer) ,但是驱动同时也会开辟出另一块内核空间的虚拟内存地址(暂称kernel_buffer) ,最终把这两块虚拟内存地址映射到同一块物理内存地址上。为什么要这么做?后面讲。

深度思考

每个进程都有一块自己的用户空间数据缓冲区,那进程多了之后岂不占用很多内存?

不会。这块缓冲区是虚拟的,当用不到的时候并没有给分配真实的内存,只在使用时才会临时给其映射到真实的内存地址,使用完毕后就又把真实内存地址收回。而且这块区域并不大,一般不会超过1MB,系统也会限制进程总数,不会无限创建。这里1MB的限制正是造成Intent传带数据超过1MB就会报TransactionTooLargeException的原因。

4、服务上线

前面已经讲过,进程一启动系统就会自动让它跟Binder驱动建立连接并完成内存映射,这是所有进程都会经历的,不管是否作为Server。如果作为Server,还有更多的工作要做:Server 端必须创建实现了 IBinder 接口的服务实例对象
对于系统服务来说 ,大部分系统服务在创建完实例后,会由system_server进程(少数是其他机制触发)触发它们去执行ServiceManager.addService(服务名, IBinder对象)以便把自己的服务名称和IBinder实例打包成带有句柄0的 Binder 请求发送给Binder驱动。Binder 驱动收到这个请求后为它们创建各自的binder_node(发现它们的binder第一次跨进程传递就会为其生成binder_node),封装后把请求直接转发给 ServiceManager 进程。ServiceManager 收到注册请求后,会在内部维护一张系统服务注册表,把「服务名 ↔ 对应 binder_node」的映射关系永久保存下来。

注册完成后,这个 Server 就正式在全系统 "挂牌上线" 了,只要其他进程知道服务名,就能找到它。

完成注册的 Server 并不会主动做任何操作,而是和 ServiceManager 一样,进入阻塞等待状态,静静等候 Binder 驱动转发来自客户端的调用请求。

5、客户端使用服务

查询服务 :客户端通过 getService(服务名字) 经由Binder驱动向ServiceManager查询服务。ServiceManager找到后,会返回一个该服务的Binder代理对象(Proxy)给客户端。客户端拿到这个代理对象,就相当于拿到了与远程服务通信的"遥控器"。
发起请求 :客户端通过代理对象发起方法调用,请求中带有 目标句柄 + 方法号 + 参数 + 回复缓冲区
底层通信与处理 :客户端请求会被发送到Binder驱动,由于Binder驱动作为中介全程参与了双方的沟通,它掌握的信息足够知道该把请求转交给谁处理,服务端处理完毕后将结果保存在它的普通用户空间里,Binder驱动将结果从普通用户空间再复制到内核空间kernel_buffer 里,而kernel_buffer由于内存映射其实和客户端的用户空间是同一块物理内存,所以也只用了一次复制,就把结果传回给了客户端。

当进程A发送数据给B时,数据由A的用户空间复制到B的内核接收缓冲区,但是由于内存映射关系B的内核接收缓冲区其实和B的用户空间里的数据缓冲区是同一块物理内存,所以B可以直接读取;反过来,B处理请求完毕后,回复结果,则把结果数据直接复制到A的内核里的接收缓冲区,仍然由于内存映射关系,进程A也可以直接读取,所以请求数据、回复数据都只需要复制一次数据。

6、AIDL

AIDL也使用的是Binder,但是调用不到ServiceManager.addService(),它是hide方法,所以普通应用不需要(也不能)注册到全局 ServiceManager。客户端bindService后,由于需要用到AMS,所以底层会先去ServiceManager处查询到AMS,查到AMS后就把请求交给AMS,然后AMS负责让服务端启动,服务端会执行onBind()把Binder交给AMS,此时AMS又经过Binder驱动(给服务端创建binder_node)将服务端的代理交给客户端。客户端拿到代理之后,和服务端之后的交互就直接由Binder驱动来传递了,不再需要AMS、ServiceManager的参与。

7、Binder机制的优势

除了前面所说的数据复制相比传统方式少了一次,性能高以外,也更安全。UID是应用的身份证,如果两个应用共用UID就会被认为是同一个APP,就可以互相访问对方的数据,所以伪造UID如果成功骗过了系统就可以访问其他应用的数据。Socket、管道、队列等方式都可以自己声明UID,也就有伪造UID的可能,Binder机制直接从内核里查询进程的UID,进程无法伪造UID,而且Binder机制也不使用进程自己设置的UID,即使进程伪造了也没用。

相关推荐
henysugar2 个月前
Android studio编译aidl若干问题记录
android·ide·android studio·aidl
消失的旧时光-19432 个月前
Binder 是如何贯穿 ART / Native / Kernel 的?
binder
灵感菇_2 个月前
全面解析Android Binder机制
android·binder
刘信的csdn3 个月前
RK3568 Android11 使用AIDL添加Hal层binder通讯
binder·hal·aidl
tmacfrank3 个月前
Binder 预备知识
linux·运维·binder
李坤林3 个月前
Android Binder 详解(6) Binder 客户端的创建
android·binder
李坤林3 个月前
Android Binder详解【5】 ServiceManager
android·binder
李坤林3 个月前
Android Binder 详解(4) Binder 线程池
android·java·binder
菩萨摩诃萨4 个月前
面试中如何谈Binder?
binder