24小时新闻关注

蔡文静,呼和浩特天气,床垫品牌排行榜-得力智选内容聚合平台

面试官: 谈一谈Binder的原理和完结一次复制的流程

心思剖析:能问出该问题,面试官对binder的了解是十分深化的。想问求职者对Android底层有没有深化了解

**求职者:**应该从linux进程通讯原理的两次复制说起,然后引申为什么binder却只有一次复制 ,终究论述内核空间 与用户空间的界说

1 Linux 下传统的进程间通讯原理

了解 Linux IPC 相关的概念和原理有助于咱们了解 Binder 通讯原理。因而,在介绍 Binder 跨进程通讯原理之前,咱们先聊聊 Linux 体系下传统的进程间通讯是怎么完结。

1.1 基本概念介绍

这儿咱们先从 Linux 中进程间通讯触及的一些基本概念开端介绍,然后逐渐打开,向咱们阐明传统的进程间通讯的原理。

上图展示了 Liunx 中跨进程通讯触及到的一些基本概念:

  • 进程阻隔
  • 进程空间区分:用户空间(User Space)/内核空间(Kernel Space)
  • 体系调用:用户态/内核态

进程阻隔

简略的说便是操作体系中,进程与进程间内存是不同享的。两个进程就像两个平行的国际,A 进程无法直接拜访 B 进程的数据,这便是进程阻隔的浅显解说。A 进程和 B 进程之间要进行数据交互就得选用特别的通讯机制:进程间通讯(IPC)。

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

现在操作体系都是选用的虚拟存储器,关于 32 位体系而言,它的寻址空间(虚拟存储空间)便是 2 的 32 次方,也便是 4GB。操作体系的中心是内核,独立于一般的应用程序,能够拜访受保护的内存空间,也能够拜访底层硬件设备的权限。为了保护用户进程不能直接操作内核,确保内核的安全,操作体系从逻辑大将虚拟空间区分为用户空间(User Space)和内核空间(Kernel Space)。针对 Linux 操作体系而言,将最高的 1GB 字节供内核运用,称为内核空间;较低的 3GB 字节供各进程运用,称为用户空间。

简略的说便是,内核空间(Kernel)是体系内核运转的空间,用户空间(User Space)是用户程序运转的空间。为了确保安全性,它们之间是阻隔的。

体系调用:用户态与内核态

尽管从逻辑进步行了用户空间和内核空间的区分,但不可防止的用户空间需求拜访内核资源,比方文件操作、拜访网络等等。为了打破阻隔约束,就需求凭借体系调用来完结。体系调用是用户空间拜访内核空间的仅有办法,确保了一切的资源拜访都是在内核的操控下进行的,防止了用户程序对体系资源的越权拜访,提升了体系安全性和稳定性。

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

当一个使命(进程)履行体系调用而堕入内核代码中履行时,称进程处于内核运转态(内核态)。此刻处理器处于特权级最高的(0级)内核代码中履行。当进程处于内核态时,履行的内核代码会运用当时进程的内核栈。每个进程都有自己的内核栈。

当进程在履行用户自己的代码的时分,咱们称其处于用户运转态(用户态)。此刻处理器在特权级最低的(3级)用户代码中运转。

体系调用首要经过如下两个函数来完结:

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

2.2 Linux 下的传统 IPC 通讯原理

了解了上面的几个概念,咱们再来看看传统的 IPC 办法中,进程之间是怎么完结通讯的。

一般的做法是音讯发送方即将发送的数据寄存在内存缓存区中,经过体系调用进入内核态。然后内核程序在内核空间分配内存,拓荒一块内核缓存区,调用 copyfromuser() 函数将数据从用户空间的内存缓存区复制到内核空间的内核缓存区中。相同的,接纳方进程在接纳数据时在自己的用户空间拓荒一块内存缓存区,然后内核程序调用 copytouser() 函数将数据从内核缓存区复制到接纳进程的内存缓存区。这样数据发送方进程和数据接纳方进程就完结了一次数据传输,咱们称完结了一次进程间通讯。如下图:

这种传统的 IPC 通讯办法有两个问题:

  1. 功用低下,一次数据传递需求阅历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需求 2 次数据复制;
  2. 接纳数据的缓存区由数据接纳进程供给,可是接纳进程并不知道需求多大的空间来寄存即将传递过来的数据,因而只能拓荒尽可能大的内存空间或许先调用 API 接纳音讯头来获取音讯体的巨细,这两种做法不是糟蹋空间便是糟蹋时间。

2. Binder 跨进程通讯原理

