Android源码分析 - Binder概述

2,235 阅读6分钟

开篇

本篇无源码分析,只对Binder做通信过程和基础架构的介绍

BinderAndroid中最重要的一种进程间通信机制,基于开源的OpenBinder

George Hoffman当时任Be公司的工程师,他启动了一个名为OpenBinder的项目,在Be公司被ParmSource公司收购后,OpenBinderDinnie Hackborn继续开发,后来成为管理ParmOS6 Cobalt OS的进程的基础。在Hackborn加入谷歌后,他在OpenBinder的基础上开发出了Android Binder(以下简称Binder),用来完成Android的进程通信。

为什么需要学习Binder

作为一名Android开发,我们每天都在和Binder打交道,虽然可能有的时候不会注意到,譬如:

  • startActivity的时候,会获取AMS服务,调用AMS服务的startActivity方法
  • startActivity传递的对象为什么需要序列化
  • bindService为什么回调的是一个Ibinder对象
  • 多进程应用,各个进程之间如何通信
  • AIDL的使用
  • ...

它们都和Binder有着莫切关系,当碰到上面的场景,或者一些疑难问题的时候,理解Binder机制是非常有必要的

为什么Android选择Binder

这就要从进程间通信开始说起了,我们先看看比较常见的几种进程间通信方式

常见进程间通信

共享内存

共享内存是进程间通信中最简单的方式之一,共享内存允许两个或更多进程访问同一块内存,当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改,它的原理如下图所示:

共享内存

因为共享内存是访问同一块内存,所以数据不需要进行任何复制,是IPC几种方式中最快,性能最好的方式。但相对应的,共享内存未提供同步机制,需要我们手动控制内存间的互斥操作,较容易发生问题。同时共享内存由于能任意的访问和修改内存中的数据,如果有恶意程序去针对某个程序设计代码,很可能导致隐私泄漏或者程序崩溃,所以安全性较差。

管道

管道分为命名管道和无名管道,它是以一种特殊的文件作为中间介质,我们称为管道文件,它具有固定的读端和写端,写进程通过写段向管道文件里写入数据,读进程通过读段从读进程中读出数据,构成一条数据传递的流水线,它的原理如下图所示:

管道

管道一次通信需要经历2次数据复制(进程A -> 管道文件,管道文件 -> 进程B)。管道的读写分阻塞和非阻塞,管道创建会分配一个缓冲区,而这个缓冲区是有限的,如果传输的数据大小超过缓冲区上限,或者在阻塞模式下没有安排好数据的读写,会出现阻塞的情况。管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式。

消息队列

消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。消息队列允许多个进程同时读写消息,发送方与接收方要约定好,消息体的数据类型与大小。消息队列克服了信号承载信息量少、管道只能承载无格式字节流等缺点,消息队列一次通信同样需要经历2次数据复制(进程A -> 消息队列,消息队列 -> 进程B),它的原理如下图所示:

消息队列

Socket

Socket原本是为了网络设计的,但也可以通过本地回环地址 (127.0.0.1) 进行进程间通信,后来在Socket的框架上更是发展出一种IPC机制,名叫UNIX Domain SocketSocket是一种典型的C/S架构,一个Socket会拥有两个缓冲区,一读一写,由于发送/接收消息需要将一个Socket缓冲区中的内容拷贝至另一个Socket缓冲区,所以Socket一次通信也是需要经历2次数据复制,它的原理如下图所示:

Socket

Binder

了解了常见进程间通信的方式,我们再来看一下Binder的原理

Binder是基于内存映射mmap设计实现的,我们需要先了解一下mmap的概念

mmap

mmap是一种内存映射的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

Linux内核不会主动将mmap修改后的内容同步到磁盘文件中,有4个时机会触发mmap映射同步到磁盘:

  • 调用 msync 函数主动进行数据同步(主动)
  • 调用 munmap 函数对文件进行解除映射关系时(主动)
  • 进程退出时(被动)
  • 系统关机时(被动)

通过这种方式,直接操作映射的这一部分内存,可以避免一些数据复制,从而获得更好的性能

原理

一次Binder IPC通信的过程分为以下几个步骤:

  1. 首先,Binder驱动在内核空间中开辟出一个数据接收缓冲区
  2. 接着,在内核空间中开辟出一个内核缓冲区
  3. 内核缓冲区数据接收缓冲区建立映射关系
  4. 数据接收缓冲区接收进程的用户空间地址建立映射关系
  5. 发送方进程通过copy_from_user将数据从用户空间复制到内核缓冲区
  6. 由于内核缓冲区数据接收缓冲区有映射关系,同时数据接收缓冲区接收进程的用户空间地址有映射关系,所以在接收进程中可以直接获取到这段数据

这样便完成了一次Binder IPC通信,它的原理如下图所示:

Binder

可以看到,通过mmapBinder通信时,只需要经历一次数据复制,性能要优于管道/消息队列/socket等方式,在安全性,易用性方面又优于共享内存。鉴于上述原因,Android选择了这种折中的IPC方式,来满足系统对稳定性、传输性能和安全性方面的要求

Binder架构

Binder也是一种C/S架构,分为BpBinder(客户端)和BBinder(服务端),他们都派生自IBinder。其中BpBinder中的p表示proxy,即代理。BpBinder通过transact来发送事务请求,BBinder通过onTransact来接收相应的事务

Ibinder

Binder一次通信的时序图如下:

Binder通信

Binder采用分层架构设计

Binder分层架构

总结

至此,我们大概了解了一下Android选用Binder的原因,以及Binder的基本结构和通信过程,为之后深入源码层分析Binder做了准备

参考文献