Android系列:Binder第一篇

496 阅读14分钟

参考文章:gityuan.com/android/

1.问题

  • binder是什么
  • binder存在的意义是什么
  • binder的架构原理

该篇文章,从以下几个角度去分析:

  • 一些Linux进程相关的知识
  • Binder到底是什么
  • Binder机制是如何跨进程的。
  • 一次Binder通信的流程是怎样的
  • Java层的Binder

一、Binder是什么

Binder是什么,英文翻译是粘合剂,它是用来实现IPC的一种机制。为了更好的解释什么是Binder,我们先来了解下Linux进程相关的一些东西。这里需要理解清楚以下几个问题:

  • 为什么需要跨进程通信
  • 怎样进行跨进程通信
  • Android为什么选择Binder

1.1 Linux为什么要IPC

进程隔离

进程隔离就是进程之间不共享内存,相互是无感知的,两个进程要交换数据就得用一套专门约定的方案趋势实现,这就是IPC。

用户空间和内核空间

==Linux Kernel==是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。

对于Kernel这么一个高安全级别的东西,显然是不容许其它的应用程序随便调用或访问的,所以需要对Kernel提供一定的保护机制,这个保护机制用来告诉那些应用程序,你只可以访问某些许可的资源,不许可的资源是拒绝被访问的,于是就把Kernel和上层的应用程序抽像的隔离开,分别称之为Kernel Space和User Space。

==也就是说用户空间和内核空间是隔离的。==

系统调用/内核态/用户态

虽然在逻辑上,用户空间和内核空间是分开的,但是它们之间总得有要通信的时候,比如读取文件(磁盘),Linux为它们之间的调用制定了统一的方案,那就是系统调用:

Kernel space can be accessed by user processes only through the use of system calls

也就是说,用户空间要访问内核空间就必须得使用系统调用,不然呢,任何一个应用软件都随便进行内核的操作,万一把内核玩玩坏了怎么办,所以这些也是Linux基于安全的角度去考虑的。我们把进程运行用户空间的状态称为用户态,运行在内核空间的状态称为内核态。

系统调用通过以下两个函数来实现

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

1.2 为什么要使用Binder?

传统的IPC

内核和用户之间使用系统调用的方式去是实现的,那么用户空间之间该怎么搞呢?我们想到的是IPC,那么Linux下的IPC有哪些呢,原理是什么呢?

通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copyfromuser() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copytouser() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。如下图:

管道,socket,信号三种ipc都是采用上述办法。内存共享的ipc虽然没有数据的拷贝,但是实现起来很复杂,流程很难控制

Linux传统的IPC的缺点:

  • 性能问题:每次通信都进行两次数据拷贝
  • 不安全:比如socket 我只要知道ip地址 我就可以去篡改,做坏事
  • 稳定性:内存共享很难控制,流程复杂,稳定性也就不怎么好了。
内核模块/驱动

上述传统的方案都是内核通信机制的一部分,能够直接使用内核,这肯定没有问题,但是考虑到传统ipc问题一堆,对移动设备而言,性能和安全问题是非常重要的,所以Android并没有采取传统的方式,采用了Bider。但是遗憾是的,

==问题:Binder并不是内核空间的一部分,那它怎么去访问内核呢?==

它利用了Linux的动态可加载模块(Loadable Kernel Module,==LKM==)机制去解决问题。模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行。这样,Android系统可以通过添加一个内核模块运行在内核空间,用户进程之间的通过这个模块作为桥梁,就可以完成通信了。

在Android系统中,这个运行在内核空间的,负责各个用户进程通过Binder通信的内核模块叫做Binder驱动;

驱动程序一般指的是设备驱动程序(Device Driver),是一种可以使计算机和设备通信的特殊程序。相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作;

驱动就是操作硬件的接口,为了支持Binder通信过程,Binder使用了一种“硬件”,因此这个模块被称之为==驱动==。

==也就是说,binder既然不是内核空间的一部分嘛,那我动态生成一个模块,并且把这个模块链接到内核空间里面去,那这个模块不就是可以操作内核空间了嘛,这个模块就是Binder驱动。==

二、为什么使用Binder

Android使用的Linux内核拥有着非常多的跨进程通信机制,比如管道,System V,Socket等;为什么还需要单独搞一个Binder出来呢?主要有两点,性能和安全。在移动设备上,广泛地使用跨进程通信肯定对通信机制本身提出了严格的要求;Binder相对出传统的Socket方式,更加高效;另外,传统的进程通信方式对于通信双方的身份并没有做出严格的验证,只有在上层协议上进行架设;比如Socket通信ip地址是客户端手动填入的,都可以进行伪造;而Binder机制从协议本身就支持对通信双方做身份校检,因而大大提升了安全性。这个也是Android权限模型的基础。

三、Binder通信的原理

3.1 Linux 内存映射

Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。

内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。

3.2 Binder通信原理

由于mmap一般是用在物理介质和应用进程之间建立映射关系,比如(磁盘-> 内核空间->用户空间),这样mmap就能够减少数据拷贝次数。但是Binder中没有物理介质的概念,它利用mmap是用来在==内核中创建用户接收数据的缓冲区==,一次完整的Binder IPC如下

  • 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
  • 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
  • 发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,==由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间==,这样便完成了一次进程间的通信。

