第一章 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 在内核中透明地转发数据。

注意:Service Manager 本身也是一个 Binder Server,其 handle 在 Binder 协议中固定为 0。Client 无需通过驱动查找,直接向 handle 0 发送请求即可获得其他服务的引用。
Binder整体通信架构图 用户空间 Client Server Service Manager 内核空间 Binder Driver mmap缓冲区 调用请求 转发请求 注册/查询 数据拷贝 说明:Client/Server/Service Manager 均在用户空间;Binder Driver 和 mmap 缓冲区位于内核空间。数据仅需一次拷贝(用户→内核→用户)。

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
调试提示:在 Android 原生模拟器或已 root 的真机上,/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;
}
注意:若 Server 进程没有调用 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 Socket1 次1 次2 次
Binder IPC1 次(直接到目标映射区)0 次(目标进程直接读取映射区)1 次
性能提醒:一次拷贝不仅减少延迟,还降低了系统调用次数。对于频繁的跨进程小数据量通信(如 SystemService 调用),Binder 的性能优势非常明显。

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_sizeBinder 对象偏移表大小必须为 4 的倍数
常见错误:如果 data_size 超过 Binder 驱动设置的缓冲区大小(默认 1MB,部分设备限制为 256KB),驱动会返回 BINDER_STATUS_OUT_OF_MEMORY。这是 AIDL 接口设计中必须考虑的内存限制。