1. Binder整体通信架构
Binder 是 Android 系统中跨进程通信(IPC)的核心机制,采用经典的客户端‑服务器(C/S)模型,并引入内核级 Binder 驱动作为通信枢纽。本章从整体架构出发,依次分析 C/S 模式、驱动角色、进程与线程模型、mmap 内存映射以及传输数据包结构,为后续 AIDL 接口编写和底层优化打下基础。
1.1 C/S 模式
Binder 通信包含三类角色:Client(发起请求的进程)、Server(提供服务的进程)和 Binder Driver(内核中的中转模块)。此外,Service Manager(servicemanager)作为全局注册表,负责管理服务名称与 Binder 实体的映射关系。Client 通过服务名向 Service Manager 查询目标 Binder 引用,随后通过 Binder Driver 发送调用数据,最终由 Server 处理并返回结果。
下面给出一个最简的 Native 层服务注册与获取示例(基于 libbinder):
// Service端:注册服务
sp<IServiceManager> sm = defaultServiceManager();
sm->addService(String16("my.example"), new ExampleService());
// Client端:获取服务
sp<IServiceManager> sm = defaultServiceManager();
sp<IBinder> binder = sm->getService(String16("my.example"));
sp<IExample> service = interface_cast<IExample>(binder);
// 调用远程方法
service->doSomething();
上述代码通过 defaultServiceManager() 获取 Service Manager 代理,再调用 addService/getService 完成服务注册与发现,整个流程依赖 Binder Driver 在内核中透明地转发数据。
1.2 Binder 驱动角色
Binder 驱动(/dev/binder)是一个字符设备驱动,位于 Linux 内核中。其主要职责包括:
- 设备管理:通过
binder_open为每个进程分配文件描述符和内部数据结构(binder_proc)。 - 命令处理:通过
binder_ioctl处理BINDER_WRITE_READ、BINDER_VERSION等命令,实现数据收发。 - 内存映射:通过
binder_mmap为进程分配内核缓冲区,用于数据的一次拷贝。 - 线程管理:维护线程池状态,避免死锁。
可以通过下列命令检查 Binder 设备节点的存在和版本:
adb shell ls -l /dev/binder
adb shell cat /proc/binder/proc/stats # 仅限 root 或有权限设备
adb shell dmesg | grep binder
/proc/binder/ 目录提供了丰富的 Binder 状态信息,包括每个进程的 Binder 线程数、事务统计等。
1.3 进程与线程模型
Binder 采用 线程池 模型来控制并发。每个进程在首次打开 Binder 驱动后,会自动创建一个线程池,默认最多 16 个线程(由 BINDER_MAX_THREADS 控制)。当 Client 发起调用时,Binder 驱动会从目标进程的线程池中分配一个空闲线程来执行请求;若池中没有空闲线程,则阻塞直到有线程释放。
下表对比了 Binder 线程与普通工作线程的关键区别:
| 特性 | Binder 线程 | 普通工作线程 |
|---|---|---|
| 创建时机 | 由 Binder 驱动自动管理 | 由应用代码主动创建 |
| 数量上限 | 默认 16(可通过内核参数调整) | 取决于系统资源 |
| 执行实体 | 处理 Binder 事务(onTransact) | 任意 CPU 任务 |
| 阻塞场景 | 线程池满载时事务异步等待 | 由开发者自行控制 |
在 Native 层可通过 ProcessState 显式启动线程池:
#include <binder/ProcessState.h>
#include <binder/IServiceManager.h>
int main() {
sp<ProcessState> proc(ProcessState::self());
proc->startThreadPool(); // 启动 Binder 线程池
// ... 注册服务或监听调用 ...
IPCThreadState::self()->joinThreadPool();
return 0;
}
startThreadPool(),则 Binder 驱动无法为它分配线程,Client 的请求会一直阻塞。在 Android 的 Java 层(Service 组件)中,系统会自动管理线程池,无需手动启动。
1.4 mmap 内存映射(一次拷贝)
传统 IPC(如 Unix Domain Socket)需要两次数据拷贝:发送端用户空间 → 内核空间 → 接收端用户空间。Binder 通过 mmap 将接收进程的用户空间缓冲区映射到内核空间,使得发送端只需将数据从自己的用户空间拷贝到内核空间(实际上直接写入接收端映射的物理页),完成 一次拷贝,大幅提升性能。
Binder 驱动内部调用 mmap 的简化示意:
// 伪码:Binder mmap 流程
int fd = open("/dev/binder", O_RDWR);
int size = 128 * 1024; // 默认 128KB
void *addr = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) perror("mmap");
close(fd);
实际内核实现中,Binder 驱动会维护一个 binder_buffer 链表,每个映射得到的内核缓冲区可被多次复用。下表对比了传统 IPC 与 Binder 的数据拷贝次数:
| 通信方式 | 用户→内核 | 内核→用户 | 总拷贝数 |
|---|---|---|---|
| Unix Domain Socket | 1 次 | 1 次 | 2 次 |
| Binder IPC | 1 次(直接到目标映射区) | 0 次(目标进程直接读取映射区) | 1 次 |
1.5 传输数据包结构
Binder 通信通过 ioctl(fd, BINDER_WRITE_READ, &bwr) 进行,其中 bwr 是 struct binder_write_read 结构体。写入和读取的数据由一组 命令 和数据组成,核心的数据单元是 struct binder_transaction_data。
下面是从 Binder 驱动源码中提取的关键结构字段(简化版):
struct binder_write_read {
signed long write_size; // 写入缓冲区总字节数
signed long write_consumed; // 已消耗的写入字节数
unsigned long write_buffer; // 写入缓冲区地址
signed long read_size; // 读取缓冲区总字节数
signed long read_consumed; // 已消耗的读取字节数
unsigned long read_buffer; // 读取缓冲区地址
};
struct binder_transaction_data {
union {
__u32 handle; // 目标 Binder 实体的句柄
void *ptr; // 内核内部指针
} target;
void *cookie; // Server 方附加数据
__u32 code; // 方法编号(如 AIDL 中的 transaction 码)
__u32 flags; // 事务标志(如 TF_ONE_WAY)
__u32 sender_pid; // 发送者 PID
__u32 sender_euid; // 发送者 UID
__u32 data_size; // 数据区大小(字节)
__u32 offsets_size; // 对象偏移表大小
void *data; // 指向实际数据(用户空间指针)
};
实际通信时,data 指向的内存包含两部分:扁平数据(如整数、字符串)和 Binder 对象引用(偏移表标记了哪些位置需要驱动转换句柄)。offsets_size 用来指导驱动处理对象序列化。
| 字段 | 说明 | 典型值/范围 |
|---|---|---|
target.handle | 目标 Binder 的句柄(Client 侧) | 0 为 Service Manager |
code | 方法标识,由 AIDL 自动分配 | 0 ~ 65535 |
data_size | 数据总长度(字节) | 通常 < 256KB |
offsets_size | Binder 对象偏移表大小 | 必须为 4 的倍数 |
data_size 超过 Binder 驱动设置的缓冲区大小(默认 1MB,部分设备限制为 256KB),驱动会返回 BINDER_STATUS_OUT_OF_MEMORY。这是 AIDL 接口设计中必须考虑的内存限制。