4. Binder 内存管理机制:binder_alloc 结构体、物理页分配与映射、binder_update_page_range、内存碎片化问题
Binder 驱动在内核空间与用户空间之间高效传递数据,其核心依赖于一套精细的内存管理机制。与传统的 socket 或共享内存不同,Binder 通过 binder_alloc 结构体管理每个进程的缓冲区池,采用 按需分配物理页 和 内核态映射 的方式,在保证数据拷贝效率的同时,最大限度减少内存占用。本章将深入剖析 Binder 内存管理的核心数据结构、物理页分配与映射流程、关键函数 binder_update_page_range 的实现,以及长期运行中可能遇到的内存碎片化问题。
核心要点: Binder 内存管理的核心目标是“零拷贝”与“按需分配”。通过在内核空间分配物理页,并同时映射到内核虚拟地址空间和用户虚拟地址空间,实现一次数据拷贝即可完成跨进程传输。
4.1 binder_alloc 结构体:进程级缓冲区管理器
每个使用 Binder 通信的进程,在内核中都有一个对应的 binder_proc 结构体,其中包含一个 binder_alloc 成员,专门负责管理该进程的 Binder 缓冲区。这个结构体是 Binder 内存管理的基石。
// 位于 kernel/drivers/android/binder_alloc.h
struct binder_alloc {
struct mutex mutex; // 保护该结构体的互斥锁
struct vm_area_struct *vma; // 用户态映射的虚拟内存区域
struct mm_struct *vma_vm_mm; // 拥有该 vma 的进程的 mm_struct
void *buffer; // 内核虚拟地址起始点
struct list_head buffers; // 已分配的缓冲区链表
struct list_head free_buffers; // 空闲缓冲区链表
struct list_head allocated_buffers; // 已分配但未映射的缓冲区链表
size_t buffer_size; // 缓冲区总大小(默认 4MB)
size_t free_async_space; // 剩余异步事务空间
struct page **pages; // 物理页描述符数组
size_t page_size; // 页面大小(通常 4KB)
size_t pages_allocated; // 已分配的物理页数量
size_t pages_high_watermark; // 已分配页面的高水位标记
bool oneway_spam_detected; // 单向垃圾检测标志
};
关键字段解析:
| 字段 | 类型 | 作用说明 |
|---|---|---|
vma |
struct vm_area_struct * |
指向用户态进程的虚拟内存区域,用于建立用户态与内核态的映射关系 |
buffer |
void * |
内核虚拟地址的起始点,所有 Binder 缓冲区操作都基于此地址偏移 |
buffers |
struct list_head |
已分配缓冲区的链表头,用于跟踪所有正在使用的缓冲区 |
free_buffers |
struct list_head |
空闲缓冲区的链表头,按地址排序,用于快速分配 |
pages |
struct page ** |
物理页描述符指针数组,每个元素指向一个已分配的物理页 |
buffer_size |
size_t |
缓冲区总大小,默认 4MB,可通过 /sys/module/binder/parameters/binder_alloc_size 调整 |
设计意图:
binder_alloc 将物理页管理、虚拟地址映射、缓冲区分配与回收整合在一个结构体中,使得 Binder 驱动可以像用户态内存分配器一样高效管理内核缓冲区。
4.2 物理页分配与映射:按需分配策略
Binder 驱动不会在进程创建时一次性分配所有物理页,而是采用 按需分配(on-demand allocation) 策略。当进程发起 Binder 事务时,驱动根据实际数据大小,逐步分配物理页并建立映射关系。
4.2.1 分配流程
- 触发条件: 用户态调用
ioctl(BC_TRANSACTION)或ioctl(BC_REPLY),驱动进入binder_ioctl_write_read处理流程。 - 缓冲区分配: 调用
binder_alloc_new_buf从空闲链表中查找合适大小的空闲块。 - 物理页检查: 检查目标缓冲区所在的内存范围是否已有物理页映射。如果没有,则调用
binder_update_page_range分配物理页。 - 建立映射: 新分配的物理页同时映射到内核虚拟地址空间和用户虚拟地址空间(通过
vma)。 - 数据拷贝: 用户态数据通过
copy_from_user拷贝到内核缓冲区,接收方通过映射直接读取。
4.2.2 映射关系
每个 Binder 缓冲区在内核中拥有两个虚拟地址映射:
- 内核虚拟地址: 起始于
binder_alloc.buffer,驱动通过此地址读写数据。 - 用户虚拟地址: 通过
binder_alloc.vma映射到用户进程的地址空间,接收方进程可以直接访问。
关键约束: 物理页一旦分配,在进程退出或显式释放缓冲区之前不会回收。这意味着即使缓冲区空闲,其占用的物理页仍然保留,这是 Binder 性能优化与内存占用之间的权衡。
4.3 binder_update_page_range:核心函数深度解析
binder_update_page_range 是 Binder 内存管理的核心函数,负责在指定地址范围内分配或释放物理页。该函数在 binder_alloc.c 中实现,是理解 Binder 内存分配与回收的关键。
4.3.1 函数原型与参数
static int binder_update_page_range(struct binder_alloc *alloc, bool allocate,
void *start, void *end)
{
// allocate: true 表示分配物理页,false 表示释放物理页
// start/end: 需要操作的虚拟地址范围
// 返回值: 0 成功,负数表示错误码
}
4.3.2 分配路径(allocate = true)
| 步骤 | 操作 | 关键代码 |
|---|---|---|
| 1 | 遍历地址范围内的每个页面 | for (addr = start; addr < end; addr += PAGE_SIZE) |
| 2 | 检查物理页是否已分配 | if (alloc->pages[(addr - alloc->buffer) / PAGE_SIZE]) |
| 3 | 调用 alloc_page() 分配物理页 |
struct page *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO); |
| 4 | 建立内核虚拟地址映射 | map_kernel_range_noflush(addr, PAGE_SIZE, PAGE_KERNEL, page); |
| 5 | 建立用户虚拟地址映射 | vm_insert_page(alloc->vma, user_addr, page); |
| 6 | 更新 pages_allocated 计数 |
alloc->pages_allocated++; |
4.3.3 释放路径(allocate = false)
| 步骤 | 操作 | 关键代码 |
|---|---|---|
| 1 | 遍历地址范围内的每个页面 | for (addr = start; addr < end; addr += PAGE_SIZE) |
| 2 | 检查物理页是否存在 | if (!alloc->pages[(addr - alloc->buffer) / PAGE_SIZE]) |
| 3 | 解除用户态映射 | zap_page_range(alloc->vma, user_addr, PAGE_SIZE); |
| 4 | 解除内核态映射 | unmap_kernel_range(addr, PAGE_SIZE); |
| 5 | 释放物理页 | __free_page(page); |
| 6 | 更新 pages_allocated 计数 |
alloc->pages_allocated--; |
性能关键点:
binder_update_page_range 在分配路径中使用了 __GFP_ZERO 标志,确保分配的物理页被清零,避免信息泄露。同时,vm_insert_page 比传统的 remap_pfn_range 更轻量,因为它不需要建立新的页表项,而是直接插入已分配的页面。