14. Binder 事务:binder_transaction 数据结构与事务的构建过程

Binder 通信的核心,说白了就是一次「事务」。你想想看,客户端要调用服务端的一个方法,参数怎么传过去?返回值怎么拿回来?中间跨了进程,数据怎么打包?这一切,都围绕着一个叫 binder_transaction 的数据结构展开。

我个人习惯把 Binder 事务比作一个「快递包裹」。你要寄东西,得先找个箱子,填好寄件人和收件人,再把东西放进去封好。Binder 事务也是一样——binder_transaction 就是那个箱子,里面装着你要传递的数据和元信息。

binder_transaction 数据结构长什么样?

咱们直接看代码。这个结构体定义在 kernel/drivers/android/binder.c 里,我挑几个关键字段说说:

struct binder_transaction {
    struct binder_work work;           // 挂到待处理队列用
    struct binder_thread *from;        // 发起事务的线程
    struct binder_process *to_proc;    // 目标进程
    struct binder_thread *to_thread;   // 目标线程(可能为NULL)
    struct binder_transaction *from_parent; // 嵌套事务的父事务
    struct binder_transaction *to_parent;   // 被嵌套事务的子事务
    struct binder_buffer *buffer;      // 数据缓冲区
    unsigned int    code;              // 事务码(比如BR_TRANSACTION)
    unsigned int    flags;             // 标志位
    struct binder_priority priority;   // 优先级
    struct binder_priority saved_priority; // 保存的原始优先级
    bool    is_failure;                // 是否失败
    bool    is_async;                  // 是否异步
    int     pid;                       // 发起进程PID
    int     uid;                       // 发起进程UID
    ktime_t start_time;                // 事务开始时间
    struct list_head fd_fixups;        // 文件描述符修复列表
};

嗯,这里要注意,from_parentto_parent 这两个字段是用来处理「嵌套事务」的。我在项目中遇到过一种情况:服务端在处理一个请求时,又主动调用了另一个 Binder 服务,这就形成了嵌套。如果没有这两个指针,事务的上下文就串不起来了。

核心要点:每个 Binder 事务都对应一个 binder_transaction 实例,它贯穿了整个 IPC 的生命周期——从发起、传输、到处理完成。

事务的构建过程:从用户态到内核态

事务是怎么构建起来的?我带你走一遍流程。假设你在 Java 层调用了 transact()

  1. Java 层打包Parcel 把参数序列化,写入 mDatamObjects
  2. JNI 桥接:调用 android_os_Binder_exec_transact(),把 Parcel 数据传到 Native 层。
  3. Native 层封装IPCThreadState::transact() 构造 binder_transaction_data 结构体。
  4. ioctl 进入内核:通过 BINDER_WRITE_READ 命令,把数据传给内核驱动。
  5. 内核构建事务binder_transaction() 函数分配 binder_transaction 对象,填充各个字段。

说白了,第5步才是真正的「事务构建」。前面几步都是在准备原材料。

内核中的事务构建:binder_transaction() 函数

这个函数是 Binder 驱动的核心之一。我挑几个关键步骤说说:

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply,
                   binder_size_t extra_buffers_size)
{
    struct binder_transaction *t;
    struct binder_work *tcomplete;
    struct binder_proc *target_proc;
    struct binder_thread *target_thread = NULL;
    struct binder_node *target_node = NULL;
    // ... 省略部分代码

    // 1. 分配事务对象
    t = kzalloc(sizeof(*t), GFP_KERNEL);
    if (!t) {
        return_error = BR_FAILED_REPLY;
        goto err;
    }

    // 2. 分配数据缓冲区
    t->buffer = binder_alloc_new_buf(&target_proc->alloc,
                     tr->data_size, tr->offsets_size,
                     extra_buffers_size, !reply && !(t->flags & TF_ONE_WAY));
    // ... 省略错误处理

    // 3. 拷贝用户数据到内核缓冲区
    copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)tr->data.ptr.buffer,
               tr->data_size);
    copy_from_user(binder_alloc_copy_user_to_buffer(...));

    // 4. 处理Binder对象引用
    for (off = 0; off < tr->offsets_size / sizeof(size_t); off++) {
        struct flat_binder_object *fp;
        fp = (struct flat_binder_object *)(t->buffer->data + offsets[off]);
        switch (fp->hdr.type) {
        case BINDER_TYPE_BINDER:
            // 处理Binder实体
            break;
        case BINDER_TYPE_HANDLE:
            // 处理Binder句柄
            break;
        case BINDER_TYPE_FD:
            // 处理文件描述符
            break;
        }
    }

    // 5. 挂入目标队列
    t->work.type = BINDER_WORK_TRANSACTION;
    list_add_tail(&t->work.entry, &target_list);
    tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;
    list_add_tail(&tcomplete->entry, &thread->todo);
}

这段代码我简化了,但核心逻辑都在。你看,第1步分配事务对象,第2步分配缓冲区,第3步拷贝数据,第4步处理对象引用,第5步挂入队列。每一步都环环相扣。

