25. 打开 Binder:open() 系统调用在 Binder 驱动中的处理流程
咱们今天聊聊 Binder 驱动里最基础的一个动作——open()。
你可能会想,一个文件打开操作有什么好讲的?嗯,这里面的门道还真不少。Binder 驱动虽然伪装成了一个字符设备(/dev/binder),但它干的活跟普通文件驱动完全不一样。我当年第一次看 Binder 驱动源码时,就被这个 open() 给绕进去了——它根本不是打开一个文件,而是在给进程“注册”一个跟内核通信的通道。
25.1 用户态发起 open()
在 Android 的 Native 层,任何想用 Binder 的进程,第一步都是这么干的:
int fd = open("/dev/binder", O_RDWR);
就这么一行代码。但这一行下去,内核里会触发一连串的动作。说白了,用户态进程只是告诉内核:“嘿,我要用 Binder 了,给我准备好。”
我个人习惯把 Binder 驱动看作一个“邮局”。open() 就是你去邮局申请了一个专属信箱。邮局(内核)得给你分配一个管理员(binder_proc),记录你的身份(PID、UID),还要给你准备收发室(内存映射区)。
25.2 内核态:binder_open() 函数
当用户态调用 open() 后,VFS 层会根据设备号找到 Binder 驱动的 file_operations 结构体,然后调用 binder_open() 函数。这个函数的原型长这样:
static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc;
// ... 分配和初始化 binder_proc 结构体
filp->private_data = proc;
// ...
return 0;
}
注意看最后一行——filp->private_data = proc。这是关键中的关键。内核把新创建的 binder_proc 结构体挂到了文件指针的私有数据区。以后这个进程再通过这个 fd 做任何 Binder 操作(比如 ioctl()、mmap()),内核都能通过 filp->private_data 找到对应的 binder_proc。
核心理解: Binder 驱动里的 open() 本质上是在创建 binder_proc 对象。每个进程只有一个这样的对象,它贯穿整个 Binder 通信生命周期。
25.3 binder_proc 结构体里有什么?
我给大家拆解一下这个结构体里的关键字段。你可以把它想象成进程的“Binder 身份证”:
| 字段 | 作用 | 我的理解 |
|---|---|---|
tsk |
指向当前进程的 task_struct | 谁在说话 |
pid |
进程 PID | 身份编号 |
uid |
进程 UID | 权限等级 |
nodes |
红黑树,管理该进程所有的 Binder 实体 | 你有哪些服务 |
refs_by_desc |
红黑树,按描述符索引的引用列表 | 你引用了谁 |
refs_by_node |
红黑树,按节点索引的引用列表 | 谁在引用你 |
buffer |
内存映射相关的管理结构 | 收发室地址 |
todo |
待处理工作队列 | 待办事项清单 |
delivered_death |
已送达的死亡通知列表 | 谁已经挂了 |
你看,光是 open() 这一步,内核就已经为进程准备好了这么多基础设施。我曾经在调试一个 Binder 死锁问题时,就是通过检查 todo 队列发现某个进程的待办事项堆积了上千条,才定位到问题根源。
25.4 流程图:open() 的完整调用链
下面这张图展示了从用户态 open() 到内核态 binder_open() 的完整路径:
25.5 关键细节:为什么 open() 不分配内存映射?
这里有个容易混淆的点。很多人以为 open() 之后就能直接传数据了。其实不行。
open() 只是创建了 binder_proc,但还没有分配数据缓冲区。Binder 的数据传输依赖 mmap() 来建立内核态和用户态共享的内存区域。我见过不少新手在写 Binder 测试程序时,只调了 open() 就急着发数据,结果 ioctl() 返回 -ENOMEM,一脸懵逼。
避坑指南: 我曾经在调试一个系统服务时,发现它 open 了 Binder 设备后没有 mmap,导致所有 Binder 调用都失败。检查了三天才发现是初始化顺序错了。记住:open() 只是第一步,mmap() 才是真正让通信跑起来的关键。
25.6 open() 的返回值与错误处理
binder_open() 可能返回的错误码不多,但每个都值得注意:
- 0:成功。fd 已经可用,但还不能收发数据。
- -ENOMEM:内核内存不足,无法分配
binder_proc。这种情况在低内存设备上偶尔出现。 - -EPERM:权限不足。Binder 设备通常需要 root 或 system 权限才能打开。
嗯,这里要注意一点:Binder 驱动在 open() 时不会检查进程是否有权限使用 Binder。真正的权限检查是在 ioctl() 阶段做的。为什么这么设计?说白了,open() 只是登记,ioctl() 才是干活。先让你进门,再查你证件。
25.7 我的一点经验总结
回过头来看,open() 在 Binder 驱动里的处理流程,本质上就三件事:
- 分配:从内核堆里切出一块内存给
binder_proc - 登记:把进程的身份信息写进去
- 关联:把
binder_proc挂到文件描述符上
你想想看,这三件事做完,内核就知道“哦,进程 A 来了,我记住它了”。但这时候还不能通信,因为收发室(内存映射区)还没建好。那是 mmap() 的工作,咱们下一章再聊。
我个人觉得,理解 open() 的关键在于抓住一个核心思想:Binder 驱动把每个进程都抽象成了一个 binder_proc 对象。这个对象是进程在内核里的“代理人”,所有 Binder 操作最终都要通过它来完成。
小技巧: 如果你在调试 Binder 相关问题,可以在 binder_open() 函数里加打印,输出当前进程的 PID 和 UID。这样你就能清楚地看到哪些进程在什么时候打开了 Binder 设备。我经常用这个方法排查“谁在滥用 Binder”的问题。
公众号:蓝海资料掘金营,微信deep3321