4. Binder 内存模型:内核态与用户态的数据拷贝、mmap 映射机制、Binder 缓冲区管理
Binder IPC 的核心性能优势来源于其独特的内存模型。与传统的 IPC 机制(如管道、消息队列、Socket)不同,Binder 在数据传递过程中,仅需一次数据拷贝,而非两次。这一突破性设计依赖于 Linux 内核的 mmap 系统调用、内核态与用户态的内存映射,以及精细化的 Binder 缓冲区管理策略。
4.1 传统 IPC 的两拷贝困境
在理解 Binder 的优势之前,我们先回顾传统 IPC 的数据流。以 Socket 为例,当进程 A 向进程 B 发送数据时,数据需要经历以下路径:
- 用户态 → 内核态(第一次拷贝):进程 A 调用
send(),内核将数据从进程 A 的用户空间缓冲区拷贝到内核空间的 Socket 缓冲区。 - 内核态 → 用户态(第二次拷贝):进程 B 调用
recv(),内核将数据从内核空间的 Socket 缓冲区拷贝到进程 B 的用户空间缓冲区。
这种两次拷贝机制,在传输大量数据时,会显著消耗 CPU 资源并增加延迟。
4.2 Binder 的一次拷贝魔法:mmap 映射
Binder 通过 内存映射(mmap) 技术,在内核空间和接收进程的用户空间之间建立了一块共享的虚拟内存区域。其核心思想是:发送进程将数据拷贝到内核空间后,接收进程可以直接通过映射关系访问该数据,无需再次拷贝。
4.2.1 mmap 映射流程
- 服务端注册: 服务端进程(如 System Server)在初始化 Binder 时,调用
mmap()系统调用,向 Binder 驱动申请一块内核虚拟地址空间。 - 内核分配: Binder 驱动在内核空间分配物理内存页,并将这些物理页映射到服务端进程的用户空间虚拟地址。
- 映射建立: 此时,服务端进程的用户空间地址与内核空间地址指向同一块物理内存。这块内存就是后续接收数据的“Binder 缓冲区”。
4.3 Binder 缓冲区管理
Binder 驱动在内核中负责管理所有用于 IPC 的缓冲区。这些缓冲区并非无限大,其大小由服务端进程在 mmap() 时指定。Binder 驱动采用了一套高效的分配与回收机制,以避免内存碎片和竞争。
4.3.1 缓冲区结构
每个 Binder 进程在内核中都有一个对应的 binder_proc 结构体,其中包含一个 buffers 红黑树,用于管理所有已分配的缓冲区。每个缓冲区由 binder_buffer 结构体描述:
struct binder_buffer {
struct list_head entry; // 链表节点,用于空闲列表或已分配列表
struct rb_node rb_node; // 红黑树节点,用于按地址快速查找
unsigned free:1; // 标记位:是否为空闲缓冲区
unsigned allow_user_free:1;
unsigned async_transaction:1;
struct binder_transaction *transaction; // 指向所属的事务
struct binder_node *target_node;
size_t data_size; // 数据大小
size_t offsets_size; // 偏移数组大小
void *user_data; // 用户空间地址
};
4.3.2 分配策略:最佳适应算法
Binder 驱动在分配缓冲区时,采用 最佳适应(Best Fit) 算法,从空闲缓冲区列表中查找大小最合适的块,以减少内存碎片。
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 查找空闲列表 | 遍历 binder_free_buffers 链表,寻找大小 >= 请求大小的空闲缓冲区。 |
| 2 | 选择最佳块 | 在所有满足条件的空闲块中,选择大小最接近请求大小的块(最小剩余空间)。 |
| 3 | 分割(可选) | 如果选中的空闲块远大于请求大小,则将其分割为两部分:一部分用于分配,另一部分作为新的空闲块。 |
| 4 | 更新状态 | 将分配的缓冲区从空闲列表移除,加入已分配列表,并设置 free = 0。 |
4.3.3 回收与碎片整理
当事务完成,缓冲区被释放时,Binder 驱动会检查相邻的缓冲区是否也为空闲。如果是,则进行 合并(Coalescing) 操作,将多个小的空闲块合并成一个大的空闲块,从而缓解内存碎片化问题。
4.4 数据拷贝的完整路径
结合上述机制,我们以一个完整的 Binder 调用为例,梳理数据从发送方到接收方的内存路径:
- 发送方准备: 进程 A 将待发送的数据打包到
Parcel对象中,该对象内部持有用户空间的数据指针。 - 系统调用: 进程 A 通过
ioctl()进入内核,触发 Binder 驱动处理BC_TRANSACTION命令。 - 内核拷贝: Binder 驱动在接收方(进程 B)的预映射缓冲区中,分配一块大小合适的物理内存。然后,驱动使用
copy_from_user()将进程 A 用户空间的数据,直接拷贝到这块内核物理内存中。 - 映射访问: 由于进程 B 已经通过
mmap将这块物理内存映射到了自己的用户空间,因此进程 B 的用户空间指针可以直接访问到这些数据。 - 唤醒接收方: Binder 驱动将进程 B 从等待队列中唤醒,进程 B 从
ioctl()返回,并从映射地址读取数据。
4.5 关键数据结构与参数
| 参数/结构体 | 作用 | 默认值/说明 |
|---|---|---|
binder_proc->buffer |
指向 mmap 映射的内核缓冲区起始地址 | 由驱动在 mmap 时分配 |
binder_proc->buffer_size |
映射的缓冲区总大小 | 通常为 1MB - 4MB,由进程在 mmap 时指定 |
binder_proc->free_buffers |
空闲缓冲区链表头 | 用于快速分配 |
binder_proc->allocated_buffers |
已分配缓冲区红黑树根节点 | 用于快速查找和释放 |
BINDER_MAX_TRANSACTION_SIZE |
单次事务允许的最大数据大小 | 通常为 64KB - 1MB,受限于 buffer_size |
ashmem(匿名共享内存)或 FileDescriptor 传递,以绕过 Binder 缓冲区的大小限制。