Binder进程间通信机制(图文解析)

前言

本来想洋洋洒洒写一篇形象生动的Binder原理的文章,再配上我这个灵魂画手的图画,但是再经过了对驱动内核的研究后,我决定了,我自首,我这太年少了 我,太懵懂了,我没成想这Binder机制这么厉害!

作为一名Android开发,常年使用java开发的人来说,C实在是太不友好了…但是Binder机制对于整个Android系统来说,你难道没有感觉到,Binder是多么地重要~~

所以我作为一名Android开发人员,对于C语言和Linux内核也仅仅停留在一些理论上的人来说,我想通过我的理解来说分析一下Binder

概念普及

我相信能找到这篇文章的人,基本上都有一个常识就是,Binder是一种进程间用来通信的机制。由于Android系统基于Linux内核,为什么Android不使用Linux提供的进程间通信机制,而是使用Binder,这是个面试问题哦,因此我们有必要Linux进程间通信的相关知识。接下来就普及一下理论概念,拥有的一定的理论基础才能通晓文章的含义

  • Linux进程
    进程定义:进程是受操作系统管理的基本单元,是一个具有独立功能的程序的一次运行活动。其实进程有很多的定义,这个只是其中一种,那么我都知道android中的app就是一个程序,一个运行中的app就会至少对应一个进程,进程是一种动态的概念,程序是一种静态的概念。

  • 虚拟内存地址空间
    虚拟内存实际上是相对于物理内存来的,我们的程序编译好后放到磁盘这类持久化存储器中,在运行的时候都要加载到内存中,此时就需要分配实际的物理内存地址,但是我们在编写程序的时候不可能知道我将来会被加载到那块物理内存地址上,比如我写的程序main函数的入口地址是0x20000,当我们程序加载进内存的时候,0x20000这个内存可能被其他的应用程序占用了怎么办,所以其实我们程序员编程的时候面向的都是虚拟内存,从计算机发展来说也从最初的实模式到现在的保护模式等其他模式演进,程序加载的时候会有操作系统给我们分配一个起始地址,我们声明的变量都是基于这个起始地址的偏移,这块的内容可以先看下内存分段,然后了解下内存分页。

  • 进程隔离
    进程隔离是为保护操作系统中进程互不干扰而设计的一组不同硬件和软件的技术。这个技术是为了避免进程A写入进程B的情况发生。 进程的隔离实现,使用了虚拟地址空间。进程A的虚拟地址和进程B的虚拟地址映射的物理地址不同,这样就防止进程A将数据信息写入进程B。以上来自维基百科;操作系统的不同进程之间,数据不共享;对于每个进程来说,它都天真地以为自己独享了整个系统,完全不知道其他进程的存在;(有关虚拟地址,请自行查阅)因此一个进程需要与另外一个进程通信,需要某种系统机制才能完成。

  • 内核空间(Kernel Space)/用户空间(User Space)
    详细解释可以参考Kernel Space Definition;简单理解如下:
    Linux Kernel是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。对于Kernel这么一个高安全级别的东西,显然是不容许其它的应用程序随便调用或访问的,所以需要对Kernel提供一定的保护机制,这个保护机制用来告诉那些应用程序,你只可以访问某些许可的资源,不许可的资源是拒绝被访问的,于是就把Kernel和上层的应用程序抽像的隔离开,分别称之为Kernel Space和User Space。

  • Linux进程空间分配
    现在操作系统都是采用的虚拟存储器,对于 32 位系统而言,它的寻址空间(虚拟存储空间)就是 2 的 32 次方,也就是 4GB。Linux的虚拟地址空间范围为0~4G,Linux内核在逻辑上将这4G字节的空间分为两部分,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。

  • 共享资源
    看过上面我们知道内核由系统内的进程共享,而且我们应该也知道,进程在各自独立的地址空间中运行,进程间的资源是不共享的,进程间如果想要共享资源,想像在同一进程中那样调用方法,传递参数就需要使用一些手段,进程之间共享数据就需要使用进程间通信机制。

  • 系统调用/内核态/用户态
    虽然从逻辑上抽离出用户空间和内核空间;但是不可避免的的是,总有那么一些用户空间需要访问内核的资源;比如应用程序访问文件,网络是很常见的事情,怎么办呢?
    Kernel space can be accessed by user processes only through the use of system calls.
    翻译过来就是用户空间访问内核空间的唯一方式就是系统调用;通过这个统一入口接口,所有的资源访问都是在内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。用户软件良莠不齐,要是它们乱搞把系统玩坏了怎么办?因此对于某些特权操作必须交给安全可靠的内核来执行。

下文需要接触到的系统调用 copy_from_usercopy_to_usermmap

当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)此时处理器处于特权级最高的(ring 0级)内核代码中执行。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(ring 3级)用户代码中运行。处理器在特权等级高的时候才能执行那些特权CPU指令。

