12. Binder 命令:详解 BC_TRANSACTION 与 BR_TRANSACTION 的交互过程

这一节,我们来聊聊 Binder 通信中最核心的一对命令——BC_TRANSACTIONBR_TRANSACTION

说白了,这就是客户端和服务端之间“发请求”和“收请求”的原始信令。你平时调用的 transact() 方法,底层最终都会落到这两个命令上。

我个人习惯把这对命令比作“快递包裹的寄出与签收”。客户端打包好数据,贴上标签(Binder 驱动),然后投递出去;服务端收到包裹,拆包处理。整个过程,Binder 驱动就是那个快递中转站。

12.1 命令的本质与数据结构

先看它们的定义。在 binder.h 中:

// 客户端 -> 驱动:发起事务
#define BC_TRANSACTION 0x40006300
// 驱动 -> 服务端:通知事务到达
#define BR_TRANSACTION 0x48026300

这两个宏的值,其实包含了命令类型和方向信息。高 16 位是命令码,低 16 位是参数大小。嗯,这里要注意,BC_ 开头的命令是从用户空间发往驱动的,BR_ 开头的则是驱动发往用户空间的。

它们携带的数据结构都是 binder_transaction_data

struct binder_transaction_data {
    union {
        __u32 handle;      // 服务端引用(客户端使用)
        void *ptr;         // Binder 对象指针(服务端使用)
    } target;
    void *cookie;          // 服务端的附加数据
    __u32 code;            // 事务码,如 FIRST_CALL_TRANSACTION
    __u32 flags;           // 标志位,如 TF_ONE_WAY
    pid_t sender_pid;      // 发送方 PID
    uid_t sender_euid;     // 发送方 EUID
    __u32 data_size;       // 数据区大小
    __u32 offsets_size;    // 偏移数组大小
    union {
        struct {
            const void *buffer;    // 数据缓冲区
            const __u32 *offsets;  // 偏移数组
        } ptr;
        __u8 buf[8];       // 小数据内联
    } data;
};

我在项目中遇到过一个问题:data_sizeoffsets_size 必须精确匹配,否则驱动会直接返回 BR_FAILED_REPLY。有一次我手算偏移量算错了,排查了半天才发现是这里对不上。

12.2 交互流程全景

整个交互过程,我画了一张图帮你理解:

客户端进程 Binder 驱动 服务端进程 1. transact() 调用 BC_TRANSACTION 2. 驱动处理事务 分配 buffer、拷贝数据 BR_TRANSACTION 3. 收到事务请求 4. onTransact() 处理 BC_REPLY 5. 驱动转发回复 BR_REPLY 6. 客户端收到回复

你看,整个过程分六步走:

  1. 客户端发起:调用 IPCThreadState::transact(),构造 binder_transaction_data,通过 ioctl() 发送 BC_TRANSACTION 命令。
  2. 驱动处理:Binder 驱动收到命令后,分配内核缓冲区,拷贝用户数据,查找目标服务端。
  3. 驱动通知服务端:驱动将事务放入服务端的待处理队列,并唤醒服务端线程。服务端在 talkWithDriver() 中收到 BR_TRANSACTION
  4. 服务端处理:服务端调用 onTransact() 执行业务逻辑,然后构造回复数据,发送 BC_REPLY
  5. 驱动转发回复:驱动将回复数据拷贝到客户端的内核缓冲区,发送 BR_REPLY 给客户端。
  6. 客户端收到回复:客户端从 ioctl() 返回,拿到回复数据。

核心要点BC_TRANSACTIONBR_TRANSACTION 是一对“请求-通知”命令。客户端主动发送请求,驱动被动通知服务端。服务端处理完后,通过 BC_REPLYBR_REPLY 完成回复。

12.3 关键代码路径

我们来看看客户端发送 BC_TRANSACTION 的代码片段:

// frameworks/native/libs/binder/IPCThreadState.cpp
status_t IPCThreadState::transact(int32_t handle, uint32_t code,
                                   const Parcel &data, Parcel *reply,
                                   uint32_t flags) {
    status_t err = data.errorCheck();
    if (err != NO_ERROR) return err;

    // 构造事务数据
    binder_transaction_data tr;
    tr.target.handle = handle;
    tr.code = code;
    tr.flags = flags;

    // 将 Parcel 数据拷贝到 tr 中
    err = writeTransactionData(BC_TRANSACTION, tr, data, reply);
    if (err != NO_ERROR) return err;

    // 等待回复(非 oneway 调用)
    if ((flags & TF_ONE_WAY) == 0) {
        err = waitForResponse(reply);
    }
    return err;
}

