Android 浅析 Binder 机制 基础 (三)

前言

Linus Benedict Torvalds : RTFSC – Read The Funning Source Code

Binder 架构

Android 的Binder机制就是一个C/S架构,整个机制包括:
1、ServiceManager;
2、服务端Server;
3、客户端client;
4、服务代理proxy;
5、Binder驱动;

Android Binder 系统架构图

1
2
3
4
5
6
7
8
9
st=>operation: 原生 Binder 客户端/服务端
op=>operation: 原生 Binder 框架
op1=>operation: Binder 核心库
op2=>operation: Binder Adapter
ProcessState.cpp/IPCThreadState.cpp
op3=>operation: Binder 驱动
e=>end
st->op->op1->op2->op3

工作流程

应用层工作流程

  1. 客户端首先获取服务端的代理对象^footnote
  2. 客户端通过调用服务端代理的方式向服务端发送请求。
  3. 代理对象将用户请求通过Binder驱动发送到服务器进程。
  4. 服务器进程处理用户请求,并通过Binder驱动返回处理结果给客户端的服务端代理对象。
  5. 客户端收到服务端的返回结果。

Binder 内存结构图:
binder 内存结构图

系统层工作流程

具体Binder工作流程:

  1. Linux系统启动,Binder driver开始工作,注册设备文件/dev/binder
  2. Android系统启动,ServiceManager开始工作,向Binder driver注册ContextManager,这个过程中,Binder driver中创建了第一个binder_node(注意:ServiceManager在内核空间有binder_node,但是在用户空间没有对应的BBinder)
  3. Service进程启动,在用户空间创建了BBinder,并向ServiceManager注册服务,注册的过程中,Binder driver为Service在内核空间创建了binder_node
  4. Client启动,向ServiceManager请求指定Serivce的Handle,这个过程中,Binder driver为Client在内核空间创建了handle对应的binder_ref
  5. Client根据ServiceManager提供的handle,向Service请求服务

完整Binder工作流程

  1. Client进程在用户态调用BpBinder的接口
  2. BpBinder调用ioctl向dev/binder文件写入数据,数据中包含自己的handle
  3. 进程进入到核心态,执行binder driver的代码,先查找handle关联的binder_ref
  4. 进一步根据binder_ref关联的binder_node,确定目标Service进程即(binder_proc)
  5. 把数据保存到目标进程(或目标线程)的todo队列,这时的数据中添加了当前线程(binder_thread)信息,并唤醒Service
  6. Client开始等待回复
  7. Service进程内的binder driver被唤醒,缓存client发送过来的数据
  8. Service进程返回用户态,调用BBinder到接口,开始处理请求
  9. 请求处理结束,调用ioctl,回复Client的请求
  10. 进程进入核心态,通过步骤7缓存的数据,确定请求发起线程(步骤五中,数据内添加了请求发起线程的信息)
  11. 把回复数据保存到client进程请求线程的todo队列中,并唤醒Client进程中的请求线程
  12. Service进程继续等待请求
  13. Client进程内的请求线程被唤醒,返回用户态
  14. 返回到用户态,client进程处理回复数据

服务

Native 服务

实际就是在C++空间完成的服务。主要指系统开始初始化时通过Init.rc脚本启动的服务。

Android 服务

就是在JVM空间完成的服务,也使用Navite框架,但服务主体存在于Android空间。Android服务是在第二阶段Init2时简历的服务。

Init 空间服务

主要用于完成属性设置,采用Sockey方式通信。

Binder Adapter

实际上是对Binder驱动的封装,用于完成Binder库与Binder 内核驱动的交互。主要实现包括:IPCThreadState和ProcessState。

PorcessState:包含通信细节,利用open_binder打开Linux设备dev\binder。通过ioctrl建立基本的通信框架。
IPCThreadState:主要负责Binder数据读取、写入和请求处理框架。
每个进程只有一个ProcessState对象。
每个线程都会有一个IPCThreadState对象。

PorcessState

ProcessState的作用是维护当前进程中所有Service代理(BpBinder对象)。一个客户端进程可能需要多个Service的服务,这样可能会创建多个Service代理(BpBinder对象),客户端进程中的ProcessState对象将会负责维护这些Service代理。

ProcessState负责打开/driver/binder并将句柄记录在变量中。真正使用Binder设备句柄的是IPCThreadState。

IPCThreadState

有三个重要的函数功能。

  1. talkWithDriver() 负责读取和写入功能
  2. executeCommand() 负责处理请求功能
  3. joinThreadPool() 负责循环结构

不管是客户端进程还是服务端进程,都需要IPCThreadState来与Binder设备通信。
客户端:通过服务代理对象BpBinder调用transact函数把请求写入Binder。
服务端:完成初始化后就进入循环状态等待客户端请求,Service进程调用它的IPCThreadState对象的joinThreadPool方法轮询Binder设备。

BBinder

BBinder是server端用于接收消息的通道。
transact方法:
当IPCThreadState实例收到BD消息时,通过BBinder的transact的方法将其传递给它的子类BnSERVICE的onTransact函数执行server端的操作。

BpBinder

BpBinder是client端创建的用于消息发送的代理。主要功能是负责client向BD发送调用请求的数据。
transact方法:
向IPCThreadState实例发送消息,通知其有消息要发送给BD。

Binder 内存管理

传统 IPC 方式

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

Binder 方式

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只需一次拷贝的‘秘密’。