腾讯面试官 :谈一谈Binder的原理和实现一次拷贝的流程
心理分析 :能问出该问题,面试官对binder的理解是很是深刻的。想问求职者对Android底层有没有深刻理解
求职者:应该从linux进程通讯原理的两次拷贝提及,而后引伸为何binder却只有一次拷贝 ,最后阐述内核空间 与用户空间的定义
了解 Linux IPC 相关的概念和原理有助于咱们理解 Binder 通讯原理。所以,在介绍 Binder 跨进程通讯原理以前,咱们先聊聊 Linux 系统下传统的进程间通讯是如何实现。java
这里咱们先从 Linux 中进程间通讯涉及的一些基本概念开始介绍,而后逐步展开,向你们说明传统的进程间通讯的原理。linux
上图展现了 Liunx 中跨进程通讯涉及到的一些基本概念:面试
简单的说就是操做系统中,进程与进程间内存是不共享的。两个进程就像两个平行的世界,A 进程无法直接访问 B 进程的数据,这就是进程隔离的通俗解释。A 进程和 B 进程之间要进行数据交互就得采用特殊的通讯机制:进程间通讯(IPC)。进程空间划分:用户空间(User Space)/内核空间(Kernel Space)如今操做系统都是采用的虚拟存储器,对于 32 位系统而言,它的寻址空间(虚拟存储空间)就是 2 的 32 次方,也就是 4GB。操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也能够访问底层硬件设备的权限。为了保护用户进程不能直接操做内核,保证内核的安全,操做系统从逻辑上将虚拟空间划分为用户空间(User Space)和内核空间(Kernel Space)。针对 Linux 操做系统而言,将最高的 1GB 字节供内核使用,称为内核空间;较低的 3GB 字节供各进程使用,称为用户空间。浏览器
简单的说就是,内核空间(Kernel)是系统内核运行的空间,用户空间(User Space)是用户程序运行的空间。为了保证安全性,它们之间是隔离的。
虽然从逻辑上进行了用户空间和内核空间的划分,但不可避免的用户空间须要访问内核资源,好比文件操做、访问网络等等。为了突破隔离限制,就须要借助系统调用来实现。系统调用是用户空间访问内核空间的惟一方式,保证了全部的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提高了系统安全性和稳定性。缓存
当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每一个进程都有本身的内核栈。当进程在执行用户本身的代码的时候,咱们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。系统调用主要经过以下两个函数来实现:安全
理解了上面的几个概念,咱们再来看看传统的 IPC 方式中,进程之间是如何实现通讯的。一般的作法是消息发送方将要发送的数据存放在内存缓存区中,经过系统调用进入内核态。而后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copyfromuser() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。一样的,接收方进程在接收数据时在本身的用户空间开辟一块内存缓存区,而后内核程序调用 copytouser() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,咱们称完成了一次进程间通讯。以下图:服务器
这种传统的 IPC 通讯方式有两个问题:1.性能低下,一次数据传递须要经历:内存缓存区 → 内核缓存区 → 内存缓存区,须要 2 次数据拷贝;2.接收数据的缓存区由数据接收进程提供,可是接收进程并不知道须要多大的空间来存放将要传递过来的数据,所以只能开辟尽量大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种作法不是浪费空间就是浪费时间。2. Binder 跨进程通讯原理理解了 Linux IPC 相关概念和通讯原理,接下来咱们正式介绍下 Binder IPC 的原理。网络
正如前面所说,跨进程通讯是须要内核空间作支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,所以经过内核支持来实现进程间通讯天然是没问题的。可是 Binder 并非 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具备独立功能的程序,它能够被单独编译,可是不能独立运行。它在运行时被连接到内核做为内核的一部分运行。这样,Android 系统就能够经过动态添加一个内核模块运行在内核空间,用户进程之间经过这个内核模块做为桥梁来实现通讯。架构
在 Android 系统中,这个运行在内核空间,负责各个用户进程经过 Binder 实现通讯的内核模块就叫 Binder 驱动(Binder Dirver)。
那么在 Android 系统中用户进程之间是如何经过这个内核模块(Binder 驱动)来实现通讯的呢?难道是和前面说的传统 IPC 机制同样,先将数据从发送方进程拷贝到内核缓存区,而后再将数据从内核缓存区拷贝到接收方进程,经过两次拷贝来实现吗?显然不是,不然也不会有开篇所说的 Binder 在性能方面的优点了。这就不得不通道 Linux 下的另外一个概念:内存映射。Binder IPC 机制中涉及到的内存映射经过 mmap() 来实现,mmap() 是操做系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系创建后,用户对这块内存区域的修改能够直接反应到内核空间;反以内核空间对这段区域的修改也能直接反应到用户空间。内存映射能减小数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正由于如此,内存映射可以提供对进程间通讯的支持。函数
Binder IPC 正是基于内存映射(mmap)来实现的,可是 mmap() 一般是用在有物理介质的文件系统上的。好比进程中的用户区域是不能直接和物理设备打交道的,若是想要把磁盘上的数据读取到进程的用户区域,须要两次拷贝(磁盘→内核空间→用户空间);一般在这种场景下 mmap() 就能发挥做用,经过在物理介质和用户空间之间创建映射,减小数据的拷贝次数,用内存读写取代I/O读写,提升文件读取效率。而 Binder 并不存在物理介质,所以 Binder 驱动使用 mmap() 并非为了在物理介质和用户空间之间创建映射,而是用来在内核空间建立数据接收的缓存空间。一次完整的 Binder IPC 通讯过程一般是这样:
介绍完 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 通讯过程:首先,一个进程使用 BINDERSETCONTEXT_MGR 命令经过 Binder 驱动将本身注册成为 ServiceManager;Server 经过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),代表能够对外提供服务。驱动为这个 Binder 建立位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。Client 经过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,经过这个引用就能实现和 Server 进程的通讯。咱们看到整个通讯过程都须要 Binder 驱动的接入。下图能更加直观的展示整个通讯过程(为了进一步抽象通讯过程以及呈现上的方便,下图咱们忽略了 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 进程,一次通讯就完成了。
如今咱们能够对 Binder 作个更加全面的定义了:
一般咱们在作开发时,实现进程间通讯用的最多的就是 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 重名的问题所以便于你们理解,下面咱们来手动编写代码来实现跨进程调用。
在正式编码实现跨进程调用以前,先介绍下实现过程当中用到的一些类。了解了这些类的职责,有助于咱们更好的理解和实现跨进程通讯。
一次跨进程通讯必然会涉及到两个进程,在这个例子中 RemoteService 做为服务端进程,提供服务;ClientActivity 做为客户端进程,使用 RemoteService 提供的服务。以下图:
那么服务端进程具有什么样的能力?能为客户端提供什么样的服务呢?还记得咱们前面介绍过的 IInterface 吗,它表明的就是服务端进程具体什么样的能力。所以咱们须要定义一个 BookManager 接口,BookManager 继承自 IIterface,代表服务端具有什么样的能力。
只定义服务端具有什么样的能力是不够的,既然是跨进程调用,那么接下来咱们得实现一个跨进程调用对象 Stub。Stub 继承 Binder, 说明它是一个 Binder 本地对象;实现 IInterface 接口,代表具备 Server 承诺给 Client 的能力;Stub 是一个抽象类,具体的 IInterface 的相关实现须要调用方本身实现。
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 接口。
咱们看看 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 进程里刚刚挂起的线程并将结果返回。这样一次跨进程调用就完成了。
2019秋招必备面试题汇总+阿里P6P7安卓进阶资料分享