从源码解析-Android中进程间通信Binder机制之Linux基础 【一】

版权声明:本文为博主原创文章,转载请标明出处!如果文章有错误之处请留言。 https://blog.csdn.net/qq_30993595/article/details/81206781

IPC定义

广义上讲,进程间通信(Inter-process communication IPC)是指运行在不同进程(不论是否在同一台机器上)中的若干线程间的数据交换,

背景

我们知道,在操作系统中各个进程通常运行在独立的内存空间中,并且有严格的进程隔离机制来防止进程间的数据访问,因为不这样做就会引起很多的数据问题,这里会涉及到进程空间的概念:

  • 进程空间分为用户空间,内核空间
  • 用户空间的数据是隔离的,每个进程占用一个用户空间,不能相互访问
  • 内核空间是公用的,数据可以相互访问,所有的进程共用一个内核空间
  • 同一个进程内的用户空间和内核空间通过系统调用相互访问
  • 系统调用主要通过Linux中的copy_from_user()方法将数据从用户空间拷贝到内核空间,copy_to_user()将内核空间数据拷贝到用户空间

但是问题又来了,那两个进程间怎么通信呢?其实传统的进程间通信机制的原理就是:

  • 发送进程中的线程通过系统调用将数据拷贝到内核空间的缓存区
  • 内核空间中的服务程序通知接收进程里的线程
  • 然后接收线程通过系统调用将数据拷贝到自己的用户空间

传统IPC方法有如下几种:

共享内存(shared Memory):

这是一种常用的进程间通信机制,由于两个进程可以直接共享访问同一块内存区域,减少了数据的复制操作,因而在速度上有优势,实现方式:

  1. 创建内存共享区:进程A通过操作系统提供的API从内存中申请一块共享区域,比如Linux中可以通过shmget函数来实现,需要传入三个参数,分别是key、size、shmflg;有个返回值是内存共享区域的id值,用于唯一标识该区域;该方法生产的共享内存将于某个特定的key(第一个参数)进行绑定
  2. 映射内存共享区:成功创建内存共享区后,把它映射到进程A的空间中,在Linux中可以通过shmat函数实现,有三个参数shmid(第一步的返回值)、shmaddr、shmflag;同时有一个返回值,表示内存区的起始地址
  3. 访问内存共享区:进程B通过shmget函数,传入同样的key,然后再调用shmat函数即可将内存映射到自己的空间
  4. 开始通信:两个进程都进行了内存映射后就可以利用该区域通信了,但是内存共享没有同步机制,往往与信号灯等同步机制共同使用
  5. 删除内存共享区:通信结束后需要回收内存,在Linux中通过shmctl函数实现,有三个参数shmid、cmd、buf;有一个返回值表示删除成功还是失败

管道(Pipe):

管道也是操作系统中常见的一种进程间通信方式,Pipe这个词很好的诠释了通信双方的方式,

  1. 进程A和进程B位于管道两边,进行数据的传输
  2. 管道中数据是单向的,即管道只运行数据从一边进,从另一边出,这就跟水管一样
  3. 管道有容量限制,当容量满的时候,写操作将阻塞,反之读操作也会阻塞
  4. Linux中可以通过pipe(int pipefd[2] , int flags)函数打开管道,pipefd[0]表示读端,pipefd[1]表示写端

远程过程调用(Remote Procedure Calls)

RPC涉及的通信双方通常运行在不同的机器中,在RPC机器中,开发人员不需要关心具体的中间传输过程是如何实现的,实现步骤:

  1. 客户端进程调用Stub接口
  2. Stub根据操作系统要求打包
  3. 操作系统内核来完成与服务器端的具体交互,主要负责将客户端的数据包发送给服务器端的内核
  4. 服务器端Stub解包并调用与数据包相匹配的进程
  5. 服务器端进程执行操作并可将结果返回

还有Socket,消息队列,信号量等其它多种方式,这里不多赘述了。

使用Binder原因