这里有个细节:writeTransactionData() 会把 BC_TRANSACTION 命令和 binder_transaction_data 写入到 mOut 缓冲区。然后 talkWithDriver() 通过 ioctl() 一次性发送出去。

我曾经踩过一个坑:TF_ONE_WAY 标志位如果设置错了,会导致客户端一直阻塞在 waitForResponse()。有一次我写了一个异步回调接口,忘了设这个标志,结果客户端线程卡死了。排查时发现 BR_REPLY 根本没回来,因为服务端根本没发回复。

12.4 驱动层的处理逻辑

驱动层收到 BC_TRANSACTION 后,会调用 binder_transaction() 函数。核心逻辑如下:

// kernel/drivers/android/binder.c
static void binder_transaction(struct binder_proc *proc,
                                struct binder_thread *thread,
                                struct binder_transaction_data *tr, ...) {
    // 1. 查找目标节点
    struct binder_node *target_node = binder_get_node(proc, tr->target.handle);

    // 2. 分配内核缓冲区
    struct binder_buffer *buffer = binder_alloc_buf(proc, tr->data_size, tr->offsets_size);

    // 3. 拷贝用户数据到内核缓冲区
    copy_from_user(buffer->data, tr->data.ptr.buffer, tr->data_size);

    // 4. 处理 Binder 对象引用(flat_binder_object)
    binder_translate_binder(tr, buffer, target_node);

    // 5. 将事务挂到目标进程的待处理队列
    t->work.type = BINDER_WORK_TRANSACTION;
    list_add_tail(&t->work.entry, &target_proc->todo);

    // 6. 唤醒目标进程
    wake_up_interruptible(&target_proc->wait);
}

你想想看,这里最关键的一步是 binder_translate_binder()。它负责把客户端传过来的 Binder 引用(handle)转换成服务端能识别的 Binder 节点指针。这就是 Binder 驱动的“地址翻译”能力。

个人经验:调试 Binder 问题时,我经常在驱动层加 printk 打印 binder_transaction 的入参。特别是 tr->target.handletr->code,能快速定位是哪个服务、哪个接口出了问题。

12.5 服务端的接收与回复

服务端在 talkWithDriver() 中收到 BR_TRANSACTION 后,会调用 executeCommand() 处理:

status_t IPCThreadState::executeCommand(int32_t cmd) {
    switch (cmd) {
    case BR_TRANSACTION: {
        binder_transaction_data tr;
        // 从 mIn 缓冲区读取事务数据
        mIn.read(&tr, sizeof(tr));

        // 构造 Parcel 对象
        Parcel data, reply;
        data.ipcSetDataReference(tr.data.ptr.buffer, tr.data_size, tr.offsets.offsets, tr.offsets_size);

        // 调用 onTransact()
        status_t error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, data, &reply, tr.flags);

        // 发送回复
        if ((tr.flags & TF_ONE_WAY) == 0) {
            sendReply(reply, 0);
        }
        break;
    }
    // ...
    }
}

注意这里的 tr.cookie,它是在服务端注册 Binder 服务时设置的。驱动在转发 BR_TRANSACTION 时,会把 cookie 原样传回来。这样服务端就知道该把事务交给哪个 BBinder 对象处理。

我记得有一次,服务端收到 BR_TRANSACTION 后,onTransact() 返回了 UNKNOWN_TRANSACTION。结果客户端那边收到了 BR_FAILED_REPLY,抛了个 DeadObjectException。排查后发现是服务端接口版本没对齐,客户端调了一个新加的接口,服务端还没更新。

12.6 避坑指南

我曾经踩过的坑,你一定要注意:

  • 数据大小限制:Binder 单次传输的数据量默认不能超过 1MB。如果你传大文件,记得分片或者用共享内存。
  • oneway 调用:如果不需要回复,一定要设 TF_ONE_WAY 标志。否则服务端不回复,客户端会一直阻塞。
  • 线程池耗尽:服务端默认只有 16 个线程处理请求。如果并发太高,新请求会排队。我曾经遇到过服务端线程卡在 I/O 上,导致所有客户端超时。
  • Binder 对象传递:跨进程传 Binder 对象时,驱动会自动转换 handle。但如果你在同一个进程内传,驱动会直接传指针,不会走 handle 转换。

12.7 小结

BC_TRANSACTIONBR_TRANSACTION 是 Binder IPC 的基石。客户端通过 BC_TRANSACTION 发起请求,驱动负责中转,服务端通过 BR_TRANSACTION 收到请求。处理完后,通过 BC_REPLYBR_REPLY 完成回复。

理解了这个交互过程,你就掌握了 Binder 通信的“骨架”。后面再聊服务注册、死亡通知这些高级话题,就轻松多了。


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