20、Binder 的缓冲区管理:Binder 内核缓冲区的大小限制、分配与回收策略

说到 Binder 通信,大家最关心的往往是传输效率。但有个东西要是没搞明白,你写再多代码也是白搭——那就是 Binder 的缓冲区管理。

我刚开始接触 Binder 时,就吃过这个亏。有一次在项目里传输一张大图片,结果进程直接挂了。查了半天,原来是 Binder 缓冲区溢出了。嗯,从那以后,我再也不敢小看这块内容了。

20.1 内核缓冲区的大小限制

Binder 在内核中维护了一块共享内存区域,用于存放进程间传输的数据。这块区域的大小,不是你想传多少就传多少的。

说白了,Binder 缓冲区的大小限制分两个层面:

  • 单次传输限制:默认是 4MB(在 64 位系统上)
  • 进程总限制:每个进程的 Binder 缓冲区总大小有限

你可能会问:这个 4MB 是怎么来的?我查过内核源码,在 drivers/android/binder.c 中,有个宏定义:

#define BINDER_MAX_TRANSACTION_SIZE  (4 * 1024 * 1024)  // 4MB

这个值不是随便定的。我个人的理解是,4MB 是性能和内存占用之间的一个平衡点。传小数据时够用,传大数据时也不会把内核内存吃光。

重要提示:单次传输超过 4MB 的数据,Binder 会直接返回 -ENOSPC 错误。你的应用会收到一个 TransactionTooLargeException。

另外,每个进程还有自己的缓冲区池。这个池子的大小由 /sys/module/binder/parameters/binder_size 控制,默认是 4MB。也就是说,一个进程同时能占用的 Binder 缓冲区总量也是有限的。

20.2 缓冲区的分配策略

Binder 的缓冲区分配,不是简单的 malloc/free。它有一套自己的玩法。

我把它总结成三步:

  1. 从进程池中划出一块:内核从当前进程的 Binder 缓冲区池中,找一块足够大的连续空间
  2. 映射到接收进程:通过 mmap 把这块空间映射到接收进程的地址空间
  3. 拷贝数据:把发送进程的数据拷贝到这块缓冲区中

你想想看,这里有个关键点:Binder 只做一次拷贝。传统 IPC 需要两次拷贝(从发送进程到内核,再从内核到接收进程),而 Binder 利用共享内存,只拷贝一次。

分配时,内核会维护一个空闲链表。每次分配时,它会用最佳适配算法(Best Fit)来找合适的空闲块。我在项目中遇到过一个问题:频繁传输大小不一的数据,导致空闲链表碎片化严重,分配效率越来越低。

我的经验:如果你需要频繁传输数据,尽量让数据大小保持一致。这样可以减少碎片,提高分配效率。

20.3 缓冲区的回收策略

回收策略,说白了就是什么时候把缓冲区还回去。

Binder 的回收是延迟回收的。什么意思呢?就是当一次传输完成后,缓冲区不会立即被回收,而是被标记为“空闲”。下次分配时,优先重用这些空闲块。

这样做的好处很明显:

  • 减少 mmap/unmmap 的系统调用次数
  • 降低内存分配的开销
  • 提高整体吞吐量

但坏处也有:如果进程一直不释放,空闲块会越积越多。所以内核会定期做一次“压缩”,把分散的空闲块合并成更大的连续空间。

我曾经调试过一个性能问题:某个服务进程的 Binder 缓冲区占用居高不下。查了半天,发现是客户端频繁发送请求但不及时处理,导致缓冲区一直被占用。后来加了个超时机制,问题就解决了。

注意:如果接收进程处理速度跟不上发送速度,缓冲区会被快速耗尽。这时候 Binder 会阻塞发送进程,直到有缓冲区可用。这就是所谓的“背压”机制。

20.4 缓冲区管理的核心流程

下面这张图,是我自己画的 Binder 缓冲区管理流程。你看一眼就能明白整个链路:

Binder 缓冲区管理流程 发送进程 准备数据 内核缓冲区池 空闲链表管理 最佳适配算法 接收进程 读取数据 拷贝数据 共享映射 延迟回收 标记为空闲块 传输完成 定期压缩 合并空闲块 碎片过多时 大小限制 • 单次传输:≤ 4MB • 进程总限制:4MB • 超出返回 -ENOSPC • 触发背压机制 • 阻塞发送进程 一次拷贝 + 共享映射 + 延迟回收 = 高效 IPC

从这张图你能看到,整个流程是环环相扣的。发送进程把数据拷贝到内核缓冲区池,内核通过共享映射让接收进程直接读取,传输完成后延迟回收,碎片多了就压缩。

20.5 实际项目中的避坑指南

讲了这么多理论,我来分享几个实际项目中踩过的坑:

坑一:大图传输

我曾经在一个相册应用里,直接通过 Binder 传输原图。结果 4MB 的限制直接把我拦住了。后来改用文件描述符传递,让接收进程自己去读文件,问题就解决了。

坑二:频繁小数据

有个同事写了个日志收集服务,每秒传输几百次小数据。结果 Binder 缓冲区碎片化严重,分配越来越慢。我建议他合并数据,批量发送,性能提升了好几倍。

坑三:内存泄漏

有一次线上服务 OOM 了。查了半天,发现是某个模块发送完数据后,没有及时释放 Binder 缓冲区引用。内核以为缓冲区还在用,就一直不回收。最后加了个引用计数检查,才把问题解决。

20.6 总结

Binder 的缓冲区管理,说白了就是三个关键词:大小限制一次拷贝延迟回收

大小限制告诉你别传太多数据,一次拷贝保证了效率,延迟回收减少了系统调用。这三者配合起来,才让 Binder 成为 Android 中最核心的 IPC 机制。

我个人建议,在实际开发中,尽量控制单次传输数据在 1MB 以内。超过这个量,就该考虑用文件描述符或者共享内存了。别等到线上出问题了,才来查 Binder 缓冲区——那时候就晚了。


公众号:蓝海资料掘金营,微信deep3321