了解了 Linux IPC 相关概念和通讯原理,接下来咱们正式介绍下 Binder IPC 的原理。

2.1 动态内核可加载模块 && 内存映射

正如前面所说,跨进程通讯是需求内核空间做支撑的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因而经过内核支撑来完结进程间通讯天然是没问题的。可是 Binder 并不是 Linux 体系内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功用的程序,它能够被独自编译,可是不能独立运转。它在运转时被链接到内核作为内核的一部分运转。这样,Android 体系就能够经过动态增加一个内核模块运转在内核空间,用户进程之间经过这个内核模块作为桥梁来完结通讯。

在 Android 体系中,这个运转在内核空间,担任各个用户进程经过 Binder 完结通讯的内核模块就叫 Binder 驱动(Binder Dirver)。

那么在 Android 体系中用户进程之间是怎么经过这个内核模块(Binder 驱动)来完结通讯的呢?莫非是和前面说的传统 IPC 机制相同,先将数据从发送方进程复制到内核缓存区,然后再将数据从内核缓存区复制到接纳方进程,经过两次复制来完结吗?明显不是,不然也不会有开篇所说的 Binder 在功用方面的优势了。

这就不得不通道 Linux 下的另一个概念:内存映射。

Binder IPC 机制中触及到的内存映射经过 mmap() 来完结,mmap() 是操作体系中一种内存映射的办法。内存映射简略的讲便是将用户空间的一块内存区域映射到内核空间。映射联系树立后,用户对这块内存区域的修正能够直接反应到内核空间;反之内核空间对这段区域的修正也能直接反应到用户空间。

内存映射能削减数据复制次数,完结用户空间和内核空间的高效互动。两个空间各自的修正能直接反映在映射的内存区域,然后被对方空间及时感知。也正由于如此,内存映射能够供给对进程间通讯的支撑。

2.2 Binder IPC 完结原理

Binder IPC 正是依据内存映射(mmap)来完结的,可是 mmap() 一般是用在有物理介质的文件体系上的。

比方进程中的用户区域是不能直接和物理设备打交道的,假如想要把磁盘上的数据读取到进程的用户区域,需求两次复制(磁盘-->内核空间-->用户空间);一般在这种场景下 mmap() 就能发挥作用,经过在物理介质和用户空间之间树立映射,削减数据的复制次数,用内存读写替代I/O读写,进步文件读取功率。

而 Binder 并不存在物理介质,因而 Binder 驱动运用 mmap() 并不是为了在物理介质和用户空间之间树立映射,而是用来在内核空间创立数据接纳的缓存空间。

一次完好的 Binder IPC 通讯进程一般是这样:

  1. 首要 Binder 驱动在内核空间创立一个数据接纳缓存区;
  2. 接着在内核空间拓荒一块内核缓存区,树立内核缓存区和内核中数据接纳缓存区之间的映射联系,以及内核中数据接纳缓存区和接纳进程用户空间地址的映射联系;
  3. 发送方进程经过体系调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接纳进程的用户空间存在内存映射,因而也就相当于把数据发送到了接纳进程的用户空间,这样便完结了一次进程间的通讯。

如下图:

3. Binder 通讯模型

介绍完 Binder IPC 的底层通讯原理,接下来咱们看看完结层面是怎么规划的。

一次完好的进程间通讯必定至少包含两个进程,一般咱们称通讯的两边分别为客户端进程(Client)和服务端进程(Server),由于进程阻隔机制的存在,通讯两边必定需求凭借 Binder 来完结。

3.1 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 驱动)之前的联系。

一般咱们拜访一个网页的进程是这样的:首要在浏览器输入一个地址,如 http://www.google.com 然后按下回车键。可是并没有办法经过域名地址直接找到咱们要拜访的服务器,因而需求首要拜访 DNS 域名服务器,域名服务器中保存了 http://www.google.com 对应的 ip 地址 10.249.23.13,然后经过这个 ip 地址才干放到到 http://www.google.com 对应的服务器。

Android Binder 规划与完结一文中对 Client、Server、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 中一个目标有多个引证相同。

3.2 Binder 通讯进程

至此,咱们大致能总结出 Binder 通讯进程:

  1. 首要,一个进程运用 BINDERSETCONTEXT_MGR 指令经过 Binder 驱动将自己注册成为 ServiceManager;
  2. Server 经过驱意向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),标明能够对外供给服务。驱动为这个 Binder 创立坐落内核中的实体节点以及 ServiceManager 对实体的引证,将姓名以及新建的引证打包传给 ServiceManager,ServiceManger 将其填入查找表。
  3. Client 经过姓名,在 Binder 驱动的协助下从 ServiceManager 中获取到对 Binder 实体的引证,经过这个引证就能完结和 Server 进程的通讯。

