binder_alloc内存分配器
Binder transaction 的接收空间不是一个普通的全局内核堆。每个 Binder 进程先拥有一段自己的 Binder VMA,binder_alloc 在这段地址空间上维护 buffer 边界;发送事务时,驱动从空闲区间挑出一段地址,在覆盖该区间的页上安装物理页,再把 payload 复制进去。接收进程看到的 binder_transaction_data.data.ptr.buffer 就是这段进程私有 VMA 中的地址。
本文面向已经读过 Binder内存映射、Binder事务发送 和 BR事务返回 的读者。BD014 解释 VMA 如何建立和关闭,BD017 解释事务怎样调用 allocator,本篇专门回答 allocator 内部怎样选择 buffer、为什么异步事务只有一半地址空间、页何时安装、释放后怎样合并以及内存压力下怎样解除用户映射。
本文不把 binder_buffer 的事务字段、对象翻译和 BC_FREE_BUFFER 协议重新展开;那些字段如何参与事务生命周期分别由 BD017、BD022 和后续 BD025 负责。这里的“释放”首先指 allocator 的地址区间和页状态变化,事务对象清理由上层驱动调用者负责。
下面这张图把三个容易混淆的层次分开:free_buffers/allocated_buffers 管理的是虚拟地址区间,pages[] 管理区间覆盖的物理页指针,binder_freelist 管理已经暂时不被活动 buffer 使用的页。一次 buffer 释放可以同时改变三层,但三层并不在同一个时刻完成。
1. 两棵树
1.1 分配器状态
源码文件:kernel/common/drivers/android/binder_alloc.h
相关类型:binder_alloc
struct binder_alloc {
struct mutex mutex;
struct mm_struct *mm;
unsigned long vm_start;
struct list_head buffers;
struct rb_root free_buffers;
struct rb_root allocated_buffers;
size_t free_async_space;
struct page **pages;
struct list_lru *freelist;
size_t buffer_size;
int pid;
size_t pages_high;
bool mapped;
bool oneway_spam_detected;
};binder_alloc 嵌在 binder_proc 中,owner 是一个打开 Binder 设备并完成 mmap 的进程。buffers 是按用户地址排列的双向链表,负责回答“某个区间的前一个和后一个是谁”;两棵红黑树承担不同查询:
| 索引 | 排序键 | 主要消费者 |
|---|---|---|
free_buffers | buffer 实际区间大小 | binder_alloc_new_buf_locked() 做 best-fit |
allocated_buffers | user_data 起始地址 | binder_alloc_prepare_to_free() 校验用户释放地址 |
buffers | 用户地址链表顺序 | 前后邻居查找、边界计算、合并 |
同一个 binder_buffer 的 rb_node 在空闲和已分配状态之间移动,不会同时出现在两棵树里。mutex 保护这些树、链表、async 配额和页数组元数据;页安装还用 release/acquire 发布 alloc->pages[index],避免另一个线程看到半完成的页状态。
1.2 Buffer元数据
源码文件:kernel/common/drivers/android/binder_alloc.h
相关类型:binder_buffer、binder_shrinker_mdata
struct binder_buffer {
struct list_head entry;
struct rb_node rb_node;
unsigned free:1;
unsigned clear_on_free:1;
unsigned allow_user_free:1;
unsigned async_transaction:1;
unsigned oneway_spam_suspect:1;
unsigned debug_id:27;
struct binder_transaction *transaction;
struct binder_node *target_node;
size_t data_size;
size_t offsets_size;
size_t extra_buffers_size;
unsigned long user_data;
int pid;
};
struct binder_shrinker_mdata {
struct list_head lru;
struct binder_alloc *alloc;
unsigned long page_index;
};binder_buffer 描述的是一个地址区间及其事务关联,不是物理页本身。页上的 page_private 保存 binder_shrinker_mdata,这样全局 list_lru 回调能从一个 LRU 节点反查所属 allocator 和页索引。页回收必须先确认该页不再被活动 buffer 使用;区间 free 与页进入 LRU 是两个相关但不相同的状态。
2. 初始化发布
2.1 空间建立
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_mmap_handler()
if (unlikely(vma->vm_mm != alloc->mm)) {
ret = -EINVAL;
goto err_invalid_mm;
}
mutex_lock(&binder_alloc_mmap_lock);
if (alloc->buffer_size) {
ret = -EBUSY;
goto err_already_mapped;
}
alloc->buffer_size = min_t(unsigned long,
vma->vm_end - vma->vm_start, SZ_4M);
mutex_unlock(&binder_alloc_mmap_lock);
alloc->vm_start = vma->vm_start;
alloc->pages = kvcalloc(
alloc->buffer_size / PAGE_SIZE,
sizeof(alloc->pages[0]), GFP_KERNEL);allocator 只接受与 binder_alloc_init() 保存的 mm 相同的 VMA,并把请求长度限制到 SZ_4M。这里的 pages 只是 struct page * 数组,数组分配不等于物理页全部分配;真正的 page object 在某个 buffer 首次覆盖该页时创建。
2.2 初始空闲区
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_mmap_handler()、binder_insert_free_buffer()
buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
if (!buffer)
goto err_alloc_buf_struct_failed;
buffer->user_data = alloc->vm_start;
list_add(&buffer->entry, &alloc->buffers);
buffer->free = 1;
binder_insert_free_buffer(alloc, buffer);
alloc->free_async_space = alloc->buffer_size / 2;
binder_alloc_set_mapped(alloc, true);整个 VMA 最初只有一个 free buffer。free_async_space 从总地址空间的一半开始,而不是单独再分配一块 oneway 区域;它是对 free_buffers 同一地址空间的逻辑配额。只有初始 buffer、页数组和配额都准备好后才发布 mapped = true,因此分配入口可以把 false 解释为“尚未可用”。
2.3 关闭屏障
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_vma_close()、binder_alloc_is_mapped()
void binder_alloc_vma_close(struct binder_alloc *alloc)
{
binder_alloc_set_mapped(alloc, false);
}
static inline bool binder_alloc_is_mapped(
struct binder_alloc *alloc)
{
return smp_load_acquire(&alloc->mapped);
}VMA close 先撤销 mapped 发布状态,使新的 binder_alloc_new_buf() 返回 -ESRCH,也使页安装和 shrinker 不再把旧地址当作有效 Binder 映射。已经存在的 buffer、事务和页不会因为这个布尔值自动消失,后续 deferred release 仍需逐项清理。
3. 尺寸清洗
3.1 三段长度
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:sanitized_size()、binder_alloc_new_buf()
static inline size_t sanitized_size(
size_t data_size,
size_t offsets_size,
size_t extra_buffers_size)
{
size_t total, tmp;
tmp = ALIGN(data_size, sizeof(void *)) +
ALIGN(offsets_size, sizeof(void *));
if (tmp < data_size || tmp < offsets_size)
return 0;
total = tmp + ALIGN(extra_buffers_size,
sizeof(void *));
if (total < tmp || total < extra_buffers_size)
return 0;
return max(total, sizeof(void *));
}分配总长度不是简单的 data_size + offsets_size + extra_buffers_size:三段分别按指针宽度对齐,并通过加法后的回绕检查拒绝溢出。即使三段长度都是 0,也至少分配一个指针宽度,使空 Parcel 也拥有唯一的用户地址,避免两个零长度 buffer 无法在 allocated_buffers 中按地址区分。
3.2 入口错误
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_new_buf()
if (!binder_alloc_is_mapped(alloc))
return ERR_PTR(-ESRCH);
size = sanitized_size(data_size,
offsets_size, extra_buffers_size);
if (unlikely(!size))
return ERR_PTR(-EINVAL);
next = kzalloc(sizeof(*next), GFP_KERNEL);
if (!next)
return ERR_PTR(-ENOMEM);这三个错误属于不同层次:-ESRCH 表示 allocator 的 VMA 已经关闭,-EINVAL 表示长度组合溢出或非法,-ENOMEM 表示仅为 buffer 元数据预分配的内存失败。地址空间不足和 async 配额不足由锁内 best-fit 函数返回 -ENOSPC,不能把所有失败都归结成物理页不足。
4. Best-fit分配
4.1 空闲树搜索
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_new_buf_locked()
n = alloc->free_buffers.rb_node;
best_fit = NULL;
while (n) {
buffer = rb_entry(n,
struct binder_buffer, rb_node);
buffer_size = binder_alloc_buffer_size(
alloc, buffer);
if (size < buffer_size) {
best_fit = n;
n = n->rb_left;
} else if (size > buffer_size) {
n = n->rb_right;
} else {
best_fit = n;
break;
}
}
if (!best_fit)
return ERR_PTR(-ENOSPC);free_buffers 的比较键是 buffer 实际区间大小,而不是 struct binder_buffer 的地址。搜索过程中遇到第一个足够大的节点就记录并继续向左,最终得到最小的可用区间;大小完全相等时立即命中。它是 best-fit,不是 first-fit,也不是按最低虚拟地址优先。
4.2 区间拆分
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_new_buf_locked()
if (buffer_size != size) {
buffer = rb_entry(best_fit,
struct binder_buffer, rb_node);
new_buffer->user_data =
buffer->user_data + size;
list_add(&new_buffer->entry,
&buffer->entry);
new_buffer->free = 1;
binder_insert_free_buffer(
alloc, new_buffer);
new_buffer = NULL;
}
rb_erase(&buffer->rb_node,
&alloc->free_buffers);
buffer->free = 0;
buffer->allow_user_free = 0;
binder_insert_allocated_buffer_locked(
alloc, buffer);分配一个大于请求的 free buffer 时,原区间被切成“已分配前段 + 空闲后段”;后段使用调用者预先分配的 new_buffer 元数据。已分配段从 free tree 移出、进入 allocated tree,并把 allow_user_free 置零,直到驱动把事务交付给用户态后才允许 BC_FREE_BUFFER。
4.3 页Lru隔离
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_lru_freelist_del()、binder_alloc_new_buf_locked()
next_used_page = (buffer->user_data + buffer_size)
& PAGE_MASK;
curr_last_page = PAGE_ALIGN(
buffer->user_data + size);
binder_lru_freelist_del(alloc,
PAGE_ALIGN(buffer->user_data),
min(next_used_page, curr_last_page));分配前要从全局 free-page LRU 移除将被重新使用的页。结束地址取 next_used_page 和本次请求页边界的较小值,是为了保留与相邻已使用 buffer 共享的末页;如果直接按整个 buffer_size 移除,可能把仍被邻居使用的页错误地当成当前分配独占页。
5. 异步配额
5.1 配额扣减
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_new_buf_locked()
if (is_async && alloc->free_async_space < size)
return ERR_PTR(-ENOSPC);
buffer->async_transaction = is_async;
if (is_async) {
alloc->free_async_space -= size;
if (debug_low_async_space_locked(alloc))
buffer->oneway_spam_suspect = true;
}is_async 由上层事务路径根据 TF_ONE_WAY 传入。异步 buffer 仍从同一棵 free tree 分配,但会额外消耗 free_async_space;同步 buffer 不受这项半空间配额直接限制。因而“总 free space 还有很多但 oneway 失败”是可能的,原因可能是 async 配额耗尽而不是地址空间耗尽。
5.2 低水位检测
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:debug_low_async_space_locked()
if (alloc->free_async_space >=
alloc->buffer_size / 10) {
alloc->oneway_spam_detected = false;
return false;
}
if (num_buffers > 50 ||
total_alloc_size > alloc->buffer_size / 4) {
if (!alloc->oneway_spam_detected) {
alloc->oneway_spam_detected = true;
return true;
}
}
return false;低水位检查只在 async 空间低于总 buffer 的 10% 时开始统计当前 pid 的 async buffer 数量和总大小;超过 50 个或超过总 buffer 的 25% 才把本次 buffer 标记为 oneway_spam_suspect。这些阈值属于驱动的检测条件,不是 oneway 队列的容量定义,也不代表事务必然被拒绝。
6. 按需装页
6.1 Buffer覆盖范围
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_install_buffer_pages()
start = buffer->user_data & PAGE_MASK;
final = PAGE_ALIGN(buffer->user_data + size);
for (page_addr = start; page_addr < final;
page_addr += PAGE_SIZE) {
index = (page_addr - alloc->vm_start)
/ PAGE_SIZE;
if (binder_get_installed_page(
alloc, index))
continue;
ret = binder_install_single_page(
alloc, index, page_addr);
if (ret)
return ret;
}一个 buffer 可能从页中间开始,也可能在末页与邻接 buffer 共享。安装函数按页对齐后的起止地址遍历,只为 alloc->pages[index] == NULL 的页创建和插入 page;已经安装的页直接复用。
6.2 页面创建
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_install_single_page()、binder_page_alloc()、binder_page_insert()
page = binder_page_alloc(alloc, index);
if (!page) {
ret = -ENOMEM;
goto out;
}
ret = binder_page_insert(alloc, addr, page);
switch (ret) {
case -EBUSY:
ret = 0;
binder_free_page(page);
page = binder_page_lookup(alloc, addr);
if (!page) {
ret = -ESRCH;
break;
}
fallthrough;
case 0:
binder_set_installed_page(
alloc, index, page);
break;
default:
binder_free_page(page);
break;
}binder_page_insert() 使用 vm_insert_page() 把新 page 插入目标进程 VMA。并发安装同一页时,-EBUSY 不是失败:另一个线程可能已经完成用户页表插入但尚未更新 alloc->pages[index],当前线程释放自己分配的 page,再通过 binder_page_lookup() 找回已存在的 page。只有找到并发布到数组后,后续复制路径才可以安全使用它。
6.3 发布顺序
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_set_installed_page()、binder_get_installed_page()
static inline void binder_set_installed_page(
struct binder_alloc *alloc,
unsigned long index, struct page *page)
{
smp_store_release(&alloc->pages[index], page);
}
static inline struct page *binder_get_installed_page(
struct binder_alloc *alloc,
unsigned long index)
{
return smp_load_acquire(&alloc->pages[index]);
}VMA 插入和 pages[] 发布是两个步骤;release/acquire 配对让消费者不会在数组中看到 page 指针后,却看不到相应的安装结果。它不替代 allocator mutex,也不负责保护 buffer 树。
7. 释放合并
7.1 释放入口
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_free_buf()、binder_free_buf_locked()
if (buffer->clear_on_free) {
binder_alloc_clear_buf(alloc, buffer);
buffer->clear_on_free = false;
}
mutex_lock(&alloc->mutex);
binder_free_buf_locked(alloc, buffer);
mutex_unlock(&alloc->mutex);如果 buffer 要求清零,清零发生在取得 allocator mutex 之前,减少大 buffer 清理对树操作的锁竞争。随后锁内完成 async 配额恢复、页 LRU 入队、树迁移和相邻区间合并。
7.2 配额恢复
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_free_buf_locked()
if (buffer->async_transaction)
alloc->free_async_space += buffer_size;
binder_lru_freelist_add(alloc,
PAGE_ALIGN(buffer->user_data),
(buffer->user_data + buffer_size)
& PAGE_MASK);
rb_erase(&buffer->rb_node,
&alloc->allocated_buffers);
buffer->free = 1;恢复使用的是整个 allocator 区间 buffer_size,不是仅 data、offsets 和 extra 三段的逻辑长度;这是因为 async 配额保护的是地址空间占用。释放后覆盖范围中不再被相邻活动 buffer 使用的页才会进入 free-page LRU。
7.3 邻居合并
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_free_buf_locked()、binder_delete_free_buffer()
if (!list_is_last(&buffer->entry,
&alloc->buffers)) {
struct binder_buffer *next =
binder_buffer_next(buffer);
if (next->free) {
rb_erase(&next->rb_node,
&alloc->free_buffers);
binder_delete_free_buffer(
alloc, next);
}
}
if (alloc->buffers.next != &buffer->entry) {
struct binder_buffer *prev =
binder_buffer_prev(buffer);
if (prev->free) {
binder_delete_free_buffer(
alloc, buffer);
rb_erase(&prev->rb_node,
&alloc->free_buffers);
buffer = prev;
}
}
binder_insert_free_buffer(alloc, buffer);合并依赖 buffers 地址链表,而不是红黑树中的相邻节点:free tree 按大小排序,不能表达虚拟地址邻接。删除被吸收的元数据后,合并后的大区间重新按大小插入 free tree,下一次 best-fit 才能看到新的连续空间。
8. 页回收
8.1 LRU候选
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_lru_freelist_add()、binder_alloc_free_page()
ret = list_lru_add(alloc->freelist,
page_to_lru(page), page_to_nid(page), NULL);
WARN_ON(!ret);页面在 buffer 释放后进入全局 binder_freelist,但仍保留在用户 VMA 和 alloc->pages[] 中;这允许后续同一地址重新分配时快速复用,不需要立即重新申请 page。LRU 是“可回收候选”,不是已经解除映射。
8.2 Shrinker检查
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_free_page()
if (!mmget_not_zero(mm))
goto err_mmget;
vma = lock_vma_under_rcu(mm, page_addr);
if (!vma) {
if (!mmap_read_trylock(mm))
goto err_mmap_read_lock_failed;
mm_locked = 1;
vma = vma_lookup(mm, page_addr);
}
if (!mutex_trylock(&alloc->mutex))
goto err_get_alloc_mutex_failed;
if (vma && !binder_alloc_is_mapped(alloc))
goto err_invalid_vma;shrinker 不能只拿 allocator mutex 就释放 page:它还要确认 mm 仍然有效、找到对应 VMA,并避免在 allocator 正在关闭或映射已失效时操作用户页表。拿不到任一锁时返回 LRU_SKIP,让本次内存回收保留候选而不是冒险破坏映射。
8.3 解除映射
源码文件:kernel/common/drivers/android/binder_alloc.c
相关函数:binder_alloc_free_page()
page_to_free = alloc->pages[index];
binder_set_installed_page(alloc, index, NULL);
list_lru_isolate(lru, item);
spin_unlock(&lru->lock);
if (vma)
zap_page_range_single(
vma, page_addr, PAGE_SIZE, NULL);
mutex_unlock(&alloc->mutex);
binder_free_page(page_to_free);
return LRU_REMOVED_RETRY;顺序是先从 pages[] 撤销 allocator 所有权,再隔离 LRU 节点、清除用户页表,最后释放 page 和 shrinker 元数据。之后如果同一 buffer 再次被分配,binder_install_single_page() 会重新创建 page 并插入 VMA。
9. 失败边界
| 阶段 | 条件 | 结果 | owner是否变化 |
|---|---|---|---|
| 映射 | VMA 的 vm_mm 不匹配 | -EINVAL | 不发布 mapped |
| 映射 | 重复建立地址空间 | -EBUSY | 保留原 allocator |
| 尺寸 | 对齐或加法溢出 | -EINVAL | 不创建 buffer |
| 选择 | 没有足够大的连续 free 区间 | -ENOSPC | free tree 不变 |
| async | free_async_space < size | -ENOSPC | async 配额不扣减 |
| 页安装 | 分配 page 失败 | -ENOMEM | 已选 buffer 被释放回 tree |
| 页安装 | VMA/mm 已失效 | -ESRCH | allocator 进入关闭收束 |
| shrinker | 锁或 mm 检查失败 | LRU_SKIP | page 留在 LRU |
尤其要区分“连续虚拟地址不足”和“物理页不足”:best-fit 找不到区间时即使系统还有大量空闲物理内存也会返回 -ENOSPC;页安装失败则发生在区间已经从 free tree 取出之后,binder_alloc_new_buf() 会调用 binder_alloc_free_buf() 回滚地址区间。
10. 测试输入
10.1 初始状态
源码文件:kernel/common/drivers/android/tests/binder_alloc_kunit.c
相关测试:binder_alloc_test_mmap()
KUNIT_EXPECT_EQ(test, alloc->mapped, true);
KUNIT_EXPECT_EQ(test, alloc->buffer_size,
BINDER_MMAP_SIZE);
n = rb_first(&alloc->allocated_buffers);
KUNIT_EXPECT_PTR_EQ(test, n, NULL);
n = rb_first(&alloc->free_buffers);
buf = rb_entry(n,
struct binder_buffer, rb_node);
KUNIT_EXPECT_EQ(test,
binder_alloc_buffer_size(alloc, buf),
BINDER_MMAP_SIZE);fixture 先建立测试 VMA,再断言 allocator 已发布、allocated tree 为空、free tree 只有覆盖整个映射的初始区间。它证明初始化后的树形不变量,不证明真实 app 的 ProcessState::mmap() 参数。
10.2 穷举分配释放
源码文件:kernel/common/drivers/android/tests/binder_alloc_kunit.c
相关测试:binder_alloc_exhaustive_test()、gen_buf_offsets()、permute_frees()
测试生成不同页对齐、前向/后向布局和所有释放顺序:先分配多个 buffer,断言覆盖页已安装;按排列组合释放,断言相关页进入 LRU;再次分配,断言 LRU 清空;最后调用 binder_alloc_free_page(),断言 pages[] 全部为空。它验证的是 buffer 边界、页共享、合并和回收组合,不是吞吐量或碎片率基准。
10.3 关闭收尾
源码文件:kernel/common/drivers/android/tests/binder_alloc_kunit.c
相关测试:binder_alloc_test_exit()、binder_alloc_vma_close()、binder_alloc_deferred_release()
teardown 关闭 backing file 触发 VMA close,再调用 deferred release,检查测试 LRU 为空。这个路径验证 allocator 可以在映射失效后释放 allocated buffer、已安装页、页数组和 mm 引用;它没有证明活动 Binder transaction 在任意时刻都能安全跳过上层事务清理。
11. 源码导航
遇到 Binder buffer 分配失败,可以按以下顺序定位:
- 看
binder_alloc_is_mapped(),排除 VMA 已关闭导致的-ESRCH; - 用
sanitized_size()复算 data、offsets、extra 对齐后的总长度; - 查看
free_async_space,判断是否只有 oneway 配额耗尽; - 检查
free_buffers是否有足够大的连续区间,而不是只看 free 总量; - 如果区间已选中但返回
-ENOMEM/-ESRCH,继续看binder_install_single_page()的 VMA 和 page 插入; - 释放后检查
buffers链表的邻居合并,以及对应页是否进入 LRU; - 内存压力场景再看
binder_alloc_free_page()是否因锁竞争返回LRU_SKIP。
源码搜索:
rg -n "binder_alloc_new_buf|sanitized_size|binder_alloc_new_buf_locked" \
kernel/common/drivers/android/binder_alloc.c
rg -n "binder_install_buffer_pages|binder_install_single_page|binder_page_insert" \
kernel/common/drivers/android/binder_alloc.c
rg -n "binder_free_buf_locked|binder_alloc_free_buf|binder_delete_free_buffer" \
kernel/common/drivers/android/binder_alloc.c
rg -n "binder_alloc_free_page|binder_shrink_count|binder_shrink_scan" \
kernel/common/drivers/android/binder_alloc.c
rg -n "binder_alloc_exhaustive_test|permute_frees|binder_alloc_test_mmap" \
kernel/common/drivers/android/tests/binder_alloc_kunit.c如果能回答“为什么 free tree 按大小、allocated tree 按地址”“为什么 async 配额和总 free space 是两个判断”“为什么释放 buffer 后页先进入 LRU 而不是立即 free”“为什么 shrinker 要同时验证 VMA、mm 和 allocator mutex”,就已经能顺着 binder_alloc 的真实状态变化阅读后续 buffer 管理和内存压力路径。