上面的情况我们称之为 上下文切换

  • 内核模块/驱动
    通过系统调用,用户空间可以访问内核空间,那么如果一个用户空间想与另外一个用户空间进行通信怎么办呢?很自然想到的是让操作系统内核添加支持;传统的Linux通信机制,比如Socket,管道等都是内核支持的;但是Binder并不是Linux内核的一部分,它是怎么做到访问内核空间的呢?Linux的动态可加载内核模块(Loadable Kernel Module,LKM)机制解决了这个问题;模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行。这样,Android系统可以通过添加一个内核模块运行在内核空间,用户进程之间的通过这个模块作为桥梁,就可以完成通信了。

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

  • Linux现有的进程间通信手段
    Linux已经拥有的进程间通信IPC手段包括(Internet Process Connection):
    管道(Pipe)、信号(Signal)和跟踪(Trace)、插口(Socket)、报文队列(Message)、共享内存(Share Memory)和信号量(Semaphore)

Binder优势,Android为什么采用Binder?

通过上面的概念普及,相信大家心里面应该对于进程间通信IPC有了一定的了解,但是我们这篇说的Binder并不是Linux进程间通信的传统手段,而是Android开发的一套新的进程间通信机制,既然有了传统的手段不用,非要自己弄,那么Binder一定相对于其它的进程间通信方法在Android平台上有一定优势。

  1. 复杂度
    Android 的System_server进程(AMS PMS这些服务所在的进程)Android Framework是一个C/S架构的Framework,简单理解就是AMS,PMS这些服务都是面向所有app进程的,一个1:N的模型。目前linux支持的IPC包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及socket中只有socket支持Client-Server架构的通信方式。当然也可以在这些底层机制上架设一套协议来实现Client-Server通信,但这样增加了系统的复杂性,在手机这种条件复杂,资源稀缺的环境下可靠性也难以保证。

  2. 效率
    socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中(copy_form_user),然后再从内核缓存区拷贝到接收方缓存区(copy_to_user),至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用(同步问题)。
    Binder进程间通信机制在进程间传输数据时,只需要执行一次数据拷贝操作,因此,它不仅提高了效率,而且节省了时间。(利用mmap来实现的一次数据拷贝)

  3. 安全性
    终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID和PID(用户ID进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID(app进程都是由Framework zygote进程fork出来的),故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID和PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,systemV的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。

当然以上都是别人说的,至于为什么安全性高,为什么只需要一次拷贝等等,这些的原理我们都不清楚,那么通过我这么些天的学习,来给大家揭开其中一部分原理。

Binder概念

面向对象思想的引入将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。最诱人的是,这个引用和java里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给其它进程,让大家都能访问同一Server,就象将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。形形色色的Binder对象以及星罗棋布的引用仿佛粘接各个应用程序的胶水,这也是Binder在英文里的原意。

Binder使用Client-Server通信方式:一个进程作为Server提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务;多个进程作为Client向Server发起服务请求,获得所需要的服务。要想实现Client-Server通信据必须实现以下两点:一是server必须有确定的访问接入点或者说地址来接受Client的请求,并且Client可以通过某种途径获知Server的地址;二是制定Command-Reply协议来传输数据。例如在网络通信中Server的访问接入点就是Server主机的IP地址+端口号,传输协议为TCP协议。对Binder而言,Binder可以看成Server提供的实现某个特定服务的访问接入点, Client通过这个‘地址’向Server发送请求来使用该服务;对Client而言,Binder可以看成是通向Server的管道入口,要想和某个Server通信首先必须建立这个管道并获得管道入口。

Binder进程间通信机制构成。

  1. Binder内核驱动
  2. Client端进程
  3. Server端进程
  4. Service Manager进程
  5. 以及要实现通信,Google帮我们封装了一个Binder进程间通信库,辅助我们实现进程间通信。

运行时 1位于内核空间 其他都位于用户空间

接下来我们单独分析。

Binder内核驱动程序

虽然翻译成驱动,但是它并不和硬件打交道,只是说原理类似驱动,既然提到内核,那么大家也就知道Binder内核驱动所在的空间,通过上面关于Linux进程空间的概念普及,我们知道进程可以通过系统调用访问内核,并且由系统内所有进程共享。

Binder内核驱动程序初始化时都做了些什么?
注意这里是初始化。接下来是时候展现我这个灵魂画手鬼斧神工般的技术了。
这里写图片描述

通过图片我们可以发现Binder初始化做的事情就是再设备上创建了一个目录,一个文件。

/proc/binder/proc目录
每一个使用了Binder进程间通信机制的进程在该目录下都对应有一个文件,这些文件以进程ID来命名,通过他们就可以读取到各个进程的Binder线程池Binder实体对象Binder引用对象以及内核缓冲区的信息。
好家伙,一个目录的作用就涉及到了N多个不知道的概念,放心既然是概念普及,我以后一定会讲解到,但是大家一定要记住这些概念的名称,别当我突然提起的时候一脸懵逼。

/dev/binder设备文件
这个设备文件的操作方法列表是由全局变量binder_fops指定的,我们不管是谁指定的,我们知道这个设备文件里有操作方法。

  • 打开方法binder_open

  • 内存映射binder_mmap

  • IO控制binder_ioctl

这3个操作方法,当被调用的时候Binder内核驱动将做哪些事情呢?内核嘛都是C/C++嘛,所以这些方法都选C!
我这里根据我学习的资料来简单的描述一下吧!当然这些方法就是Binder内核的奥秘了,不可能通过我的短暂学习就理解透彻,更不可能三言两语就说的清楚,但是希望能在大家的脑海中留下一些理论概念。既然都是C/C++我就不带大家看代码了,以java面向对象的思维去分析它们都干了什么!

打开方法binder_open

  1. 一个进程在使用Binder进程间通信机制之前,首先就会调用这个函数来打开设备文件/dev/binder来获得一个文件描述符,然后才能通过这个文件描述符和Binder驱动程序交互,继而执行Binder进程间通信。

  2. Binder驱动程序会在内核中创建一个binder_proc结构体proc。结构体大家当对象理解就好了。并将proc加入到一个hash队列binder_procs中,Binder驱动程序将所有打开了设备文件/dev/binder的进程都加入到全局hash队列binder_procs中,通过遍历这个hash队列就知道系统当前有多少个进程在使用Binder进程间通信机制。

  3. 文件描述符和结构体proc关联在一起,使用文件描述符作为参数调用mmapioctl,内核就能找到proc结构体。

友情提示:一个进程想要进行进程间通信,一定会在某个逻辑中调用这个方法
废话不多说,继续上图
这里写图片描述

内存映射binder_mmap

mmap前面见过吧,不是mmp啊,没学习过相关概念的人,相信是没办法理解什么是内存映射的,我就是其中之一,所以我度娘了一下,后来经过一系列的学习,对这个内存映射有了一定理解,还记得前面Linux的进程空间吗?不理解的可以自行度娘,这个就简单普及一下

Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap()
是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。

  1. 进程打开了设备文件/dev/binder之后,还必须要调用mmap把这个设备文件映射到用户进程的地址空间,映射到用户进程的地址空间,映射到用户进程的地址空间,重要的事情说三遍,然后才能使用Binder进程间通信机制。映射到用户进程的地址空间是为了啥?

  2. 为了为进程分配内核缓冲区,以便用来传输进程间通信的数据,知识点到了,内核缓冲区,我理解就是一块位于内核空间的一块内存区域。当进程调用mmap将设备文件/dev/binder映射到用户的地址空间时,就会分配内核缓冲区,Binder驱动程序为进程分配的内核缓冲区有两个地址,其中一个是用户空间地址,由参数vma所指向的一个vm_area_struct结构体来描述;另一个地址是内核空间地址,由变量area所指向的一个vm_struct结构体来描述。进程通过用户空间地址来访问这块内存缓冲区的内容,而Binder驱动程序通过内核空间地址来访问这块内核缓冲区内容,由于它们是连续的,并且起始值相差一个固定值,因此,只要知道其中一个地址,就可以方便地计算另外一个地址。

  3. 将内核缓冲区的信息和binder_proc关联在一起

结合内核缓冲区解读效率优势的秘密

用户空间、内核空间的概念前面我已经说过了,这里出现了吧,能明白一点吧,还记得我们谈论的Binder机制的优势之一效率吗?通过上面的描述,我们可以将内核缓冲区想象成一个共享的内存空间,Binder驱动程序和用户进程都可以访问,但是注意用户进程只可以读这块内存,Binder驱动才有管理的权利,当Binder驱动将其他进程的数据通过拷贝(copy_from_user)写入这个缓冲区,当前进程就可以直接获取到数据了,无需拷贝(因为这块内存可以简单当成进程和内核的共享内存),所以Binder机制只进行了一次的数据拷贝,这大大的节省了效率和时间。

知识点:Binder驱动程序会为进行进程间通信的进程创建相对应的内核缓冲区。
灵魂画手前来拜访
这里写图片描述

接下来是一段引用的段落,引用地址,这个描述比我详细,如果大家理解了我说的可以跳过引用段。
http://blog.csdn.net/universus/article/details/6211589

暂且撇开Binder,考虑一下传统的IPC方式中,数据是怎样从发送端到达接收端的呢?通常的做法是,发送方将准备好的数据存放在缓存区中,调用API通过系统调用进入内核中。内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程,完成一次数据发送。这种存储-转发机制有两个缺陷:首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝,在此过程中如果使用了高端内存(high memory),这种拷贝需要临时建立/取消页面映射,造成性能损失。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间或先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder采用一种全新策略:由Binder驱动负责管理数据接收缓存。我们注意到Binder驱动实现了mmap()系统调用,这对字符设备是比较特殊的,因为mmap()通常用在有物理存储介质的文件系统上,而象Binder这样没有物理介质,纯粹用来通信的字符设备没必要支持mmap()。Binder驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间。先看mmap()是如何使用的:

fd = open(“/dev/binder”, O_RDWR);

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区。mmap()的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必也不能直接访问(映射类型为PROT_READ,只读映射)。

接收缓存区映射好后就可以做为缓存池接收和存放数据了。前面说过,接收数据包的结构为binder_transaction_data,但这只是消息头,真正的有效负荷位于data.buffer所指向的内存中。这片内存不需要接收方提供,恰恰是来自mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是,存放binder_transaction_data结构本身以及表4中所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放data.buffer所指向的内存区。在介绍Binder协议时已经提到,这是由命令BC_FREE_BUFFER完成的。

通过上面介绍可以看到,驱动为接收方分担了最为繁琐的任务:分配/释放大小不等,难以预测的有效负荷缓存区,而接收方只需要提供缓存来存放大小固定,最大空间可以预测的消息头即可。在效率上,由于mmap()分配的内存是映射在接收方用户空间里的,所有总体效果就相当于对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,提升了一倍的性能。顺便再提一点,Linux内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’。

系统调用IO控制binder_ioctl

说实在的这个方法就要通过具体的情景分析了,既然说到了IO那肯定和数据传输有关,读啊,写啊,来啊,快活啊!其实我对于这部分的了解及其有限,但是我们可以知道它都做了什么。

  1. 系统调用,系统调用是干什么的,上面我已经说过了,进程可以通过系统调用访问内核,那么也就是说这个方法,可以让进程访问内核,自由的飞翔~ 这里实际到Binder线程池的概念。以后会说。

  2. IO命令控制,进程与内核间的很多协议通信传输就是依靠这个方法的case来进行相应的处理。

至此Binder内核部分分析暂时告于段落,这里只是分析了Binder驱动程序初始化做了什么,当然Binder驱动程序做的事情远远不止这些!

Binder进程间通信库

Android系统在应用程序框架层中将各种Binder驱动程序操作封装成一个Binder库,这样进程就可以方便地调用Binder库提供的接口来实现进程间通信。大家如果开发中想要使用让程序可以进行进程间通信,Android提供了很多的方法,其中基于Binder原理实现的进程间通信方式有Content Provider、Messenger、AIDL等,其中Messenger是基于AIDL的封装,Content Provider和AIDL底层都是Binder,所以说这三者都是基于Binder的。如果不知道AIDL如何使用的,可以先去了解一下!

进程间通信,既然需要跨进程通信那么肯定至少需要两个进程了,Binder机制是C/S架构的,什么是是C/S呢,就是Clinet端和Server端,一个是客户端,一个是提供服务服务端,如果想使用通信需要在客户端和服务端创建相应的Binder对象。这里说一下Client端和Server端,大家还需要知道这样一个理论概念,就是Client端和Server端是一个相对的概念,Client端请求服务,Server端提供服务,Client端同样可以提供服务(那么这个时候他就是Server端),Server端也同样可以请求服务(那么这个时候他就是Client端),只是在一次进程间通信过程中扮演的角色而已,每个进程的身份角色会根据自身的需要而发生变化。

这里继续普及概念啊,通信库涉及到的概念:
Service组件(Server端)和Client组件(Client端)分别使用模板类BnIterface和BpInterface来描述,Bn我想应该是Binder native的缩写,而Bp应该就是Binder Proxy的缩写,前者称为Binder本地对象,后者称为Binder代理对象,实际上进程间通信就是通过Binder代理对象发送进程间通信请求,通过Binder驱动等一系列操作,最后由Binder本地对象来处理请求,完成进程间通信。在使用Binder库开发Service组件和Client组件时,除了要定义Service组件接口之外,还必须要实现一个Binder本地对象和一个Binder代理对象,而如何获取Binder代理对象或者注册一个Binder本地对象就需要Service Manager的协助

  • 模板类BnIterface

模板类BnIterface继承自BBinder类,BBinder类有两个重要的成员函数transact和OnTransact,当一个Binder代理对象通过Binder驱动程序向一个Binder本地对象发出一个进程间通信请求时,Binder驱动程序就会调用Binder本地对象的成员函数transact来处理该请求。OnTransact是由BBinder的子类来实现的,它就是负责分发和业务相关的进程间通信请求,比如你要提供什么服务,服务里想做些什么,就是在这里写逻辑的。BBinder类又继承了IBinder类,而后者有继承了RefBase类。

  • 模板类BpInterface

模板类BpInterface继承自BpRefBase类,BprefBase类也继承了RefBase类,BpRefBase类有一个重要的成员变量mRemote,它指向一个BpBinder对象(Binder代理对象),可以用通过成员函数remote来获取。

  • BpBinder类

BpBinder实现了BpRefBase类的进程间通信接口,BpBinder成员变量mHandle是一个整数,表示一个Client组件的句柄值,每一个Client组件在Binder驱动程序中都对应一个Binder引用对象,而每一个Binder引用对象都有一个句柄值,这里又出现一个句柄值概念,句柄值是和内存地址息息相关的,通过句柄值就可以在列表中找到Binder引用对象。其中Client组件就是通过这个句柄值来和Binder驱动程序中的Binder引用对象建立对应关系的。BpBinder类成员函数transact用来发送进程间通信请求。

  • Binder线程池

看到这里有没有唤醒你的记忆呢,Binder本地和Binder代理都简单说过了,我相信这个概念大家不会不明白,线程池线程池,就是线程的池子咯,每一个使用了Binder进程间通信机制的进程都有一个Binder线程池们用来处理进程间通信请求,这个Binder线程池由Binder驱动程序来维护,还记的之前打开/dev/binder设备文件创建的结构体binder_proc吗,它的成员变量threads就是一个红黑树的根节点,它以线程ID作为关键字来组织一个进程的Binder线程池。进程可以调用ioctl将一个线程注册到Binder驱动中。

  • IPCThreadState类

无论是BBinder类(Binder本地对象),还是BpBinder类(Binder代理对象),它们都是通过IPCThreadState类来和Binder驱动程序交互的,对于每一个Binder线程来说,它内部都有一个IPCThreadState对象,IPCThreadState对象我们可以IPCThreadState的静态成员函数self来获取,并且调用它的成员函数transact(臭不要脸起一样的名字)来和Binder驱动程序交互,在IPCThreadState类的成员函数transact内部,与Binder驱动程序的交互操作又是通过调用成员函数talkWithDriver来实现的,OK终于知道进程是怎么和内核中Binder驱动程序交互的。

  • ProcessState类

IPCThreadState类有一个成员变量mProcess,它指向一个ProcessState对象。对于每一个使用了Binder进程间通信机制的进程来说,它内部都有一个ProcessState对象,它负责初始化设备,即打开/dev/binder设备文件,以及将设备文件/dev/binder映射到进程的地址空间。OK我们又找到了打开设备文件和进行内存映射的类。ProcessState对象在进程范围内是唯一的,不像IPCThreadState对象每个Binder线程内部都有一个,因此每个Binder线程都可以通过它来和Binder驱动程序建立连接。

还记得之前内核中的一些结构吗,如果不记得了看看之前的图片,我们之前只是用想进程间通信来代替细节,现在我们根据已知的东西我们可以描绘出进程和Binder驱动程序的交互过程。

灵魂画手前来拜访

这里写图片描述
到目前为止前文中提到的概念,还有Binder引用对象Binder实体对象还没有介绍,我们可以发现Binder本地对象、Binder代理对象都是在用户空间中的,也就是C/S端进程中,也说了实际上进程间通信就是通过Binder代理对象发送进程间通信请求,通过Binder驱动等一系列操作,最后由Binder本地对象来处理请求,完成进程间通信。那么Client端的Binder代理对象时如何通过Binder驱动一系列处理找到Server端Binder本地对象的,只要弄明白了这里,我们就可以知道Binder驱动程序都做了什么。

其实Binder引用对象、Binder实体对象都是位于Binder驱动程序中的其实就是C语言写的数据结构,或者当成对象来理解也可以,Binder代理对象对应的就是Binder引用对象,Binder本地对象对应的就是Binder实体对象,而Binder引用对象,引用的就是Binder实体对象,Binder实体对象中有Binder引用对象的列表,为什么是对应列表呢?因为Binder进程间通信是支持并发的,一个Server端是可以向多个Client端提供服务的,所以用一个列表来保存Client端Binder代理对象对应的Binder本地对象,Binder引用对象有又相应的句柄值。

上图,鬼斧神工~~
这里写图片描述

这张图片添加了一些Binder引用对象和Binder实体对象和binder_proc结构体以及内核缓冲区的一些联系,显示了Binder引用对象和Binder实体对象在内核中的数据结构,但是也省略了很多的内容,比如通过进程间通信,让Server的本地对象处理逻辑,我需要先有Server端的代理对象,这个对象怎么来,怎么成为Server端,让内核中有对应我Binder对象的Binder实体对象等等!那么接下来我将通过Servicer Manager慢慢给大家揭开。

Service Manager上下文管理者

Servicer Manager是Binder进程间通信机制的核心组件之一,它扮演着Binder进程间通信机制上下文管理者的角色,同时负责管理系统中Service组件,并向Client组件提供获取Service代理对象的服务。

还记的上面的问题吗?我们可以发现上面的问题Servicer Manager都能帮我们解答,那么Servicer Manager是如何解决的呢,我们先来看Servicer Manager本身,Servicer Manager本身就是一个运行在一个进程当中,我这么说可能大家没什么反应,但是细思极恐啊,Servicer Manager在一个进程中,那么Client端和Server端都是在各自的进程中,我们无论是想通过Servicer Manager获取代理对象,还是将本地对象放到内核中,首先就要进行跨进程间的通信啊,也就是说我们要进行进程间通信,先要和Servicer Manager进行进程间通信,妈卖批啊!这就成了先有蛋还是先有鸡的问题了,懵逼啊!

还记得Client端和Server端的那个相对概念吗,无论你是打算将进程作为Client端还是Server,它们跟Service Manager通信时,相对于Servicer Manager来说都是Client端,而Servicer Manager进程自身就是提供服务的Server端,而Servicer Manager自身成为Server端,是Servicer Manager主动注册到Binder内核驱动中的,所以也可以说Servicer Manager是一个特殊的Server端(因为它不需要和管理者交互,因为它自己就是管理者)

Servicer Manager是由init进程负责启动的,init进程是Linux的第一个进程,所以说Servicer Manager启动的早啊,它这只鸡得赶紧先把自己孵出来啊。不然其他进程通信的时候就懵逼了啊。
Servicer Manager初始化时都做了些什么?
不废话,上图
这里写图片描述

Service Manager一共就干了三件事,而且都是大事,我们来分析一下

打开/dev/binder设备文件、映射到本进程的地址空间

  • 打开/dev/binder设备文件、映射到本进程的地址空间,干了这些事的Service Manager,Binder驱动程序会做些什么,相信大家通过前面的介绍应该知道,这里还是简单的唠叨一下,获得文件描述用来和驱动程序交互用,Binder驱动程序分配内核缓冲区,创建binder_proc结构体等。

注册成为管理者

  • 注册成为管理者,Service Manager想注册成为管理者,就必须要通过IO控制命令将自己注册到Binder驱动程序,IO控制命令可以理解成进程和内核的交互方法,既然Service Manager是一个特殊的Server端,如果想成为Server那么它就需要有一个自己Binder本地对象,我们知道创建对象在内存中都是有一个对应的内存地址值的,但是Service Manager的Binder本地对象是一个虚拟对象,它的地址值为0,请大家记住这个0的地址值,还记得我们之前说到的一个概念句柄值吗?我当时说句柄值和内存地址值息息相关。而Binder驱动程序就是通过binder_ioctl函数来处理IO控制命令的。简单的分析下注册流程。

首先,第一步中我们为Service Manager创建的结构体binder_proc,通过以前的图片我们可以知道,binder_proc中的threads是对应这Binder线程池的(threads红黑树,以线程PID为关键字来组织),Binder线程池中的线程就是处理和Binder驱动程序交互的,而描述这个Binder线程的结构体就是binder_thread,Binder驱动程序会查看这个threads有没有没对应的binder_thread结构体,因为ServiceManager是进行初始化注册,所以没有对应的结构体,Binder驱动程序就为Service Manager创建一个这样的结构体,当前线程就是Service Manager的主线程。

其次,Binder驱动程序会根据Service Manager的Binder本地对象(地址为0的虚拟对象),判读是否存在对应的Binder实体对象,还是因为进行初始化注册,没有对应的实体对象,就为其建一个Binder实体对象,保存在全局变量binder_context_mgr_node中。

最后,将这个新创建的实体对象加入结构体binder_proc的nodes成员变量里,然后继续返回用户空间,开始开启循环。

以上就是注册成为管理者的过程,总结一下就是,让驱动程序根据IO控制命令弄出Binder线程,Binder实体对象,然后和binder_proc这个结构体的相关成员变量关联起来。

循环等待Client进程请求

  • 循环等待Client进程请求
    这里将要出现一个新的结构体binder_writr_read,看名字可以看出是读写相关的,既然读写会出现输入和输出,binder_writr_read就包含输入缓存区和输出缓冲区,循环依然使用的是IO控制命令通过Binder线程(binder_thread结构体)以及inder_writr_read来和Binder驱动程序交互,依然使用的是binder_ioctl函数来处理IO控制命令,注册的时候我们已经创建了binder_thread结构体,所以就使用这个结构体,使用copy_from_use将从用户空间(Service Manager)传进来的结构体binder_writr_read拷贝进来。既然是交互,那么就有接收和返回,当进程同过IO控制命令传递binder_writr_read数据的时候,如何判断是让Binder驱动程序读数据,还是让Binder驱动程序写入数据返回给用户空间呢,这里就是通过判断binder_writr_read结构体的缓冲区,如果输入缓存区长度大于0,Binder驱动程序就使用binder_thread_write来处理数据,如果输出缓存区长度大于0,Binder驱动程序就使用binder_thread_read将需要处理的工作项写入到输出缓冲区,即将相应的返回协议写入该缓冲区中,以便进程在用户空间处理。工作项在内核中使用binder_transaction结构体来描述,工作项对应的数据就是binder_transaction_data结构,这里可能涉及到很多新的东西。我接下来会做一个比较易懂的总结。

总结

总结,循环我们可以想象我们工作中使用的轮询操作,就是不停的发送数据给Binder驱动程序,然后驱动程序会根据发送来的数据状态是将数据copy下来,还是去找看看有没有你需要做的工作返回给你做,总之一句话就是循环等待Client进程请求。这只是我的理解,当然真实情况比这个复杂的多。

老样子上图,其实这个图画的我心里也是没底的,感觉有些细节还是不是很清楚,简化了一下打开和映射的关联,如果不记得了可以看之前的图片。
这里写图片描述

到此我们就分析完了Service Manager进程是怎么工作的,怎么成为管理者的,又是怎么样来等待Client端请求的。

Service Manager代理对象的获取

Service Manager成为管理者了,也开启循环了,它已经准备好的一切,就等为Client进程服务了,相信大家对于Binder进程间通信心里已经有了一大概的框架,我们要想使用其他进程提供给我们的服务,就需要拿到Server端的Binder代理对象,我们要使用Service Manager的服务,当然也要获取它Binder代理对象,然后通过代理对象找到Binder引用对象然后找到Binder实体对象,然后最终交给Binder本地对象来处理。而中间这个找来找去的过程,离不开Binder内核驱动,但是Service Manager的代理对象比较特殊,它省去了和Binder内核驱动的交互,为什么,谁让Service Manager是管理者,谁让它是一个特殊的Server端,谁让他的Binder本地对象是一个虚拟对象,谁让它的地址值是0呢,这个时候Binder进程间通信库就发挥作用了,它就提供了直接获取Service Manager的Binder代理对象的函数defaultServiceManager。

进程通过使用Binder库的defaultServiceManager函数获取Service Manager的过程也比较简单,

1.判断进程中有没有Service Manager代理对象,有就直接用还获取个P啊,如果没有继续。

2.这个部分总共分为3小部分,获取Service Manager代理对象的过程,实际上就是进程间通信的过程,Client端和Service Manager进程间通信,凡是需要进程间通信的,都需要打开和映射/dev/binder设备文件这个过程,从上文我们可以知道干这件事的类就是ProcessState对象啊,然后在使用句柄值0创建一个Binder代理对象,接着将这个Binder代理对象调用模板函数封装成一个Service Manager代理对象。以上就是Service Manager代理对象的获取过程。

这里写图片描述

对于一般的Service组件来说,Client进程首先要通过Binder驱动程序来获得它的一个句柄值,然后根据这个句柄值创建Binder代理对象,最后将这个Binder对象封装成一个实现特定接口的代理对象。因为Service
Manager的句柄值恒为0,所以省略了和Binder驱动程序的交互过程。对于非Servicer Manager来说,获取句柄值就需要和内核进行交互,获取的办法将会在下文中提到。

Server端,服务的提供者

服务不是你想提,想提就能提,要想成为Server端,先得将Service组件注册到Service Manager中,接着启动Binder线程池来等待和处理Client进程的通信请求。
注册服务就是Binder库提供的addService函数,我这里说一下大体的流程,首先如果想成为Server端,就得先准备Binder本地对象,数据,Service名称,偏移数组(Binder本地对象的地址值)什么的,将这一部分东西通过一个Parcel的对象进行多次封装,因为我们已经分析的Service Manager代理对象的获取过程,这里就使用Service Manager代理对象同过之前分析Binder进程间通信库的那个交互流程将Parcel封装的数据(最终转换成flat_binder_object结构体)发送给Binder内核驱动程序,内核驱动程序就会根据Service Manager代理对象的句柄值0,我们可以确定是和Service Manager进行通信,然后就找到了全局变量binder_context_mgr_node,最后定位到了binder_proc结构体和内核缓冲区,将数据拷贝到Service Manager对应的内核缓冲区处理数据,同时我也可以根据Service Manager代理对象传入的数据分析出注册Service的进程对应的binder_proc结构体,由于是首次注册该binder_proc结构体的nodes对象一定为null,Binder驱动程序就会为创建一个Binder实体对象关联到nodes,然后再看它有没有引用对象,肯定没有啊实体都没有,哪里来的引用,接着创建引用对象(binder_ref),然后将这个对象(binder_ref)插入到Service Manager进程的binder_proc对象的一个链表中。最后启动Binder线程池将当前线程加入到Binder线程池中。

接下来结合代理对象的获取过程,简单的用一张图片来演示,其实关于这个过程的很多细节描述起来需要至少一篇很长的博客,而且我的理解也不见得有多么透彻,中间关于数据传输到内核中,内存相关,工作项事物相关的东西都没有提到,这里分享一篇博客,就是专门描述addService的这个过程,里面细节部分很详细了,http://blog.csdn.net/armwind/article/details/72852560结合这个博客再来看我的图片,就会发现是多么地辣眼睛~
这里写图片描述

Client端,服务的请求者,我要获取Server端的代理对象,让你服务我

Client端想要使用服务,当然需要获取Server端的代理对象了,只有获取到这个代理对象,才能通过代理对象来让Server端的本地对象给我服务,我们之前了解了Servicer Manager代理对象的获取过程,知道了Server端的代理对象和Servicer Manager代理对象的获取方式上还是有些不同的,关键就是句柄值的获取,Service Manager代理对象的句柄值恒为0,我们直接用0去获取就好了。还是上面的图
这里写图片描述
知道句柄值轻轻松松拿代理,而通过上文分析Server端的注册过程我们知道,Server端的Binder引用对象(desc变量就是分配的句柄值)最终通过一系列的交互插入到Service Manager的svclist列表中,所以我们Client端想要获取Server端的代理对象就需要去Service Manager中去找,而去Service Manager找的过程又是一个进程间通信过程,还是要获得Service Manager的代理对象,这里我就不分析Service Manager的代理对象获取过程了,不记得的看上图。

Binder进程间通信库提供了defaultServiceManager()函数来获取Service Manager的代理对象,提供了addServicer()函数来注册成为Server端,那么也同时提供了一个函数getService()来获取Server端的代理对象,在调用getService()来获取Server端的代理对象时,需要指定这个Service组件的名称(这是自然,你要告诉Servicer Mananger找那个提供服务的服务端啊),内核驱动最终联系到Service Manager进程,然后Service Manager进程根据名称数据,在和内核交互,从全局变量svclist中查找,找到指定的Server端句柄,内核根据句柄找到相应的引用对象,然后在根据引用对象找到实体对象,重新创建一个引用对象供客户端转换成代理对象。

通过上面的阅读我们可以发现客户端获取代理对象的大体流程如图:
这里写图片描述

至此我们就分析完了所有的流程,为什么我开篇要说我太懵懂了,其实分析到后半段已经力不从心了,Binder内核驱动程序远比我说的要复杂,这里面内核对象的生命周期管理(引用计数技术)都没有没分析,IO控制命令的基本上一带而过,很多东西其实并不像我图中箭头方法那么直接,都经过了和Binder驱动程序多次命令协议交互,根据命令协议进程也进行了相应的操作,功力有限实在只能分析这么些了。

文中如有错误,导致误导了大家,那么算你倒霉,欢迎各位大神积极留言,帮我指正。
最后文中内容很多引用自《Android系统源代码分情景分析》一书中的内容,也引用了一些其他博主的文字,如有侵权请告知!

终于写完了,灵魂画手的精神力已经被掏空~~~

  • 37
    点赞
  • 91
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
千里马8年Android系统及应用开发经验,曾担任过美国unokiwi公司移动端技术总监兼架构师,对系统开发,性能优化,应用高级开发有深入的研究,Android开源定制ROM Lineage的贡献者之一,国内首家线下开辟培训Android Framework课程,拥有2年的Android系统培训经验。成为腾讯课堂专业负责android framework课程分享第一人,致力于提高国内android Framework水平Android Framework领域内是国内各大手机终端科技公司需要的人才,应用开发者都对Android系统充满着好奇,其中的binder是重中之重,都说无binder无Android,binde是Android系统的任督二脉。课程水平循序渐进,由中级再到高级,满足各个层次水平的android开发者。1、灵活使用binder跨进程通信,在app端对它的任何api方法等使用自如2、可以单独分析android系统源码中任何binder部分,分析再也没有难度3、掌握binder驱动本质原理,及对应binder驱动怎么进行跨进程通信,及内存等拷贝方式数据等4、对binder从上层的java app端一直到最底层的内核binder驱动,都可以顺利理通5、针对系统开发过程中遇到的binder报错等分析方法,及binder bug案例学习6、针对面试官任何的binder问题都可以对答自如7、socket这种跨进程通信实战使用8、针对android源码中使用的socket源码轻松掌握9、android系统源码中最常见的socketpair中双向跨进程通信10、使用socket实现一个可以让app执行shell命令的程序
是的,Binder进程间通信(IPC)机制可以实现跨进程调用原进程的方法。 在Binder IPC中,存在客户端和服务端两个角色。客户端通过Binder代理(Proxy)向服务端发送请求,而服务端通过Binder存根(Stub)接收请求并执行相应的方法。 当客户端向服务端发送请求时,可以通过Binder IPC机制实现跨进程调用原进程的方法。具体步骤如下: 1. 客户端调用:客户端通过Binder代理(Proxy)调用服务端的方法,并传递相应的参数。 2. 数据传输:客户端将方法调用请求和参数打包成一个Parcel对象,并通过Binder IPC机制将该Parcel对象发送给服务端。 3. 服务端接收:服务端在接收到客户端的请求后,通过Binder存根(Stub)解析接收到的Parcel对象,并提取出方法调用请求和参数。 4. 方法执行:服务端根据解析得到的方法调用请求和参数,执行相应的方法。 5. 返回结果:在方法执行完毕后,服务端将返回的结果打包成一个Parcel对象,并通过Binder IPC机制将该Parcel对象发送回客户端。 6. 客户端接收:客户端在接收到服务端返回的结果后,通过Binder代理(Proxy)解析接收到的Parcel对象,提取出返回结果。 需要注意的是,跨进程调用原进程方法需要在服务端实现相应的方法,并且确保方法的调用参数和返回值能够正确地在进程间进行序列化和反序列化。此外,由于跨进程调用涉及到进程间的通信,因此需要注意线程安全和数据一致性的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值