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 驱动可以像用户态内存分配器一样高效管理内核缓冲区。
binder_alloc 结构体与缓冲区布局 binder_alloc mutex vma → 用户态 VMA buffer → 内核虚拟地址 buffers (链表头) free_buffers (链表头) allocated_buffers (链表头) buffer_size = 4MB pages → 物理页数组 page_size = 4KB pages_allocated pages_high_watermark free_async_space oneway_spam_detected Binder 缓冲区池 (4MB) 已分配 (A) 已分配 (B) 空闲 (C) 空闲 (D) 物理页映射 (pages 数组) 页 0 页 1 页 2 未分配 未分配 页 N 链表组织 free_buffers 链表 空闲 (C) 空闲 (D) allocated_buffers 链表 已分配 (A) 已分配 (B)

4.2 物理页分配与映射:按需分配策略

Binder 驱动不会在进程创建时一次性分配所有物理页,而是采用 按需分配(on-demand allocation) 策略。当进程发起 Binder 事务时,驱动根据实际数据大小,逐步分配物理页并建立映射关系。

4.2.1 分配流程

  1. 触发条件: 用户态调用 ioctl(BC_TRANSACTION) 或 ioctl(BC_REPLY),驱动进入 binder_ioctl_write_read 处理流程。
  2. 缓冲区分配: 调用 binder_alloc_new_buf 从空闲链表中查找合适大小的空闲块。
  3. 物理页检查: 检查目标缓冲区所在的内存范围是否已有物理页映射。如果没有,则调用 binder_update_page_range 分配物理页。
  4. 建立映射: 新分配的物理页同时映射到内核虚拟地址空间和用户虚拟地址空间(通过 vma)。
  5. 数据拷贝: 用户态数据通过 copy_from_user 拷贝到内核缓冲区,接收方通过映射直接读取。

4.2.2 映射关系

每个 Binder 缓冲区在内核中拥有两个虚拟地址映射:

  • 内核虚拟地址: 起始于 binder_alloc.buffer,驱动通过此地址读写数据。
  • 用户虚拟地址: 通过 binder_alloc.vma 映射到用户进程的地址空间,接收方进程可以直接访问。
关键约束: 物理页一旦分配,在进程退出或显式释放缓冲区之前不会回收。这意味着即使缓冲区空闲,其占用的物理页仍然保留,这是 Binder 性能优化与内存占用之间的权衡。
物理页分配与映射流程 用户空间 进程 A 用户 VMA 进程 B 用户 VMA ioctl(BC_TRANSACTION) copy_from_user copy_to_user 内核空间 binder_alloc.buffer 内核虚拟地址 binder_update_page_range alloc_page() vm_insert_page() 物理内存 物理页 0 物理页 1 物理页 2 未分配 未分配 未分配

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 更轻量,因为它不需要建立新的页表项,而是直接插入已分配的页面。
binder_update_page_range 执行流程 binder_update_page_range allocate? 分配路径 (allocate=true) 遍历地址范围 alloc_page(GFP_KERNEL) map_kernel_range_noflush vm_insert_page 释放路径 (allocate=false) ← 上一章 📖 返回目录 下一章 →