咱们看到整个通讯进程都需求 Binder 驱动的接入。下图能愈加直观的展示整个通讯进程(为了进一步笼统通讯进程以及出现上的便利,下图咱们疏忽了 Binder 实体及其引证的概念):

4 Binder 通讯中的署理形式

咱们现已解说清楚 Client、Server 凭借 Binder 驱动完结跨进程通讯的完结机制了,可是还有个问题会让咱们困惑。A 进程想要 B 进程中某个目标(object)是怎么完结的呢?究竟它们分属不同的进程,A 进程 无法直接运用 B 进程中的 object。

前面咱们介绍过跨进程通讯的进程都有 Binder 驱动的参加,因而在数据流经 Binder 驱动的时分驱动会对数据做一层转化。当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 回来给 A,而是回来了一个跟 object 看起来一模相同的署理目标 objectProxy,这个 objectProxy 具有和 object 一摸相同的办法,可是这些办法并没有 B 进程中 object 目标那些办法的才能,这些办法只需求把把恳求参数交给驱动即可。关于 A 进程来说和直接调用 object 中的办法是相同的。

当 Binder 驱动接纳到 A 进程的音讯后,发现这是个 objectProxy 就去查询自己保护的表单,一查发现这是 B 进程 object 的署理目标。于是就会去告诉 B 进程调用 object 的办法,并要求 B 进程把回来成果发给自己。当驱动拿到 B 进程的回来成果后就会转发给 A 进程,一次通讯就完结了。

5 Binder 的完好界说

现在咱们能够对 Binder 做个愈加全面的界说了:

  • 从进程间通讯的视点看,Binder 是一种进程间通讯的机制;
  • 从 Server 进程的视点看,Binder 指的是 Server 中的 Binder 实体目标;
  • 从 Client 进程的视点看,Binder 指的是对 Binder 署理目标,是 Binder 实体目标的一个长途署理
  • 从传输进程的视点看,Binder 是一个能够跨进程传输的目标;Binder 驱动会对这个跨过进程鸿沟的目标对一点点特别处理,主动完结署理目标和本地目标之间的转化。

6. 手动编码完结跨进程调用

一般咱们在做开发时,完结进程间通讯用的最多的便是 AIDL。当咱们界说好 AIDL 文件,在编译时编译器会帮咱们生成代码完结 IPC 通讯。凭借 AIDL 编译今后的代码能协助咱们进一步了解 Binder IPC 的通讯原理。

可是无论是从可读性仍是可了解性上来看,编译器生成的代码对开发者并不友爱。比方一个 BookManager.aidl 文件对应会生成一个 BookManager.java 文件,这个 java 文件包含了一个 BookManager 接口、一个 Stub 静态的笼统类和一个 Proxy 静态类。Proxy 是 Stub 的静态内部类,Stub 又是 BookManager 的静态内部类,这就造成了可读性和可了解性的问题。

Android 之所以这样规划其实是有道理的,由于当有多个 AIDL 文件的时分把 BookManager、Stub、Proxy 放在同一个文件里能有用防止 Stub 和 Proxy 重名的问题。

因而便于咱们了解,下面咱们来手动编写代码来完结跨进程调用。

6.1 各 Java 类责任描绘

在正式编码完结跨进程调用之前,先介绍下完结进程中用到的一些类。了解了这些类的责任,有助于咱们更好的了解和完结跨进程通讯。

  • IBinder : IBinder 是一个接口,代表了一种跨进程通讯的才能。只需完结了这个托言,这个目标就能跨进程传输。
  • IInterface : IInterface 代表的便是 Server 进程目标具有什么样的才能(能供给哪些办法,其实对应的便是 AIDL 文件中界说的接口)
  • Binder : Java 层的 Binder 类,代表的其实便是 Binder 本地目标。BinderProxy 类是 Binder 类的一个内部类,它代表长途进程的 Binder 目标的本地署理;这两个类都承继自 IBinder, 因而都具有跨进程传输的才能;实际上,在跨过进程的时分,Binder 驱动会主动完结这两个目标的转化。
  • Stub : AIDL 的时分,编译东西会给咱们生成一个名为 Stub 的静态内部类;这个类承继了 Binder, 阐明它是一个 Binder 本地目标,它完结了 IInterface 接口,标明它具有 Server 许诺给 Client 的才能;Stub 是一个笼统类,详细的 IInterface 的相关完结需求开发者自己完结。

6.2 完结进程解说

