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() 的完整路径:

open() 系统调用在 Binder 驱动中的处理流程 用户态进程 open("/dev/binder", O_RDWR) 系统调用 VFS 层 查找 file_operations 调用 binder_open() 核心处理函数 1. kzalloc 分配 binder_proc 结构体 约 1KB 内核内存 2. 初始化字段:pid, uid, tsk 记录进程身份信息 3. filp->private_data = proc 建立 fd ↔ binder_proc 映射 返回 0 (成功) 用户态 → 内核态

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 驱动里的处理流程,本质上就三件事:

  1. 分配:从内核堆里切出一块内存给 binder_proc
  2. 登记:把进程的身份信息写进去
  3. 关联:把 binder_proc 挂到文件描述符上

你想想看,这三件事做完,内核就知道“哦,进程 A 来了,我记住它了”。但这时候还不能通信,因为收发室(内存映射区)还没建好。那是 mmap() 的工作,咱们下一章再聊。

我个人觉得,理解 open() 的关键在于抓住一个核心思想:Binder 驱动把每个进程都抽象成了一个 binder_proc 对象。这个对象是进程在内核里的“代理人”,所有 Binder 操作最终都要通过它来完成。

小技巧: 如果你在调试 Binder 相关问题,可以在 binder_open() 函数里加打印,输出当前进程的 PID 和 UID。这样你就能清楚地看到哪些进程在什么时候打开了 Binder 设备。我经常用这个方法排查“谁在滥用 Binder”的问题。


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