Binder机制(Binder机制的作用和原理?)
前言
binder机制为什么要这么设计,为什么要这么用,这就是本次分享要深挖的底层的机制设计。
深入挖掘前的知识储备
进程和多进程
计算机实际上可以做的事情实质上非常简单,比如计算两个数的和,再比如在内存中寻找到某个地址等等。这些最基础的计算机动作被称为指令 (instruction)。所谓的程序(program),就是这样一系列指令的所构成的集合。通过程序,我们可以让计算机完成复杂的操作。程序大多数时候被存储为可执行的文件,而进程(process)则是程序的一个具体体现,进程就是一个程序的执行过程,一个程序是可以被执行多次的。不同程序自然有不同的进程,这是一个理所当然的事,因为不同程序需要同时运行,并且还不能互相干扰对方的运行,同时,某些进程间还需要互通有无,就如前面说到的Activity的展示,实际是app使用了WindowManagerService提供的功能来实现的,WindowManagerService就是一个运行在另外进程里的一段程序,它是个系统级的进程,因为手机就一个屏幕,系统自然要统筹管理这个屏幕上所有的窗口展示。简单可以理解为程序运行起来就是进程,多个程序运行那自然有了多进程,多个程序之间互通有无那就是跨进程通信。
跨进程通信跨的是什么?
前面也说到了进程的概念,那么两个进程之间有通信需求的时候,为什么不能直接通信呢?跨进程通信,字面意思上是跨的进程,那么自然是因为多个进程之间有一个障碍在,那才需要跨过去,没有障碍的话那自然不需要跨过去了,直接通信就好了,那到底跨的是什么障碍呢?
通过一个类比来说明一下,进程就像我们地球上的一个个国家,每个国家都有自己的领土,领土为国家这个概念提供了一个具体的载体,其中大部分国家的领土是连续的一块,但也有的国家有很多海外领土,总的来说有领土才能称为一个国家,就算是梵蒂冈他也有自己的领土。那么进程也是一样的道理,进程之所为进程,那是因为系统保证了它的独立性,给他分配了领土也就是内存空间,同时也将这些空间隔离开,这就是进程隔离,跨进程通信跨的就是这个内存空间的隔离,实质是不同进程的内存空间进行数据交换。
进程隔离
进程隔离是为保护操作系统中进程互不干扰而设计的,是为了避免进程A写入进程B的情况发生。进程的隔离实现,使用了虚拟地址空间。进程A的虚拟地址和进程B的虚拟地址不同,这样就防止进程A将数据信息写入进程B。这里又涉及到了虚拟地址,与之对应的有物理地址,前面也说了程序大多数时候被存储为可执行的文件,这就是存在物理的寄存器上面的,那么就会有一个地址来表示存储位置,这就是物理地址,在实现进程隔离时自然不能把寄存器切开,并且更重要的是物理内存是碎片化的,程序在上面是零散存储的,为了提高编程效率和提高物理内存利用效率,就用了一套虚拟地址,构成一个虚拟地址空间,虚拟地址和物理地址是存在映射关系的,给每个进程分配不同的且连续的虚拟地址空间,这些虚拟内存地址空间是映射到碎片化的物理内存地址的。总的来说就是通过分配不同的虚拟内存地址空间在逻辑上实现了进程间的隔离
内存空间
前面说的每个进程的内存空间之间如果完全隔离的没有一点缝隙的话,那么跨进程通信自然也不可能实现,之所以能实现,就是系统在每个进程的内存空间分配上做了设计,每个进程分配到的内存空间是分为两部分的,一部分是用户空间,一部分是内核空间。
对于32位系统而言每个进程分配的虚拟内存空间是4G(2^32),其中连续的低位03G是进程的用户空间,高34G是进程的内核空间,64位系统的话用户空间和内核空间可达到128T。
我们的手机实际物理内存可能才1G或者2G,那么未什么虚拟内存可以这么大,虚拟内存不是和物理内存有映射关系么?这涉及到内存分页的机制,有兴趣的同学可以去深入了解一下,在此不赘述。
用户空间就是进程的私人领地,别的进程不能直接访问,而内核空间是可以和其他进程共享的,而且用户空间可以通过系统之手和内核空间交互,这也就为跨进程通信提供了通道。
但要说明的是,应用程序都是运行在用户空间的,而且一个进程的用户空间不能直接通过指针来访问内核空间,因为系统会对用户空间访问的指针地址进行检查,所以一个进程要通过内核空间来和其他进程进行通信,必须借助系统来访问内核空间,这就是Binder机制所主要解决的问题。
Binder机制
铺垫做好了,下面正式讲一下Binder机制。
结合前面说的,跨进程通信的要解决的问题主要有两个
1.一个进程的用户空间和内核空间进行数据交互;
2.不同进程间内核空间数据进行共享。
Binder机制就是Android系统设计来解决这两个问题,来实现跨进程通信的,这是Android系统的一个特色机制。
Binder架构设计
分层实现
完整的Binder机制的架构其实是分了多层来实现的,大致可以分为应用层、Framework层和驱动层。我们从下往上介绍一下各层:
驱动层
驱动层位于Linux内核中,它提供了最底层的数据传递,对象标识,线程管理,调用过程控制等功能。驱动层是整个Binder机制的核心。
Framework层
驱动层虽然提供了很多底层的功能,但他这些功能较为原始,而且因为驱动层Linux内核中,它是用C来实现的,所以还不能直接提供到具体的业务中去使用,这就需要一个中间层进行封装,并且将C实现的功能封装并且通过JNI衔接提供出Java可调用的接口,这就是Framework层。
应用层
就是我们在Android开发时用java写的那部分,例如前言中说的,是Binder机制在具体业务上的实现和使用。
角色划分
整个Binder架构纵向从下到上做了层的划分,同时横向上也对不同功能和职责做了角色的划分,既然是实现通信,那自然就涉及到两方,一个是发起通信请求想要使用某种服务的一方,一个是响应通信请求提供某种服务的一方,这就是个典型的C/S架构,那就有以下的角色划分
Client
服务使用者
Server
服务提供者
ServiceManager
实际情况中,系统并不只有一个服务,也并不止有一个应用程序在运行,那么一个Client怎么找到自己需要的Service呢?那就需要一个管理者,可以管理各种Service并能提供给Client获取指定Service的方法,这就是ServiceManager。
Binder驱动
Client、Server、ServiceManager都是位于Binder整体架构中处于用户空间的程序,更底层提供基础能力的是驱动层里位于内核空间的Binder驱动。
这个Binder驱动本质上和我们平时说的显卡驱动,主板驱动是一个类型的东西,也是个设备驱动。但是不同的是它是个虚拟设备驱动,只有软体没有硬件。对于一个设备而言,单纯的硬件只是一副肉体,想要让它工作,还需要给它思想,让它能调用肉体的能力,那这就是驱动程序,对于系统而言是只认驱动程序不认硬件的,驱动程序里定义了一个硬件的各种信息并且提供出了硬件的功能接口,虚拟设备就是没有硬件只有驱动的一个不存在实体,但是系统会当做设备来对待的设备。
驱动都是运行在内核空间中的,所以能提供一个跨进程的通信渠道,主要就是通过Linux定义的驱动函数iotcl函数来实现数据的交互的。
那么讲到这就有一个大致的脉络了,可以看下这个图
Binder工作原理
这部分我们也是从下到上按照上面说的三层的划分去讲,这样方便深入的理解。
Binder驱动的加载
这里有必要先回顾一下Android系统的启动流程
用户按下电源建,主板通电,系统加电自检,加载引导程序,然后内核映像被加载到内存中,接下来回启动内核程序的kernel_init
android/kernel/msm-4.9/init/main.c
android/kernel/msm-4.9/init/main.c:kernel_init_freeable
3.android/kernel/msm-4.9/init/main.c:do_initcall_level
这里会对一个数组执行一个for循环去执行各种细分的初始化操作,我们所关注的设备驱动的初始化就定义在这个数组里,在android/kernel/msm-4.9/include/linux/init.h有定义
4.android/kernel/msm-4.9/include/linux/init.h
由此可见device_initcall是level为6的优先级,这个函数有个入参fn,这就是每个设备具体的初始化方法,我们关注的是binder的初始化,它是被这样调用的device_initcall(binder_init);在binder.c中则定义了binder_init方法
5./kernel/drivers/staging/android/binder.c
//绑定binder驱动操作函数 static const struct file_operations binder_fops = { .owner = THIS_MODULE, .poll = binder_poll, .unlocked_ioctl = binder_ioctl, .compat_ioctl = binder_ioctl, .mmap = binder_mmap, .open = binder_open, .flush = binder_flush, .release = binder_release, }; //创建misc类型的驱动 static struct miscdevice binder_miscdev = { .minor = MISC_DYNAMIC_MINOR, .name = "binder", .fops = &binder_fops//绑定binder驱动操作函数 }; //binder驱动初始化 static int __init binder_init(void) { int ret; binder_deferred_workqueue = create_singlethread_workqueue("binder"); if (!binder_deferred_workqueue) return -ENOMEM; //创建目录/binder binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL); if (binder_debugfs_dir_entry_root) //创建目录/binder/proc binder_debugfs_dir_entry_proc = debugfs_create_dir("proc", binder_debugfs_dir_entry_root); //注册binder驱动 ret = misc_register(&binder_miscdev); //创建其他文件 if (binder_debugfs_dir_entry_root) { //创建文件/binder/proc/state debugfs_create_file("state", S_IRUGO, binder_debugfs_dir_entry_root, NULL, &binder_state_fops); //创建文件/binder/proc/stats debugfs_create_file("stats", S_IRUGO, binder_debugfs_dir_entry_root, NULL, &binder_stats_fops); //创建文件/binder/proc/transactions debugfs_create_file("transactions", S_IRUGO, binder_debugfs_dir_entry_root, NULL, &binder_transactions_fops); //创建文件/binder/proc/transaction_log debugfs_create_file("transaction_log", S_IRUGO, binder_debugfs_dir_entry_root, &binder_transaction_log, &binder_transaction_log_fops); //创建文件/binder/proc/failed_transaction_log debugfs_create_file("failed_transaction_log", S_IRUGO, binder_debugfs_dir_entry_root, &binder_transaction_log_failed, &binder_transaction_log_fops); } return ret; }复制代码
上面这些都是在Linux内核中,Linux内核初始化完成后,它会在系统文件中寻找 init.rc 文件,并启动init 进程,可见binder驱动的加载是先于第一个进程的。这时已经由内核空间进入到用户空间了
然后init进程在处理很多工作后会启动Zygote进程,其他系统进程都是Zygote进程fork自身产生的。
Binder驱动的工作
了解了Binder驱动的加载后,我们来了解一下它怎么工作,Binder驱动的存在形式是一个设备文件:/dev/binder文件,里面定义了各种可供调用的接口函数,因为linux系统对驱动的实现做了一个统一规范,方便系统调用,所以在binder驱动中也通过函数指针来实现了各种系统函数调用
其中最核心的是三个函数:
binder_open
用于打开binder虚拟设备
binder_mmap
内存映射方法,用于申请供binder驱动操作内核虚拟地址空间
binder_iotcl
进行实际的操作。Client对于Server端的请求,以及Server对于Client请求结果的返回,都是通过ioctl完成的。
binder的实体struct binder_node结构是存在于提供实体的进程中的内核空间里的,但是它可以在其他进程中存在很多个引用,这样两个进程间通道的轮廓就搭建起来了
1.Server在启动之后,调用对/dev/binder设备调用mmap
2.内核中的binder_mmap函数进行对应的处理:申请一块物理内存,里面是binder的实体,然后在用户空间和内核空间同时进行映射
3.Client通过BINDER_WRITE_READ命令发送请求,这个请求将先到驱动中,同时需要将要传递的数据从Client进程的用户空间拷贝到内核空间
4.驱动通过BR_TRANSACTION通知Server有人发出请求,Server进行处理。由于这块内存也在用户空间进行了映射,因此Server进程的代码可以直接访问
还有个ServerManager,它本身和Client、Server一样也是一个单独的进程,在binder驱动中定义了一个*binder_context_mgr_node,这个变量就是ServiceManager,ServiceManager会先于所有server启动,但值得注意的是为了使用方便,ServiceManager是作为Server来实现的,只不过它在binder驱动中有一个特殊位置,可以很方便的访问它。
Framework层的工作
Framework层主要通过C++实现了对驱动底层能力的封装,并且通过JNI,提供了供上层应用层直接调用的java接口。
Framework层的C++部分会打包成libbinder.so。
在libbinder中有这样的几个类,他们按照远程本地也就是客户端和服务端做了区分定义。
类 | 描述 |
---|---|
BpRefBase | RefBase的子类,提供remote()方法获取远程Binder |
IInterface | Binder服务接口的基类,Binder服务通常需要同时提供本地接口和远程接口 |
BpInterface | 远程接口的基类,远程接口是供客户端调用的接口集 |
BnInterface | 本地接口的基类,本地接口是需要服务中真正实现的接口集 |
IBinder | Binder对象的基类,BBinder和BpBinder都是这个类的子类 |
BpBinder | 远程Binder,这个类提供transact方法来发送请求,BpXXX实现中会用到 |
BBinder | 本地Binder,服务实现方的基类,提供了onTransact接口来接收请求 |
ProcessState | 代表了使用Binder的进程 |
IPCThreadState | 代表了使用Binder的线程,这个类中封装了与Binder驱动通信的逻辑 |
Parcel | 在Binder上传递的数据的包装器 |
Framework的c++部分做好了通信功能的封装,那么是怎么提供到应用层使用的呢,就是通过我们经常说的JNI进行的衔接。
有这个几个核心的类
类 | 描述 |
---|---|
IInterface | 供Java层Binder服务接口继承的接口 |
IBinder | Java层的IBinder类,提供了transact方法来调用远程服务 |
Binder | 实现了IBinder接口,封装了JNI的实现。Java层Binder服务的基类 |
BinderProxy | 实现了IBinder接口,封装了JNI的实现。提供transact方法调用远程服务 |
JavaBBinderHolder | 内部存储了JavaBBinder |
JavaBBinder | 将C++端的onTransact调用传递到Java端 |
Parcel | Java层的数据包装器,见C++层的Parcel类分析 |
这里的IInterface,IBinder和C++层的两个类是同名的。这个同名并不是巧合:它们不仅仅同名,它们所起的作用,以及其中包含的接口都是几乎一样的,区别仅仅在于一个是C++层,一个是Java层而已。
除了IInterface,IBinder之外,这里Binder与BinderProxy类也是与C++的类对应的,下面列出了Java层和C++层类的对应关系
C++ | Java层 |
---|---|
IInterface | IInterface |
IBinder | IBinder |
BBinder | Binder |
BpProxy | BinderProxy |
Parcel | Parcel |
为什么选择Binder
Binder相较于传统IPC来说更适合于Android系统,具体原因的包括如下三点:
1. 性能
性能上更有优势:管道,消息队列,Socket的通讯都需要两次数据拷贝,而Binder只需要一次。要知道,对于系统底层的IPC形式,少一次数据拷贝,对整体性能的影响是非常之大的
Socket:通用接口,传输效率低,开销大,主要用于跨网络的进程间通信
消息队列和管道: 采取储存-转发方式,数据先从发送方缓存区拷贝到内核缓冲区,然后从内核缓冲区拷贝到接收方缓存区,需要经历2次拷贝过程
共享内存:无需拷贝,但是控制复杂,使用很困难
Binder:只需要一次数据拷贝,性能上比消息队列和管道要好很多
Binder Driver 如何在内核空间中做到一次拷贝的?当Client向Server发送数据时,Client会先从自己的用户空间把通信数据拷贝到内核空间,因为Server和内核共享数据,所以不再需要重新拷贝数据,而是直接通过内存地址的偏移量直接获取到数据地址。总体来说只拷贝了一次数据。
Server和内核空间之所以能够共享一块空间数据主要是通过binder_mmap来实现的。它的主要功能是在内核的虚拟地址空间申请一块和用户虚拟内存相同大小的内存,然后再申请一个page大小的内存,将它映射到内核虚拟地址空间和用户虚拟内存空间,从而实现了用户空间缓冲和内核空间缓冲同步的功能。
传统IPC机制
Binder 机制
2. 安全
传统IPC机制无法获取对方可靠的进程ID。传统IPC只能由用户在数据包里填入UID和PID,但这样不可靠,无法鉴别对方身份,容易被恶意程序利用。
Android 为每一个安装好的APP分配了自己的UID,Binder 基于此会根据请求端的UID来鉴别调用者的身份,保障了安全性
3. 稳定
Binder 基于 C/S架构,职责明确,架构清晰
Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。
service_manager本质上其实是维护了一个handle链表,便于客户端通过binder服务name进行查询,随后可以获取到binder代理handle,最终以封装成binder代理对象
Binder驱动是Binder服务端和Binder客户端之间连接的一个桥梁,当一个服务端Binder被创建出来的时候,系统同时会在Binder驱动中创建另外一个Binder对象,当客户端想要访问远程的Binder服务端的时候, 都是通过这个Binder对象来完成的
作者:Cloud_lys
链接:https://juejin.cn/post/7039020937764667399