重学Binder进程间通信-原理篇

710 阅读8分钟

“我正在参加「掘金·启航计划」”

前言

Binder对于Android系统来说非常重要,毫不夸张的说:无Binder就无Android,Binder是Android系统的任督二脉。正如其名Binder其名“粘合剂”一般,Binder是系统间各个组件的桥梁。贯穿整个Android系统。

理解Binder对于理解整个Android系统有着非常重要的作用,

我们都知道,Android是在Linux基础上构建的,如果有人问你Android相比较Linux有什么明显的特点,回答肯定是Android的Binder是相较于Linux最大的特点。

Android系统的四大组件(activity、service、content provider、broadcast receiver)、ActivityManagerService(简称:AMS)、PacageManagerService(简称:PMS)、BatterManager等等系统服务无一不和Binder挂钩,很多人对Binder的理解只是通过aidl进行跨进程通信。至于怎么灵活使用Binder,内部是怎么执行到驱动都不太懂,作为一名老Android来说很惭愧,所以打算重学基础,理解FrameWork,Binder是必须要迈出的一步。

为了理解Binder我们需要先澄清一些概念。

  • 什么是跨进程通信(IPC)?
  • 为什么Android非得需要跨进程通信(IPC),
  • Android又是怎么做到跨进程通信(IPC)的?
  • 到底什么是是Binder,作用是什么?

这些问题的背后都与 Binder 有莫大的关系,要弄懂上面这些问题理解 Bidner 通信机制是必须的。

Linux下传统的进程间通信原理

了解 Linux IPC 相关的概念和原理有助于我们理解 Binder 通信原理。因此,在介绍 Binder 跨进程通信原理之前,我们先聊聊 Linux 系统下传统的进程间通信是如何实现。

这里我们先从 Linux 中进程间通信涉及的一些基本概念开始介绍,然后逐步展开,向大家说明传统的进程间通信的原理。 上图展示了 Liunx 中跨进程通信涉及到的一些基本概念:

  • 进程隔离
  • 进程空间划分:用户空间(User Space)/内核空间(Kernel Space)
  • 系统调用:用户态/内核态

1. 进程隔离

为了保证 安全性 & 独立性,一个进程 不能直接操作或者访问另一个进程,即Android的进程是相互独立、隔离的

譬如两个进程就像两个平行的世界,A 进程没法直接访问 B 进程的数据,这就是进程隔离的通俗解释。A 进程和 B 进程之间要进行数据交互就得采用特殊的通信机制:进程间通信(IPC)。

2. 进程空间

进程空间划分为:用户空间(User Space) / 内核空间(Kernel Space)

现在操作系统都是采用的 虚拟存储器 ,对于 32 位系统而言,它的寻址空间(虚拟存储空间)就是 2 的 32 次方,也就是 4GB。操作系统的核心是内核,独立于普通的应用程序,内核是计算机硬件与其进程之间的核心接口。它负责两者之间的通信,还要尽可能高效地管理资源。

可以这样理解 内核就像是一个为高管(硬件)服务的个人助理。助理的工作就是将员工(用户)的消息和请求(进程)转交给高管,记住存放的内容和位置(内存),并确定在任何特定的时间谁可以拜访高管、会面时间有多长,也可以保证高管的人身安全。

在操作系统层面,为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间(User Space)和内核空间(Kernel Space)。针对 Linux 操作系统而言,将最高的 1GB 字节供内核使用,称为内核空间;较低的 3GB 字节供各进程使用,称为用户空间。 总结来说就是:内核空间(Kernel Space)是系统内核运行的空间,用户空间(User Space)是用户程序运行的空间。为了保证安全性,它们之间是绝对隔离的。

3. 系统调用

虽然从逻辑上对进程空间进行了用户空间和内核空间的划分,但不可避免的用户空间需要访问内核资源,比如文件操作、访问网络等等。为了突破隔离限制,就需要借助 系统调用 来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。

用户态与内核态

Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。

1> 当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态) 此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。

2> 当进程在执行用户自己的代码时,则称其处于用户运行态(用户态) 。此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。

系统调用主要通过如下两个函数来实现:

copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间

4. Linux 跨进程通信( IPC )

理解了上面的几个概念,我们再来看看传统的 IPC 方式中,进程之间是如何实现通信的。

通常的做法是(如下图所示):

1:发送进程 通过 系统调用, 将需要发送的数据拷贝到 Linux 进程的内核空间的缓存区中(即通过 copy_from_user() 进行一次数据拷贝)。

2:内核服务程序被唤醒,唤醒接收进程的接收线程,通过 系统调用 将数据发送到接收进程的 用户空间 中(即通过 copy_to_user() 进行第二次数据拷贝),最终及完成了数据发送。 这种传统的 IPC 通信方式有两个问题:

  1. 效率低下,一次数据传递需要经历 2 次数据拷贝:内存缓存区 --> 内核缓存区 --> 内存缓存区。
  2. 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法前者浪费空间后者费时间。

Binder 跨进程通信机制

Binder通信采用C/S架构,从组件视角来说,包含 ClientServerServiceManager 以及 binder驱动 ,其中ServiceManager用于管理系统中的各种服务。架构图如下所示: image.png 首先是要注册一个 ServiceManager,server端创建实名binder,向SMG注册自己可以提供的服务,以及该实名binder的标签,smg会在svcinfo 链表中缓存该server提供的binder信息,当client需要使用该服务时,只需要向smg中查询服务,获取server端binder的引用就可以了,这其中所有的通讯细节,全部需要binder驱动来实现。

binder驱动为应用层提供了open(),mmap(),poll(),ioctl()等标准的文件操作【注1】,open()负责打开驱动,mmap()负责对binder做内核空间向用户空间的地址映射,ioctl()负责binder协议的通信。 image.png 打开一次binder通讯,大致分为以下流程:

  • 1 调用open()方法打开binder驱动。
  • 2 调用 mmap()方法申请一块内存用来接受通信中的数据,并进行内存映射。
  • 3 调用 ioctl()方法 开启binder通讯。

勉强看完还是云里雾里,一大堆名词,一头雾水。完全看不懂,我想说的是: Binder机制终究不是三言两语就能解释清楚的,我们要一点点的啃,本篇我们先大概了解一下,后边的几篇我们将一步步慢慢的揭开Binder的神秘面纱。

Binder进程间到底是怎么通信的,如何在Android中实现 C/S Binder 通信。

什么是aidl?为什么要有aidl?aidl是怎样生成java代码的?

关键字 inoutinout是什么?

binder 的 linkToDeath、messager、epoll等等。

参考资料:

# 什么是 Linux 内核?

# Binder学习指南

# 写给 Android 应用工程师的 Binder 原理剖析

# 从驱动角度理解binder