Fork me on GitHub

Android Framework 专项 - IPC Binder 机制(一)

问题

1
2
The formulation of the problem is often more essential than its solution, which may be merely a matter of mathematical or experimental skill.
― Albert Einstein

Q: Binder 是什么?binder 是如何出现的

Q: Binder 通信模型是什么?

Q: Aidl 通信机制是什么?

Q: Bindservice 流程分析

Q: Binder 通信是如何走到 Native 层的

Q: ServiceManager 是什么?

Q: Binder 通信之 Client 端调度流程解析

从 Android 系统设计说起

Android 的系统的三个层次

application 应用层 - Framework 层- native 层

Android 中的应用层和系统服务层不在同一个进程,系统服务在单独的进程中。
Android 中的不同应用属于不同的进程,每一个应用是 zygote fork 出来的

为了安全,Android 的应用层与系统层之间是隔离的

Android 系统 IPC 原理

pFOKnAI.png

每个 Android 的进程,只能运行在自己进程所拥有的虚拟地址空间。
对应一个 4GB 的虚拟地址空间,其中 3GB 是用户空间,1GB 是内核空间,当然内核空间的大小是可以通过参数配置调整的。
对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。
Client 进程向 Server 进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client 端与 Server 端进程往往采用 ioctl 等方法跟内核空间的驱动进行交互。

用户空间和内核空间知识补充

怎么理解不同进程之间用户空间不能共享,而内核空间却是可共享的?

  1. 用户空间的共享: 在标准情况下,不同进程的用户空间是彼此隔离的,不能直接共享内存。每个进程有自己独立的虚拟地址空间,不同进程的相同虚拟地址并不会映射到相同的物理内存。这意味着一个进程不能直接访问另一个进程的用户空间。
  2. 内核空间的共享: 内核空间是操作系统内核的一部分,对所有进程来说都是共享的。这是因为内核提供了操作系统的核心功能,比如进程调度、内存管理、文件系统等。不同进程需要与内核进行交互来请求服务和操作资源。因此,所有进程都共享一个操作系统内核
  3. 内核空间中的数据隔离: 尽管内核空间对所有进程来说是共享的,但内核本身会实施严格的隔离措施,以防止一个进程的操作影响其他进程。内核使用许多机制来确保不同进程的请求和数据是独立的,从而保障系统的稳定性和安全性。
  4. 内核空间中的共享数据结构: 在某些情况下,内核中可能存在一些数据结构是为多个进程共享的,例如文件描述符表、进程控制块等。这种共享是通过内核维护的数据结构来实现的,而不是直接让不同进程的内核空间映射到相同的物理内存。

总结起来,不同进程的用户空间通常是不能直接共享的,每个进程都有自己的独立虚拟地址空间。但所有进程共享同一个内核空间,内核提供了操作系统的核心功能。内核空间中的数据隔离和共享是通过内核内部的机制来实现的

页表知识补充

页表是操作系统中用于管理虚拟内存与物理内存之间映射关系的数据结构。在计算机中,虚拟内存是指操作系统为每个进程提供的独立的内存地址空间,而物理内存则是实际的硬件内存。由于物理内存有限,虚拟内存允许多个进程同时运行,而不会受到物理内存大小的限制。

页表的主要功能是将虚拟地址转换为物理地址。当进程访问虚拟地址时,操作系统通过页表查找,找到对应的物理地址,从而实际完成内存的读写操作。

具体来说,页表包含了虚拟地址和物理地址之间的映射关系。每个进程都有自己的页表,其中的页表项记录了虚拟地址的页号(page number)与物理地址的页框号(page frame number)之间的对应关系。页框是物理内存的一个固定大小的块,而页是虚拟内存的一个固定大小的块。通过查找页表,操作系统可以找到虚拟地址对应的物理地址,从而实现内存访问。

总结

  1. 用户空间与内核空间: IPC 机制总的操作又可以分为用户空间进行的操作和内核空间进行的操作
  2. 内核空间中的数据结构不同: 不同 IPC 机制内核中的数据结构不同
  3. 复制与映射: IPC 机制利用进程间可共享的内核空间来完成底层通信工作,其中我们可以简单的分为复制和虚拟内存映射两种方式

