Binder 驱动与内存映射:mmap 机制与内核源码浅析
讲 Binder 绕不开 mmap,这是整个 IPC 效率的根基。我刚开始啃这部分时也被 mmap 搞得一头雾水,后来在系统稳定性 project 里排查过一次 Binder 通信卡顿——嗯,就是 mmap 的映射大小没配好。今天咱们就把这块啃透。
mmap 到底在干嘛?
说白了,mmap 就是建立用户空间和内核空间的一段共享内存区域。传统 IPC 需要把数据从用户 A 拷贝到内核,再从内核拷贝到用户 B,两次拷贝。Binder 的 mmap 让内核和用户进程直接共享同一块物理内存,数据只需拷贝一次——从发送进程的用户空间拷贝到内核的共享缓存区,接收进程直接读这块内存,零复制。
你想想看,省掉一次拷贝,对于频繁跨进程调用的 Android 系统来说,性能差距是数量级的。
Binder 驱动的 mmap 实现
Binder 驱动在 drivers/android/binder.c 中实现了 binder_mmap()。我记得第一次看这块代码时,感觉像是在读一篇精密的机械图纸。来看核心流程:
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct binder_proc *proc = filp->private_data;
// 只允许映射一次
if (proc->buffer)
return -EBUSY;
// 限制映射大小,默认 4MB,可通过 sysctl 调整
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
// 在内核中分配物理页
proc->buffer = kzalloc(SZ_4M, GFP_KERNEL);
// 建立页表映射
vma->vm_ops = &binder_vm_ops;
return 0;
}
注意几个要点:
- 每个 Binder 进程只允许一次 mmap,第二次会返回
-EBUSY。我在项目中遇到过有人重复调用ioctl时触发了这个错误,排查了好久才发现是上层重复初始化了 Binder 服务。 - 映射大小默认 4MB,这个值直接影响你能同时处理的 Binder 事务数量。曾经有客户反馈相机预览掉帧,最后发现是 Binder 缓冲区太小,大尺寸数据来回排队。调大
binder_buffer_size后问题解决。 - 实际物理内存是延迟分配的,首次写入时才真正分配页面,这叫 惰性分配,能有效节省内存。
内核源码关键路径:binder_ioctl 与 binder_transaction
当用户进程通过 ioctl(BC_TRANSACTION) 发送数据时,驱动会走 binder_ioctl_write_read() → binder_transaction()。这里面有个经典逻辑:
- 从发送进程用户空间拷贝数据到接收进程的
proc->buffer中(也就是 mmap 共享区) - 把数据偏移量通过
binder_buffer结构体传给接收进程 - 接收进程从 mmap 区域直接读取数据,没有第二次拷贝
我曾在调试一个第三方 APK 的 Binder 死锁问题时,硬啃这段代码。注意看 binder_alloc_new_buf(),它负责分配内核缓冲区。如果分配失败,会返回 BR_DEAD_REPLY,上层就以为是服务挂掉了——其实只是缓冲池满了。
| 方式 | 数据拷贝次数 | 上下文切换 |
|---|---|---|
| 传统 Socket IPC | 2 次 | 至少 2 次 |
| 共享内存 | 0 次 | 无(需自行同步) |
| Binder + mmap | 1 次 | 1 次 |
你想想看,少了 1 次拷贝意味着什么?对于高频的 Binder 调用(比如屏幕刷新、传感器数据),每个事务节省几十微秒,整个系统的流畅度感知就有了质的提升。
避坑指南:mmap 大小与 OOM
某个定制的 Android 设备,进程数多达 200+,每个进程默认映射 4MB,光 Binder 就占用了 800MB 内核内存。结果系统频繁 OOM,杀进程都杀不掉。后来才发现需要根据设备物理内存动态调整
vm.mmap_min_addr 和每个进程的映射上限。建议在量产项目中评估峰值并发事务数,合理设置 binder_buffer_size。
查看当前系统各进程的 Binder 映射大小:
adb shell cat /proc/*/maps | grep "binder"可以看到每个 Binder 进程的虚拟地址范围。
整体流程一张图讲清
下面是我画的一张流程/结构图,把 Binder 驱动层这几步串起来:
引用内核源码的关键点
我个人习惯在分析 Binder 问题时直接扔出 kernel/msm-4.14/drivers/android/binder.c。你可以重点关注这些函数:
binder_mmap()—— 初始化共享内存区binder_alloc_new_buf()—— 分配内核缓冲区binder_transaction()—— 数据拷贝与队列管理binder_thread_read()—— 接收进程从 mmap 读取数据
说实话,Binder 驱动写得非常优雅。它用 mmap 把内核空间和用户空间粘在一起,省掉了传统 IPC 的冗余拷贝。这也是为什么 Android 用 Binder 而不是 System V IPC 的根本原因——性能和数据安全性两全其美。
Binder 的 mmap 机制使得跨进程通信只发生一次数据拷贝(用户→内核共享区),接收端通过内存映射零成本读取。这是 Binder 性能的关键所在。