3.3 Binder通信模型

接下来我们看看Binder的设计,一次完整的IPC通信必然会涉及到两个进程,我们称为Server和Client,由于进程隔离,所以这两个进程需要采用IPC。

讲解几个在通信过程中涉及的几个概念

Client/Server/ServiceManager/驱动

Binder 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。其中 ==Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间==。其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。

Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之前的关系。

具体表述如下:

==Binder 驱动==

Binder 驱动就如同路由器一样,是整个通信的核心;驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。

==ServiceManager 与实名 Binder==

ServiceManager 和 DNS 类似,作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用,使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用。注册了名字的 Binder 叫实名 Binder,就像网站一样除了除了有 IP 地址意外还有自己的网址。Server 创建了 Binder,并为它起一个字符形式,可读易记得名字,将这个 Binder 实体连同名字一起以数据包的形式通过 Binder 驱动发送给 ServiceManager ,通知 ServiceManager 注册一个名为“张三”的 Binder,它位于某个 Server 中。驱动为这个穿越进程边界的 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager。ServiceManger 收到数据后从中取出名字和引用填入查找表。

细心的读者可能会发现,ServierManager 是一个进程,Server 是另一个进程,Server 向 ServiceManager 中注册 Binder 必然涉及到进程间通信。当前实现进程间通信又要用到进程间通信,这就好像蛋可以孵出鸡的前提却是要先找只鸡下蛋!Binder 的实现比较巧妙,就是预先创造一只鸡来下蛋。ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡)。其次这个 Binder 实体的引用在所有 Client 中都固定为 0 而无需通过其它手段获得。也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信。类比互联网,0 号引用就好比是域名服务器的地址,你必须预先动态或者手工配置好。要注意的是,这里说的 Client 是相对于 ServiceManager 而言的,一个进程或者应用程序可能是提供服务的 Server,但对于 ServiceManager 来说它仍然是个 Client。

==Client 获得实名 Binder 的引用==

Server 向 ServiceManager 中注册了 Binder 以后, Client 就能通过名字获得 Binder 的引用了。Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder: 我申请访问名字叫张三的 Binder 引用。ServiceManager 收到这个请求后从请求数据包中取出 Binder 名称,在查找表里找到对应的条目,取出对应的 Binder 引用作为回复发送给发起请求的 Client。从面向对象的角度看,==Server 中的 Binder 实体现在有两个引用:一个位于 ServiceManager 中,一个位于发起请求的 Client 中。如果接下来有更多的 Client 请求该 Binder,系统中就会有更多的引用指向该 Binder ,就像 Java 中一个对象有多个引用一样==。

我们总结下Binder通信的过程

  • 首先,一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;

  • Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。

  • Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。

3.4 Binder中的代理模式

现在有个疑惑,就是A进程要想调用B进程中的某个对象的add方法。首先A需要拿到对象B,之后才可以调用B的方法。由于IPC数据都会流经Binder驱动,Binder驱动在数据流过的时候会做一点手脚,驱动看到Client发来的请求,需要请求B,它会查自己维护的表,返回给客户端一个代理对象proxy,这个proxy和Binder实体的代理几乎是一摸一样,里面也有add方法,只是这个add方法是一个傀儡,它没有执行能力,但是客户端并不知道它是傀儡,当客户端调用add方法的时候,数据流经binder驱动,binder会再次做数据转换,它会去通知server去执行add方法,并且把结果返回给Binder驱动,Binder驱动拿到结果后返回给Client,这样就完成了一次跨进程的通信。

这里需要一点,Binder驱动接收到Client的请求后,会做个简单的判断:如果Server和Client在同一进程中,那就不需要跨进程,直接返回binder实体,否则就返回Binder的proxy。代码如下

  public static BookManager asInterface(IBinder binder) {
        if (binder == null)
            return null;
        IInterface iin = binder.queryLocalInterface(DESCRIPTOR);
        if (iin != null && iin instanceof BookManager)
            return (BookManager) iin;
        return new Proxy(binder);
    }

到底什么是Binder?

  • 从IPC的角度来看:Binder是IPC的一种机制
  • 从Server端角度来看,它是Binder本地的实体
  • 从Client角度看,Binder是远程Server的一个代理对象,对这个代理对象的操作最终会通过Binder驱动去调用到最终的Binder实体上。
  • 从传输的角度看,Binder是一种可以跨进程传输的对象,Binder驱动可以对流经的对象做自动转换:代理对象和实体对象之间的转换

驱动里面的Binder

我们现在知道,Server进程里面的Binder对象指的是Binder本地对象,Client里面的对象值得是Binder代理对象;在Binder对象进行跨进程传递的时候,Binder驱动会自动完成这两种类型的转换;因此==Binder驱动必然保存了每一个跨越进程的Binder对象的相关信息;在驱动中,Binder本地对象的实体是一个叫做binder_node的数据结构,Binder代理对象是用binder_ref代表的==;有的地方把Binder本地对象直接称作Binder实体,把Binder代理对象直接称作Binder引用(句柄),其实指的是Binder对象在驱动里面的表现形式;读者明白意思即可。