我们从这几个角度来快速的区分 IPC 机制

IPC-共享内存

用户空间与内核空间行为

用户空间:

  1. 创建共享内存: 进程通过系统调用在用户空间申请一块共享内存,得到一个唯一的标识符。
  2. 映射共享内存: 进程使用系统调用将共享内存映射到自己的虚拟地址空间,从而可以直接访问这块内存区域。这个映射实际上是指向了内核空间中设置的共享页表项。
  3. 读写数据: 进程可以在映射的共享内存区域进行读写操作,与普通内存一样,无需复杂的通信操作。

内核空间:

  1. 设置共享内存页表: 内核负责在共享内存的物理地址和虚拟地址之间建立映射关系,设置页表项,确保多个进程能够访问相同的物理内存。
  2. 同步和权限控制: 内核维护共享内存的元信息,包括大小、权限等。在多个进程访问时,内核会处理访问的同步和权限控制问题。
  3. 不同进程的映射: 当不同进程请求映射共享内存时,内核将相同的物理内存映射到不同的进程虚拟地址空间,使它们共享同一块内存。它们共享相同的页表项,由 TLB(Translation Lookaside Buffer,页表查找缓冲器)实现。

内核空间中的关键数据结构

共享的页表项

复制与映射

双进程分享的是共享页表项,物理地址内数据只有一份,复制次数 0

IPC-管道

不同管道类型

无名管道(Unnamed Pipe)
  • 无名管道是一种单向通信机制,只能用于父子进程或者具有共同祖先的进程之间通信
  • 创建无名管道使用的是 pipe() 系统调用。该调用返回一对文件描述符,一个用于读取,一个用于写入。
  • 无名管道的数据传输是单向的,数据写入一个描述符后可以被另一个描述符读取。
  • 在很多系统上,无名管道的缓冲区大小是固定的,通常为一页大小(例如4KB)。这意味着管道的数据容量有限,无法容纳大规模的数据。
有名管道(Named Pipe,FIFO):
  • 有名管道是一种基于文件系统的命名管道,可以用于任意进程之间通信,不受关系限制

  • 使用 mkfifo 命令或 mkfifo() 系统调用创建有名管道。它在文件系统中创建一个特殊的文件节点,进程可以像读写普通文件一样读写这个节点来进行通信。

  • 有名管道的数据传输是单向的,需要同时创建一个读取端和一个写入端。多个进程可以连接到同一个有名管道进行通信。

  • 有名管道(Named Pipe,FIFO)的缓冲区大小是由系统内核设置的,并且通常与页大小(Page Size)有关。在大多数Linux系统中,页大小通常为4KB,因此默认情况下,有名管道的缓冲区大小也会是4KB。

    某些系统可能会允许你通过特定的系统参数进行配置。具体的设置方法可能会因操作系统版本和发行版而异。

管道的指针操作,写满与阻塞

写入从头指针开始,读取从尾指针开始。写入之后,头指针挪动,读取之后尾指针挪动。
如果是头指针赶上尾指针,那么管道被写满,写就会被阻塞。如果是尾指针赶上头指针,那么管道为空,read阻塞。

用户空间与内核空间行为

用户空间:

  1. 创建管道: 进程通过系统调用在用户空间创建一个管道,得到两个文件描述符,一个用于读取,一个用于写入。
  2. 写入数据: 进程使用写入文件描述符将数据写入管道。
  3. 读取数据: 进程使用读取文件描述符从管道中读取数据。

内核空间:

  1. 管道管理: 内核维护管道的数据结构,包括缓冲区和读写指针。
  2. 数据传递: 内核通过管道将写入的数据从一个进程的写入文件描述符复制到另一个进程的读取文件描述符。
  3. 进程同步: 内核确保在多个进程访问管道时的同步,避免数据错乱。

内核空间中的关键数据结构

单向管道

复制与映射

在基本的管道(Pipe)IPC 机制中,数据实际上只涉及一次复制操作,因为管道是一个字节流传输机制,数据在管道中以字节为单位连续传输。

