1. Binder是干什么的?
简单来说binder就是用于进程间通讯的。但从不同角度对binder可以有不同的理解。引用《Android开发艺术探索》中的解释:
- Binder是Android中一个实现了IBinder接口的类
- 是一种跨进程通讯的方式
- 是一种虚拟的物理设备,驱动为/dev/binder
- 在Android中是ServiceManager连接各种Manager极其Service的桥梁
- 是Android客户端与服务端的媒介
估计在看完这些概念时,感觉是一脸懵。不着急先继续往下看。
在深入了解binder前,我们有必要了解一些相关概念来循序渐进:
1.1 App为什么要多进程?
通常我们在日常开发中特定场景下会使用多进程,比如接入webview、视频音乐播放、大图浏览、推送等功能时,会开启多进程。开启多进程的方法很简单,在对应的组件manifest文件节点下面指定process属性即可。
为什么要开启多进程?
- 系统分配的dalvik虚拟机内存空间有限,当使用过程中内存超出时会oom。一个进程代表一个虚拟机,代表一块内存,开启多进程相当于向系统多申请了内存空间。
- 使用多进程可以实现风险隔离。当一个进程挂掉了,不会影响另外一个进程。
ps:
查看系统给App分配的内存空间使用命令:
getprop dalvik.vm.heapsize
1.2 了解到多进程的作用后,那么Linux进程间通讯的方式有哪些?
管道、信号量、socket、共享内存、binder等
那Android中为什么要使用binder来进行通讯?
2. Biner通讯特点
binder与共享内存、socket对比
从表中可以看出
binder相比共享内存,在传递数据时会多一次数据拷贝,而比传统的通讯方式socket少一次数据拷贝。
那又什么是数据拷贝?
说到这里就不得不先说下Android的内存划分了,在我们的Android系统中,内存被操作系统划分为两块:内核空间和用户空间:内核空间是共享的,是内核代码运行的地方,可以调用系统的一切资源。用户空间是每个进程独有的,是用户程序代码运行的地方,从安全角度考虑,它不能直接调用系统的相关资源,只能通过系统接口(system call)来向内核空间发出指令作出相关操作,比如访问网络,读写文件等,都是通过内核空间中转了一次的。两者相互隔离,即当用户的程序崩溃了,内核也不会受影响。
二者关系如下图
例如当应用需要像系统中写入一个文件时,它会分成以下几步
- 调用wirte,告诉内核需要写入数据的开始地址与长度
- 内核将数据拷贝到内核缓存
- 由操作系统调用,将数据拷贝到磁盘,完成写入
BInder方式为什么高效呢?下面来对比下传统IPC方式与Binder方式进程1需要向进程2写入数据时的不同
传统IPC方式
Binder方式
用binder来进行进程间通讯时,在从内核空间拷贝数据到进程2这一步,会通过mmap()内存映射关系,直接与数据接收方共享同一块物理内存,从而减少一次数据拷贝。所以使用binder比传统IPC方式更高效。那么有人会问,那写入的时候也使用mmap不是更高效吗?直接不用拷贝了。其实这就是共享内存。就是上面表中的0次拷贝。但共享内存由于安全关系,同步控制复杂,反而没有binder高效。
那什么是mmap呢?
Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)
用户空间与内核空间上内存概念都是逻辑地址,而对应到真正的物理内存地址,背后存在着地址一对一的映射。通过mmap实现这样的映射关系后,就可以采用指针的方式读写操作某一段内存,而系统会自动回写到对应的文件磁盘上。
前面只是大概的描述了下两个进程间的通讯过程。我们知道Android 上层都是用 Java 写的,但是 Binder 驱动是 C 实现,他们之间数据具体是如何传递的呢?
3. Binder执行流程
下面再给出一张binder的框架图,和涉及到的类图
binder框架图
binder涉及到的类图
简单来说就是Fromwork(java)层与native(c)层通过JNI搭建一个桥梁,注册各种方法,然后到native层又通过系统提供的接口(system call)与内核层通讯,来进行一些操作,比如注册服务与获取服务等。
下面再从源码的角度来看看,这一步一步是怎么实现的
4. Binder的JNI方法注册
4.1 Android系统开机启动启动流程
首先来回顾下Android系统的开机启动流程
① 电源启动
② 引导芯片代码加载引导程序BootLoader到RAM中去执行。BootLoader是Android操作系统开始运行前的一个小程序,它负责把操作OS系统拉起来并运行。
③ Linux内核系统启动后开始系统设置,并在系统文件中找到init.rc文件,启动init进程
④ init进程启动后,会做一些初始化及启动属性服务的工作,并且启动zygote进程
⑤ zygote进程启动后会创建java虚拟机并为虚拟机注册jni方法,创建服务端socket,启动SystemServer进程
⑥ SystemServer会启动Binder线程池及SystemServiceManager,并启动各种Service(AMS、WMS等)
⑦ AMS会启动Launcher,Launcher启动后会把已安装的应用图标显示到手机桌面上
4.2 zygote进程启动注册jni
在上述的第四步中,init进程通过解析init.zygote.rc文件,创建zygote进程。zygote进程所对应的可执行程序是app_process,所对应的源文件是app_main.cpp。该文件位于Android系统源码的/system/core/rootdir/init.zygote32.rc目录下
4.3 源码跟踪
framworks/base/cmds/app_process/app_main.cpp
//执行main方法
//将zygote标志位置为true
//运行AndroidRuntime.cpp的start方法
framworks/base/core/jni/AndroidRuntime.cpp
//执行start方法
//执行startReg()
//注册jni方法
//循环注册jni方法
//里面包含binder
framworks/base/core/jni/android_util_Binder.cpp
//具体执行注册
以第一个为例,下面两个方法注册都差不多就不再一 一分析。
//这里可以理解为相当于做个一个反射,将java层的方法一一对应起来,建立关联。
通过findClassOrDie方法,查找文件kBinderPathName="android/os/Binder"返回对应的class对象;通过gBinderOffsets结构体,保存Java层Binder类的信息,为JNI层访问Java层提供通道;通过RegisterMethodsOrDie为gBinderMethods数组完成映射关系,从而为Java层访问JNI层提供通道
经过上面的源码流程,把java层到native层JNI注册的流程摸清楚了,下面再介绍native层到内核层systemCall是怎么进行的
5. Binder的驱动启动
5.1 示意图
在Linux中,一切皆文件。对binder的驱动初始化也是从binder.c 开始的。主要涉及到binder_init()、binder_open()、binder_mmap()、binder_ioctl()这几个函数。
5.2 源码追踪
#kernel/drivers/staging/android/binder.c
注意该文件不在Android源码里面,位于Linux源码里面。与Android源码framwork文件夹里那个不是一回事。
在binder_init函数中调用了init_binder_device(const char *name)函数,在这里主要干了三件事
- 为binder设备开辟内存空间
- 初始化驱动设备
- 将hlist节点添加到binder_devices为表头的设备链表
其中在第二步初始化时拿到了binder_fops结构体指针,在后续过程中,会调用到binder_fops.mmop、binder_fops.open、binder_ioctl
先来看binder_open函数中
在这里主要干了四件事:
- 分配内存,创建binder_proc
- 初始化binder_proc(可以理解为把java层的binder相关信息保存到proc)
- 将proc托管到file中
- 把proc添加到链表中procs保管起来
再来看binder_mmap 函数
android6.0已经把binder_buffer有关的操作和binder.c分开了,实现在binder_alloc.c文件里面,这里不再详述。
感兴趣可查看
https://github.com/torvalds/linux/blob/master/drivers/android/binder_alloc.c
在这里主要干了三件事
- 通过用户空间的虚拟内存大小分配一块内核的虚拟内存,二者大小相等,且不超过4M
- 分配一块物理内存--4KB (先分配1页one page)等需要用的时候再扩大,免费内存浪费
- 计算到的用户空间和内核空间地址偏移量,并把这块物理内存分别映射到用户空间的虚拟内存和内核的虚拟内存
注意mmap所映射的内容大小最大为4M,是由sz_4M属性决定的。
紧接着来到binder_ioctl函数中
它会根据传递过来的命令做出相应的操作,比如读写操作
找了这么就终于来到我们前面提到的copy_from_user()和copy_to_user()中了,这两个函数分别把用户空间数据ubuf拷贝到内核空间数据bwr及反之。
注意这里拷贝的是数据头,不是有效数据
5.4 常见面试题
顺便提一句,在面试的时候经常会被问到intent最大一次能携带多少数据?答案:1M-8k(8k是两个pagesize,一个pagesize是申请物理内存的最小单元)
原因是如果一个进程使用ProcessState这个类来初始化Binder服务,这个进程的Binder内核内存上限就是BINDER_VM_SIZE,也就是1MB-8KB。
frameworks/native/libs/binder/ProcessState.cpp
初始化在zygote进程初始化binder服务时调用的