但是Android并没有采用这些通信方法而选择了Binder(其实有少许地方使用到了其它方式,比如Zygote在fork进程的时候使用的是Socket),为什么呢,个人理解为

  • 性能:在早期移动设备上,各种资源是有限的,特别是内存,CPU资源,电量等,所以对各种进程间通信机制的性能要求高,而Binder的实现机制更加高效;一些传统进程通信机制使用了两次用户空间和内核空间的数据拷贝,比如管道、消息队列、Socket都需要2次,共享内存方式一次内存拷贝都不需要,但实现方式又比较复杂且没有同步机制;而Binder只用了一次,主要是因为使用了内存映射,底层IPC机制多使用一次数据拷贝其实对整体性能影响很大
  • 安全:传统的进程通信机制没有对通信双方做严格的身份验证,在使用Binder时,Android为每个安装好的应用程序分配了自己的UID/PID(用户ID/进程ID),这些身份标示是跟随调用过程而自动传递的。Server端很容易就可以知道Client端的身份,非常便于做安全检查;使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接
  • 架构:Binder是基于C/S架构的,更符合Android系统架构,比如媒体播放,音视频捕获,到各种让手机更智能的传感器(加速度,方位,温度,光亮度等)都由不同的Server负责管理,应用程序只需做为Client与这些Server建立连接便可以使用这些服务,传统IPC只有Socket支持Client-Server的通信方式

这里有一个内存映射的概念,它的出现也是因为传统进程通信方式的弊端;我们知道操作系统分为用户态和内核态,而用户态不能直接与硬件打交道,需要走硬件—>内核—>用户,这就出现了两次拷贝;使用了内存映射后,就只用一次拷贝,大大提高效率。

解释内存映射就要先说虚拟内存,说虚拟内存的概念先说为什么需要虚拟内存

虚拟内存的由来:

我们知道程序代码和数据需要被解释在内存中才能得以运行,早期计算机内存容量是比较小的,但是当时的程序更小,基本都是很简单的那种,所以把程序直接解释到内存中没有问题,不影响程序运行;但是随着互联网的飞速发展,计算机中装的软件越来越多,而且软件也越来越大,如果这时候还像以前那样把程序全部都解释到内存中,显然这种方式是行不通的,将会导致很多程序无法得到很好的运行,何况后来多任务的操作系统。
有什么解决办法呢,第一个想到的就是切片,将程序分成片段,只让当前需要的那一部分留在内存中,其它的放在外部存储上,执行结束后再调下一个需要运行的片段;在以前的一些古董系统里靠这方法解决一些大程序,并且这个分片工作是由程序员完成的;但是时代在发展,各种语言层出不穷,操作系统也越来越透明,程序员对底层技术的依赖逐渐消失,谁还能这样去操作系统呢;现在还让程序员这样做,不光程序难以运行,可能把机器都能搞蹦,所以系统需要一种新的按需分配内存且不用程序员管理的技术,就这样虚拟内存技术就应运而生

虚拟内存

计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换
#####虚拟内存原理
虚拟内存是将系统硬盘空间和系统实际内存联合在一起供程序使用,给程序提供了一个比实际内存大得多的虚拟空间。比如Win操作系统,只有4个G内存,然后把系统盘通常是C盘作为外部存储,这样就能给程序提供远大于4个G的虚拟内存使用。在程序运行时,只把虚拟内存的一小部分存储到内存,其余都存储在硬盘上(也就是说程序虚拟空间就等于实际物理内存加部分硬盘空间)。当被访问的虚拟地址(也就是虚拟内存对应的地址)不在内存时,则说明该地址未被存储到内存,而是被存贮在硬盘中,因此需要的虚拟地址随即被调入到内存;同时当系统内存紧张时,也可以把当前不用的虚拟内存换出到硬盘,来腾出物理内存空间。系统如此周而复始地运转——换入、换出,而用户几乎无法查觉,这都是拜虚拟内存机制所赐。

内存映射的概念