简单的说 Linux 是文件系统,管道也是文件,这个文件由两个指针进行操作,一头写入一头读取,当一个进程将数据写入管道时,数据直接写入这个文件内,即管道的缓冲区中,当另一个进程从管道中读取数据时,这些数据会被从管道的缓冲区读取到接收方进程的内存中,而管道不持有这些数据,数据的角度来看,实际上只有一次数据复制。

IPC-消息队列

用户空间与内核空间行为

用户空间:

  1. 创建消息队列: 进程通过系统调用在用户空间创建一个消息队列,得到一个唯一的标识符。
  2. 发送消息: 进程使用系统调用将消息发送到消息队列,包括消息类型和数据(需要通信双方约定好)。
  3. 接收消息: 进程使用系统调用从消息队列中接收消息,根据消息类型读取相应的数据。

内核空间:

  1. 消息队列管理: 内核维护消息队列的元信息,包括消息队列的状态、大小等。
  2. 消息传递: 内核将进程发送的消息复制到消息队列中,或从消息队列中复制消息给接收的进程。(跨进程消息队列两次复制
  3. 进程同步: 内核确保在多个进程访问消息队列时的同步,以避免竞态条件。

内核空间中的关键数据结构

消息队列

复制与映射

两次复制

  1. 写入数据: 当一个进程将消息写入消息队列时,消息数据会从发送方进程的内存复制到消息队列的内核缓冲区中。这是第一次复制操作。
  2. 读取数据: 在接收方进程中,数据需要从内核缓冲区复制到接收方进程的内核空间中。这是第二次复制操作。

IPC-Socket

用户空间与内核空间行为

用户空间:

  1. 创建 Socket: 进程通过系统调用在用户空间创建一个 Socket,得到一个文件描述符,用于读写数据。
  2. 发送数据: 进程使用文件描述符发送数据到指定的 Socket。
  3. 接收数据: 进程使用文件描述符从 Socket 中接收数据。

内核空间:

  1. Socket 管理: 内核维护 Socket 的数据结构,包括缓冲区、连接状态等。
  2. 数据传递: 内核通过 Socket 将进程发送的数据从一个进程的发送缓冲区复制到另一个进程的接收缓冲区。
  3. 连接管理: 内核负责管理连接的建立、维护和断开,以及处理各种网络协议。

内核空间中的关键数据结构

内核空间中的关键数据结构

  1. 发送缓冲区: 发送方进程使用 Socket 发送数据时,数据首先被复制到发送缓冲区(Send Buffer)中。这个缓冲区在内核空间中,用于临时存储待发送的数据。发送缓冲区的大小可以由操作系统参数或套接字选项进行配置。
  2. 接收缓冲区: 接收方进程使用 Socket 接收数据时,数据会被存储在接收缓冲区(Receive Buffer)中。这个缓冲区同样位于内核空间,用于临时存储接收到的数据。接收缓冲区的大小也可以通过操作系统参数或套接字选项进行配置

复制与映射

linux 2.4 内核以下

两次用户、内核态的切换,三次数据拷贝

linux 2.4 内核及其以上

两次用户、内核态的切换,两次数据拷贝

内核态零拷贝原理

数据不再被复制到 socket 关联的缓冲区中了,仅仅是将一个描述符(包含了数据的位置和长度等信息)追加到 socket 关联的缓冲区中。DMA 直接将内核中的缓冲区中的数据传输给协议引擎,消除了那一次需要 cpu 周期的数据复制。

IPC-Binder

用户空间与内核空间行为

从用户空间和内核空间的角度来看,IPC(Inter-Process Communication,进程间通信)Binder 的工作可以简洁地描述如下:

用户空间:

  1. 创建 Binder 对象: 进程通过系统调用创建 Binder 对象,通常是 Binder 类的子类实例。这个对象用于表示一个通信通道,可以用来发送和接收数据。
  2. 发送数据: 进程通过 Binder 对象将数据(通常是 Parcel 对象)发送到另一个进程。这个过程会将数据传递给内核空间的 Binder 驱动
  3. 接收数据: 进程通过 Binder 对象接收另一个进程发送的数据。接收的数据也是以 Parcel 对象的形式返回给用户空间。

内核空间:

  1. Binder 驱动: Binder 驱动位于内核空间,负责管理 Binder 通信。当进程发送数据时,数据会传递给 Binder 驱动。
  2. 数据传递: Binder 驱动将进程发送的数据从发送方进程的用户空间复制到接收方进程的用户空间,这一过程中涉及数据的复制和映射
  3. 线程池管理: Binder 驱动还管理了一个线程池,用于处理进程间的数据传递请求。这确保了数据的传递不会阻塞主线程,提高了性能。
  4. 权限和安全性: Binder 驱动实施权限和安全性控制,确保只有经过授权的进程可以进行 Binder 通信。

内核空间中的关键数据结构

指向物理内存的内核空间虚拟内存

复制与映射

复制:发送方进程将数据复制到内核空间虚拟内存中。
映射:将这个内核中的虚拟内存映射给接收方进程。

为什么 Android 要采用 Binder 作为 IPC 机制?

在下不才,这个问题,交给大佬回答

作者:Gityuan
链接:https://www.zhihu.com/question/39440766/answer/89210950
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在开始回答 前,先简单概括性地说说Linux现有的所有进程间IPC方式:

  1. 管道: 在创建时分配一个page大小的内存,缓存区大小比较有限。
  2. 消息队列: 信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信。
  3. 共享内存: 无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快,但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决。
  4. 套接字: 作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信。
  5. 信号量: 常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等。

Android的内核也是基于Linux内核,为何不直接采用Linux现有的进程IPC方案呢,难道Linux社区那么多优秀人员都没有考虑到有Binder这样一个更优秀的方案,是google太过于牛B吗?事实是真相并非如此,请细细往下看,您就明白了。


接下来正面回答这个问题,从5个角度来展开对Binder的分析:

(1)从性能的角度 数据拷贝次数:Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。

(2)从稳定性的角度
Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。

仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是:

(3)从安全的角度
传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐射数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。

Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。

针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。

Android中权限控制策略有SELinux等多方面手段,下面列举从Binder的一个角度的权限控制:
Android源码的Binder权限是如何控制? -Gityuan的回答

传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。

说到这,可能有人要反驳,Android就算用了Binder架构,而现如今Android手机的各种流氓软件,不就是干着这种偷窥隐射,后台偷偷跑流量的事吗?没错,确实存在,但这不能说Binder的安全性不好,因为Android系统仍然是掌握主控权,可以控制这类App的流氓行为,只是对于该采用何种策略来控制,在这方面android的确存在很多有待进步的空间,这也是google以及各大手机厂商一直努力改善的地方之一。在Android 6.0,google对于app的权限问题作为较多的努力,大大收紧的应用权限;另外,在Google举办的Android Bootcamp 2016大会中,google也表示在Android 7.0 (也叫Android N)的权限隐私方面会进一步加强加固,比如SELinux,Memory safe language(还在research中)等等,在今年的5月18日至5月20日,google将推出Android N。

话题扯远了,继续说Binder。

(4)从语言层面的角度
大家多知道Linux是基于C语言面向过程的语言,而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。

另外,Binder是为Android这类系统而生,而并非Linux社区没有想到Binder IPC机制的存在,对于Linux社区的广大开发人员,我还是表示深深佩服,让世界有了如此精湛而美妙的开源系统。也并非Linux现有的IPC机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种Linux的IPC机制都有存在的价值,同时在Android系统中也依然采用了大量Linux现有的IPC机制,根据每类IPC的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制,Android中的Kill Process采用的signal(信号)机制等等。而Binder更多则用在system_server进程与上层App层的IPC交互

(5) 从公司战略的角度

总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。

而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下 开源与商业化共存的一个成功典范。

有了这些铺垫,我们再说说Binder的今世前缘

Binder是基于开源的 OpenBinder实现的,OpenBinder是一个开源的系统IPC机制,最初是由 Be Inc.开发,接着由 Palm, Inc. 公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。

另外,再说一点关于OpenBinder,在2015年OpenBinder以及合入到Linux Kernel主线 3.19版本,这也算是Google对Linux的一点回馈吧。

综合上述5点,可知Binder是Android系统上层进程间通信的不二选择。


IPC 原理来自于

gityuan: http://gityuan.com/2015/10/31/binder-prepare/

参考资料:《计算机操作系统》

,