我曾经踩过一个坑:在拷贝数据时,如果 copy_from_user 失败,必须回滚之前分配的所有资源。否则就会造成内存泄漏。Binder 驱动里用了 goto err 的清理机制,这个设计很值得学习。

事务的两种类型:同步 vs 异步

Binder 事务分两种:同步(one-way = false)和异步(one-way = true)。区别在哪?

特性 同步事务 异步事务
调用方是否阻塞 是,等待回复 否,立即返回
是否需要回复 是,必须有 BR_REPLY 否,不需要回复
缓冲区分配策略 分配在内核缓冲区 分配在内核缓冲区
典型场景 方法调用、获取属性 事件通知、日志上报
事务队列 挂入目标线程的 todo 队列 挂入目标进程的 todo 队列

你想想看,为什么异步事务要挂到进程队列而不是线程队列?因为异步事务不需要指定目标线程,哪个线程空闲就由哪个线程处理。这样可以提高并发度。

数据缓冲区:binder_buffer 的结构

事务的数据存在 binder_buffer 里。这个结构体管理着内核中的一块连续内存:

struct binder_buffer {
    struct list_head entry;    // 链表节点
    struct rb_node rb_node;    // 红黑树节点
    unsigned int free:1;       // 是否空闲
    unsigned int allow_user_free:1; // 是否允许用户释放
    unsigned int async_transaction:1; // 是否异步事务
    unsigned int debug_id:1;
    struct binder_transaction *transaction; // 所属事务
    struct binder_node *target_node;        // 目标Binder节点
    size_t data_size;          // 数据大小
    size_t offsets_size;       // 偏移数组大小
    size_t extra_buffers_size; // 额外缓冲区大小
    void *data;                // 数据起始地址
};

这里有个细节:data 指向的是内核虚拟地址,用户态不能直接访问。所以数据拷贝必须通过 copy_from_user / copy_to_user 来完成。嗯,这也是 Binder 安全性的一个体现——用户态程序无法直接操作内核内存。

事务构建的完整流程图

下面这张图展示了从用户态发起调用到内核构建事务的完整流程:

Binder 事务构建流程 用户态 Java: transact() → Parcel 序列化 → JNI → Native: IPCThreadState::transact() ioctl(BINDER_WRITE_READ) 内核态 (binder驱动) ① 分配 binder_transaction ② 分配 binder_buffer ③ copy_from_user 拷贝数据 ④ 处理 Binder 对象引用 ⑤ 挂入目标队列 ⑥ 唤醒目标线程 目标进程:从 todo 队列取出事务 → binder_thread_read() → 返回用户态 → 处理事务 用户态 内核态 目标进程处理

事务构建中的关键细节

有几个细节我觉得值得单独拿出来说说:

  • 缓冲区分配策略:同步事务的缓冲区会优先分配在目标线程的「空闲缓冲区列表」中,这样可以减少内存碎片。异步事务则从进程的共享缓冲区池中分配。
  • 对象引用的处理:当数据中包含 Binder 对象或文件描述符时,内核会进行「对象迁移」。比如文件描述符,内核会在目标进程中复制一个 fd,并自动关闭源进程中的 fd(如果设置了 TF_ACCEPT_FDS)。
  • 优先级继承:如果目标线程的优先级低于发起线程,Binder 会临时提升目标线程的优先级。这就是 saved_priority 字段的作用——处理完事务后再恢复回去。

一个小技巧:在调试 Binder 事务时,可以查看 /sys/kernel/debug/binder/proc/ 下的文件。每个进程的 Binder 状态都暴露在这里,包括待处理的事务列表、缓冲区使用情况等。我经常用这个来排查事务堆积的问题。

事务构建的异常处理

事务构建过程中可能出各种问题。我总结了几种常见情况:

  • 内存不足:分配 binder_transactionbinder_buffer 失败,返回 BR_FAILED_REPLY
  • 目标进程已死亡:在查找目标节点时发现进程已退出,返回 BR_DEAD_REPLY
  • 数据拷贝失败copy_from_user 时发生缺页异常或地址非法,返回 BR_FAILED_REPLY
  • 对象引用无效:数据中的 Binder 句柄或文件描述符在目标进程中无效,返回 BR_FAILED_REPLY

我曾经遇到过一个问题:某个服务频繁 crash,导致客户端调用时总是收到 BR_DEAD_REPLY。排查后发现是服务端的内存泄漏导致进程被 LMK 杀掉了。嗯,从那以后我养成了一个习惯——在客户端代码里一定要处理 DEAD_BINDER 异常,做重连或降级处理。

好了,关于 binder_transaction 数据结构和事务构建过程,就讲这么多。这个结构体虽然看起来字段很多,但核心逻辑并不复杂——分配、填充、挂入队列。理解了它,你就掌握了 Binder IPC 的「快递包裹」是怎么打包和寄出的。


专注资料整理