学习数学的时候肯定学过映射的概念,映射就是将两个事物建立一种一一对应的关系,只是逻辑上的一种关系,在物理上是不存在的;就是说你脑海中的想象,并不是真实存在的。
在这里就是把程序的虚拟内存的一块区域与某物理内存块之间建立一一对应的联系,这就是映射,然后操作程序的虚拟内存就是操作这块物理内存,就不需要经过内核转换;在内存映射过程中,并没有实际的数据拷贝,只是逻辑上放入了内存,具体到代码,就是建立并初始化了相关的数据结构,这个过程由系统调用mmap()实现,所以映射的效率很高。

  • 我们这里以文件为例:把硬盘文件内容映射到程序的虚拟内存中,通过对虚拟内存的读取修改就是对文件的读取修改,这样进程就可以像访问普通内存一样访问文件,而不用调用read()、write()等操作了
  • 再以进程为例,分别把进程A和进程B的虚拟内存区域的一块与共享内存C建立映射关系,假如进程A对自己的这块虚拟内存区域进行修改,那么也会映射到进程B的虚拟内存块;因为这两个进程都映射到了同一个共享内存C,所以A的修改对于B是可见的

上面说这么多Linux相关的知识都是为了Binder机制做铺垫


Binder组成

我们知道,Binder是Android中使用最广泛的IPC机制,那作为开发者怎么理解Binder这个东西呢,从字面理解为【捆绑】,也就是将两个需要相互联系的对象用某种力量绑定在一起,那这些参与进来的对象有哪些呢?

  • Binder驱动: 一种虚拟的设备驱动,尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的,在设备目录/dev下注册一个binder节点,提供标准文件操作;它是连接Client、Server、ServerManager的桥梁;Binder驱动保存了每个Server进程在内核空间中的Binder实体,并给Client进程提供Server进程的Binder实体的引用
  • Service Manager:主要管理进程注册与查询,并保存进程信息;每个对外公开服务的进程都需要到这里注册,注册成功会被分配一个唯一id;类似于派出所会保存每个人的信息,给每个人分配一个身份证号码,车管所保存每辆车信息,给每个车分配一个车牌号
  • Binder Client:使用Server的进程
  • Binder Server:提供Server的进程

Binder机制就是把这些东西绑定在一起,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间

Binder中的核心组件就是Binder驱动,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信

四个组件中的Binder驱动和Service Manager,Android已经替开发者封装好了,开发者只要写好Client和Server就可以了

类比理解

从上面的分析是不是感觉Binder原来就这么回事,实际上我感觉Binder是Android系统中最难理解的的一个机制了;那怎么以一个通俗易懂的例子来理解呢?

从组成Binder机制的的四个组件可以看出来,这其实与我们天天接触的TCP/IP网络非常相似

  • Binder驱动 可以理解为 Router路由器
  • Service Manager可以理解为 DNS服务器
  • Client 可以理解为 客户端,比如浏览器
  • Server 可以理解为 服务器

现在就客户端从浏览器访问Google的例子我们来分析

  1. 当我们在Client端浏览器输入google.com后,它会先通过DNS服务器查询该网站的IP地址(当然Client得先知道DNS的IP地址才能发起查询,这是在客户端接入网络就设置好了的);如果Client已经知晓了Server的IP,那就不需要DNS查询这一步了,直接去与Server链接。比如Window系统下有一个Host文件,用于查询常用网址与其对于的IP,当用户访问这个网址的时候就不要再去DNS查询了,就可以提高访问速度(当年还用过改host文件达到翻墙的功能)
  2. DNS服务器将查询结果返回Client,至于怎么返回,Client的IP地址都封装在了TCP/IP包中,在查询的时候发送给了DNS
  3. Client得到Google ip后就可以发起连接

这些步骤中没有提到Router,但是它的作用很重要,将数据包发到用户设定的目标IP中,也会获取Server端发送过来的数据包转发给Client端,所以Router可以说是整个网络通信的基础

在这里我们知道DNS其实不是必须要每一个完整通信过程都要存在的,它只是帮人们将繁杂的IP和好记的域名联系起来

从这个通信过程中我们可以看出IP地址对于每个用户来说是唯一的,是Client和Server进行通信的凭证

与Binder机制进行类比

Binder的本质就是进程A(客户端)要与进程B(服务器)进行通信,但是因为是跨进程(跨网络),所以必须借助于Binder驱动(路由器)来把请求正确发送到对方所在进程中,而通信的进程们需要持有Service Manager(DNS)颁发的唯一标志(IP)。
类比于TCP/IP网络,其实Binder当中的DNS也不是必须的,因为如果客户端进程能记住服务端进程的Binder标志(IP),就不需要通信前通过查询DNS了。
但是这个通信标志其实是动态改变的,也就是说即使客户端进程记住了这一次通信的服务端进程的标志,但下一次就不管用了,所以还是需要Service Manager来统一管理双方进程的标志。

