我们知道Android系统启动之后,要想启动一个应用程序则需要保证该应用程序的进程先被启动,如果该进程不存在就会请求 Zygote 进程启动需要的应用程序进程。这样在 Zygote 的 Java 框架层(Framework源码层)中会创建一个 Server 端的 Socket,这个 Socket 用来等待 AMS 请求 Zygote 来创建新的应用程序进程(Zygote 进程通过 fork 自身创建应用程序进程),这样应用程序进程就会获得 Zygote 进程在启动时创建的虚拟机实例。与此同时,也创建出了 Binder 线程池 和 消息循环,这样运行在应用进程中的应用程序就可以很方便的使用 Binder 进行进程间通信以及处理消息了。
简介
既然决定吃透源码,那就必须从源头捋起,所以这篇文章会综合《深入理解Android内核设计思想》、《Android开发艺术探索》以及《Android进阶解密》三书,加上在享学课堂学习有关 Binder 进程课程,会在本文中对Binder设计细节做一个全面的阐述。首先通过介绍 Binder 通信模型和 Binder 通信协议了解 Binder 的设计需求;然后分别阐述 Binder 在系统不同架构分层间的呈现方式及其作用,最后分析 Binder 在数据接收端的设计考虑,包括:线程池管理、内存映射、等待队列等。为读者尽最大可能展现 Android Binder 原本的样貌。这样学就够了!
内存划分

传统 IPC 传输数据

Binder 传输数据

Binder
Binder 是 Android 系统进程间通信(IPC)方式之一,这里为了突出 Android Binder IPC 通信机制的优越性,先分析对比一波 共享内存、Socket 和本文介绍的 Binder 的数据拷贝。
三种 IPC 方式的数据的拷贝次数

共享内存
共享内存可以类比 Java 的内存, 堆就是线程共享区域,而此处的堆就是 Linux 里的内存共享区域,虽然无需拷贝,但控制复杂,难以复用,基于此方案的通信,试想一下,如果有一个恶意的程序或者某个app运行崩溃,就会导致关联的应用甚至系统跟着奔溃,这是致命的。
Socket/管道/消息队列
基于 C/S 架构的通信,目前 linux 支持 的 IPC 包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及 socket 中只有 socket 支持 Client-Server 的通信方式。
socket 作为一款通用接口,其传输效率低效,开销巨大,主要用在跨网络的进程间通信和本机上进程间的低速通信。如果采用socket在底层架设一套协议,这无疑加重系统设计的复杂性,而且也难以保证通信效率。
消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,这里至少有两次拷贝过程。
Binder(基于传输性、安全性考虑均是最佳)
基于 C/S 通信模式,传输过程只需一次拷贝,为发送方添加 UID/PID 身份,既支持实名 Binder,也支持匿名 Binder,安全性高。
面向对象的 Binder IPC
Binder 使用 Client-Server 通信方式:一个进程作为 Server 提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务;多个进程作为 Client 向 Server 发起服务请求,获得所需要的服务。
要想实现 Client-Server 通信据必须实现以下两点:
Server 必须有确定的访问接入点或者说地址来接受Client的请求, 并且 Client 可以通过某种途径获知 Server 的地址;
制定 Command-Reply 协议来传输数据
这就好比,网络通信中,Server 的访问接入点就是 Server 主机的IP地址+端口号,传输协议为 TCP 协议;
对 Binder 而言,Binder 可以看成 Server 提供的实现某个特定服务的访问接入点,Client 通过这个 “地址” 向 Server 发送请求来使用该服务;
对 Client 而言,Binder 可以看成是通向 Server 的管道入口,要想和某个 Server 通信首先必须建立这个管道并获得管道入口。
与其他 IPC 方式不同点在于,Binder 使用了面向对象思想来描述作为访问接入点的 Binder 和在 Client 中的入口:
Binder 是一个实体位于 Server 中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。
遍布于 Client 中的入口可以看成指向这个 Binder 对象的“指针”,一旦获得了这个“指针”就可以调用该对象的方法访问 Server。
Binder 模型
Binder 框架定义了四个角色:Server、Client、ServiceManager、Binder驱动,这其中前三个位于 用户空间,驱动位于 内核空间。这四个角色的关系和互联网类似:Server 是服务器,Client 是客户终 端,SMgr 是域名服务器(DNS),驱动是路由器。
Linux进程间通信机制
管道、信号量、Socket、共享内存
共享内存:数据发送方、内核、数据接收方 三者共享一块物理内存空间
Binder:数据发送方通过 copy_fromm_user() 拷贝一次,将数据拷贝到内核,而内核和数据接收方有一块内存共享区域,所以这两者无需拷贝,即可使用。
Binder的驱动注册
Linux 一切皆文件
binder_init
分配内存
初始化设备
放入设备链表 binder_devices
binder_open
初始化 proc 创建 binder_proc 进程对象
创建同步锁 binder_lock
串联进程链表 binder_procs -> binder_proc -> binder_proc-> // hlist_head -> hlist_node -> hlist_node ->
内核函数filp -> proc 赋值 private_data(后续binder访问全是它) // flip()函数反置bitset中所有的位,即将1设为0,0设为1。
解除同步锁 binder_unlock
binder_mmap
struct vm_struct*area —— 内核的虚拟内存
struct vm_area_struct*vma —— 进程的虚拟内存(4M 驱动设定;1M - 8k 应用层设定 ===》 binder传输超过1M就会崩溃)
area = get_vm_area(vma->vma_end - vma->vma_start, VMREMAP) 分配内存重新建立映射关联
proc buffer指针 指向 area 的内核虚拟指针
计算用户空间偏移值 user_buffer_offset:用户空间虚拟内存地址(proc->buffer) = 虚拟内存地址(vma->vm_start) + 偏移值(proc->user_buffer_offset)
Binder的JNI注册
剑指大厂
Binder有什么优势? – 《ByteDance》
优点:
内存开辟
风险隔离
Binder是如何做到一次拷贝? – 《Tencent》
内存划分:内存被操作系统分成两块:用户空间和内核空间,用户空间时用户程序代码运行的地方,内核空间是系统内核运行的地方,可以共享。为了安全,这两者是隔离的,这样即使用户的应用程序崩溃也不会影响到内核。
32位系统 Vs 64位系统
32位系统,即2^32,即总共可访问地址为4G。内核1G,用户空间3G。
64位系统,低位:0-47位才是有效的可变地址(寻址空间256T),高位:48-63位全补0或1。
一般高位全补0对应的地址空间是用户空间。高位全补1对应的是内核空间。
mmap的原理讲解 – 《Tencent》
MMAP:Linux通过将一个虚拟 内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)
对文件进行mmap,会在进程的虚拟内存分配地址空间,建立映射关系。实现这样的映射关系后,就可以对这一段内存采用指针的方式进行读写操作,而系统会自动回写到对应的文件磁盘上。