一次跨进程通讯必定会触及到两个进程,在这个比如中 RemoteService 作为服务端进程,供给服务;ClientActivity 作为客户端进程,运用 RemoteService 供给的服务。如下图:

那么服务端进程具有什么样的才能?能为客户端供给什么样的服务呢?还记住咱们前面介绍过的 IInterface 吗,它代表的便是服务端进程详细什么样的才能。因而咱们需求界说一个 BookManager 接口,BookManager 承继自 IIterface,标明服务端具有什么样的才能。

/**
* 这个类用来界说服务端 RemoteService 具有什么样的才能
*/
public interface BookManager extends IInterface {
void addBook(Book book) throws RemoteException;
}

只界说服务端具有什么样的才能是不行的,既然是跨进程调用,那么接下来咱们得完结一个跨进程调用目标 Stub。Stub 承继 Binder, 阐明它是一个 Binder 本地目标;完结 IInterface 接口,标明具有 Server 许诺给 Client 的才能;Stub 是一个笼统类,详细的 IInterface 的相关完结需求调用方自己完结。

public abstract class Stub extends Binder implements BookManager {
...
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);
}
...
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
switch (code) {
case INTERFACE_TRANSACTION:
reply.writeString(DESCRIPTOR);
return true;
case TRANSAVTION_addBook:
data.enforceInterface(DESCRIPTOR);
Book arg0 = null;
if (data.readInt() != 0) {
arg0 = Book.CREATOR.createFromParcel(data);
}
this.addBook(arg0);
reply.writeNoException();
return true;
}
return super.onTransact(code, data, reply, flags);
}
...
}

Stub 类中咱们要点介绍下 asInterface 和 onTransact。

先说说 asInterface,当 Client 端在创立和服务端的衔接,调用 bindService 时需求创立一个 ServiceConnection 目标作为入参。在 ServiceConnection 的回调办法 onServiceConnected 中 会经过这个 asInterface(IBinder binder) 拿到 BookManager 目标,这个 IBinder 类型的入参 binder 是驱动传给咱们的,正如你在代码中看到的相同,办法中会去调用 binder.queryLocalInterface() 去查找 Binder 本地目标,假如找到了就阐明 Client 和 Server 在同一进程,那么这个 binder 自身便是 Binder 本地目标,能够直接运用。不然阐明是 binder 是个长途目标,也便是 BinderProxy。因而需求咱们创立一个署理目标 Proxy,经过这个署理目标来是完结长途拜访。

接下来咱们就要完结这个署理类 Proxy 了,既然是署理类天然需求完结 BookManager 接口。

public class Proxy implements BookManager {
...
public Proxy(IBinder remote) {
this.remote = remote;
}
@Override
public void addBook(Book book) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel replay = Parcel.obtain();
try {
data.writeInterfaceToken(DESCRIPTOR);
if (book != null) {
data.writeInt(1);
book.writeToParcel(data, 0);
} else {
data.writeInt(0);
}
remote.transact(Stub.TRANSAVTION_addBook, data, replay, 0);
replay.readException();
} finally {
replay.recycle();
data.recycle();
}
}
...
}

咱们看看 addBook() 的完结;在 Stub 类中,addBook(Book book) 是一个笼统办法,Server 端需求去完结它。

  • 假如 Client 和 Server 在同一个进程,那么直接便是调用这个办法。
  • 假如是长途调用,Client 想要调用 Server 的办法就需求经过 Binder 署理来完结,也便是上面的 Proxy。

在 Proxy 中的 addBook() 办法中首要经过 Parcel 将数据序列化,然后调用 remote.transact()。正如前文所述 Proxy 是在 Stub 的 asInterface 中创立,能走到创立 Proxy 这一步就阐明 Proxy 结构函数的入参是 BinderProxy,即这儿的 remote 是个 BinderProxy 目标。终究经过一系列的函数调用,Client 进程经过体系调用堕入内核态,Client 进程中履行 addBook() 的线程挂起等候回来;驱动完结一系列的操作之后唤醒 Server 进程,调用 Server 进程本地目标的 onTransact()。终究又走到了 Stub 中的 onTransact() 中,onTransact() 依据函数编号调用相关函数(在 Stub 类中为 BookManager 接口中的每个函数中界说了一个编号,只不过上面的源码中咱们简化掉了;在跨进程调用的时分,不会传递函数而是传递编号来指明要调用哪个函数);咱们这个比如里边,调用了 Binder 本地目标的 addBook() 并将成果回来给驱动,驱动唤醒 Client 进程里刚刚挂起的线程并将成果回来。

这样一次跨进程调用就完结了。

推荐新闻