上面的类比理解中,提到了其实Client端和Server端也是需要知道DNS服务器的ip的,要不然没办法给DNS发请求进行查询,所以在网络接入的时候提前设置好;到Binder机制中,Service Manager是DNS,那它的IP是多少呢?Binder机制做了特别规定,Service Manager在Binder通信过程中的唯一标志永远是0。

Service Manager和其它进程同样采用Binder通信,Service Manager是Server端,有自己的Binder对象(实体);其它进程都是Client,需要通过这个Binder的引用来实现Server的注册,查询和获取。Service Manager提供的Binder比较特殊,它没有名字也不需要注册,当系统启动时,一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成Service Manager时,Binder驱动会自动为它创建Binder实体。其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。也就是说,一个Server若要向Service Manager注册自己的Binder就必需通过0这个引用号和Service Manager的Binder通信。


跨进程通信的实现

假设Client进程想要调用Server进程的object对象的add方法,对于这个跨进程通信,我们看看Binder机制是如何做的
这里写图片描述

  1. 首先Server进程需要通过Binder驱动向SM注册,告诉SM自己名字叫LaoWang,有一个Object对象,可以执行add方法
  2. SM收到消息后,建立了一张表,保存LaoWang及对应的Server进程的Object对象
  3. Client通过Binder驱动向SM查询,需要用到叫LaoWang的进程的Object对象
  4. 这时候驱动不会返回一个真正的Object对象给Client进程,而是返回一个它的代理对象ObjectProxy,这个代理对象也有一个add方法,不过没有具体功能实现,只是将Client发送的参数包装起来交给Binder驱动
  5. 驱动收到消息后,查询得知之前用ObjectProxy代替Object发送给Client的,它真正要调用的是Object的add方法;于是Binder驱动通知Server进程,调用你的Object对象的add方法,然后把结果返回给我
  6. Server进程收到消息,进行调用后将结果返回给Binder驱动,然后Binder驱动再把结果发送给Client,这样一个跨进程通信就结束了

由于驱动返回的ObjectProxy与Server进程里面原始的Object是如此相似,给人感觉好像是直接把Server进程里面的对象Object传递到了Client进程;因此,我们可以说Binder对象是可以进行跨进程传递的对象

但事实上我们知道,Binder跨进程传输并不是真的把一个对象传输到了另外一个进程;传输过程好像是Binder跨进程穿越的时候,它在一个进程留下了一个真身,在另外一个进程幻化出一个影子(这个影子可以很多个);Client进程的操作其实是对于影子的操作,影子利用Binder驱动最终让真身完成操作。

理解这一点非常重要;务必仔细体会。另外,Android系统实现这种机制使用的是代理模式, 对于Binder的访问,如果是在同一个进程(不需要跨进程),那么直接返回原始的Binder实体;如果在不同进程,那么就返回Binder的引用(影子);一个Binder实体可以有很多Binder引用,因为可以有很多Clinet要使用Server;我们在系统源码以及AIDL的生成代码里面可以看到很多这种实现。

Server进程里面的Binder对象指的是Binder本地对象,Client里面的对象指的是Binder代理对象;在Binder对象进行跨进程传递的时候,Binder驱动会自动完成这两种类型的转换;因此Binder驱动必然保存了每一个跨越进程的Binder对象的相关信息;在驱动中,Binder本地对象的代表是一个叫做binder_node的数据结构,Binder代理对象是用binder_ref代表的;有的地方把Binder本地对象直接称作Binder实体,把Binder代理对象直接称作Binder引用(句柄)

一句话总结就是:Client进程只不过是持有了Server端的代理对象;代理对象协助驱动完成了跨进程通信。

接下来可以查看下面这篇AIDL文章,相信你看完后会理解的更透彻
Android通过AIDL达到进程间通信

展开阅读全文

没有更多推荐了,返回首页