[TOC]

概述

Binder实现的远程调用是一种面向对象的远程调用,那么它和面向过程的远程调用区别在什么地方呢?面向过程的远程调用实现起来比较容易,只要通过某种方式把需要执行的函数号和参数传递到服务进程,然后服务进程执行对应的函数就完成了。但是面向对象的调用则不同,同一个服务类可以创建出多个对象,因此,调用时不但要通过函数号和参数来识别要执行的函数,同时还要指定具体的对象,而对象是有生命周期的,还需要管理,这将使得面向对象的实现更加复杂。但是,这种复杂也带来更强大的功能,正因为Binder是面向对象的,我们可以创建出多个Binder实体对象来服务不同的客户,每个对象有自己的数据,相互间不会干扰,而面向过程的调用则无法做到这一点,它同一时刻只能服务一个客户。

Binder实现的远程调用是一种面向对象的远程调用,那么它和面向过程的远程调用区别在什么地方呢?面向过程的远程调用实现起来比较容易,只要通过某种方式把需要执行的函数号和参数传递到服务进程,然后服务进程执行对应的函数就完成了。但是面向对象的调用则不同,同一个服务类可以创建出多个对象,因此,调用时不但要通过函数号和参数来识别要执行的函数,同时还要指定具体的对象,而对象是有生命周期的,还需要管理,这将使得面向对象的实现更加复杂。但是,这种复杂也带来更强大的功能,正因为Binder是面向对象的,我们可以创建出多个Binder实体对象来服务不同的客户,每个对象有自己的数据,相互间不会干扰,而面向过程的调用则无法做到这一点,它同一时刻只能服务一个客户。

参数的传递问题是面向对象的实现面临的又一个难题,一般的对象作为参数传递没有太大的问题,只需要序列化和反序列化就能实现。但是,当Binder对象作为参数传递的时候,就会面临实体对象和引用对象相互转换的问题,为了让上层应用使用方便,这种转换也在驱动中自动完成。

Binder的数据拷贝

正如前面所说,跨进程通信是需要内核空间做支持的。传统的 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 通信过程通常是这样:

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

img