Binder(四):ioctl(BINDER_WRITE_READ) 之后,事务怎样到达目标进程?

Binder(四):ioctl(BINDER_WRITE_READ) 之后,事务怎样到达目标进程?

第三篇已经建立了远端对象链:

text 复制代码
BinderProxy
    → BpBinder(handle)
    → binder_ref
    → binder_node
    → 服务端本地 Binder 实体

现在客户端 IPCThreadState 已经持有:

text 复制代码
目标 handle
transaction code
data Parcel
flags

并执行:

cpp 复制代码
ioctl(fd, BINDER_WRITE_READ, &bwr)

驱动接下来必须完成的是:

Binder Driver 收到这些数据后,怎样解析目标、把事务准备到另一个进程、转换其中的 Binder 对象,并让服务端线程读到 BR_TRANSACTION

调用栈在驱动中的主干如下:

text 复制代码
IPCThreadState::transact()
    ↓ writeTransactionData()
BC_TRANSACTION + binder_transaction_data
    ↓ talkWithDriver()
ioctl(BINDER_WRITE_READ)
    ↓
binder_ioctl()
    ↓
binder_ioctl_write_read()
    ↓
binder_thread_write()
    ↓
binder_transaction()
    ↓
handle → binder_ref → binder_node → target_proc
    ↓
准备目标 Binder buffer
    ↓
复制普通数据 + 转换 Binder 对象 / fd
    ↓
进入明确目标线程或目标进程工作队列
    ↓
需要时唤醒相应 Binder 线程
    ↓
binder_thread_read()
    ↓
BR_TRANSACTION

驱动函数、字段和行号按导读第四节"系列统一基线"中的固定 commit 定位。本文跟踪 BC_TRANSACTIONBR_TRANSACTION 的正常主路径。

进入驱动前,先认清五类对象

五类对象足够描述一次路由

内核结构很多,但解释一次事务只需要先抓住五类:

内核对象 表示什么 与本次事务的关系
binder_proc 一个打开 Binder Driver 的进程上下文 发送方和目标进程各有一个
binder_thread 进程中一条与驱动交互的线程上下文 发送事务、等待 reply 或接收工作
binder_node 服务端本地 Binder 实体的内核表示 决定目标端点及其拥有进程
binder_ref 某进程对远端 node 的引用 把发送方 handle 解析到 node
binder_transaction 一笔正在传递或等待处理的事务 连接请求、目标工作和 reply

可以先把关系压缩成:

text 复制代码
发送方 binder_proc
    └─ binder_thread
    └─ binder_ref(handle)
            └─ binder_node
                    └─ 目标 binder_proc
                            └─ 目标 binder_thread / proc work queue

handle 到目标进程的路由图

读图时只看四个阶段:

text 复制代码
命令入口:BC_TRANSACTION
目标解析:handle → ref → node → proc
数据交付:buffer + 特殊对象转换 + work queue
用户态出口:BR_TRANSACTION / BR_REPLY

不要把驱动理解成"直接调用 Stub"。驱动交付的是事务和端点标识,服务端 libbinder 才会继续调用 BBinderJavaBBinder 和 AIDL Stub。

用户态交给驱动的命令与数据

IPCThreadState::writeTransactionData() 把输出缓冲区组织成:

text 复制代码
BC_TRANSACTION
        +
binder_transaction_data

Android Common Kernel UAPI 的 binder_transaction_data 关键字段可以整理为:

c 复制代码
struct binder_transaction_data {
    union {
        __u32 handle;        // 发送请求时的远端 handle
        binder_uintptr_t ptr;
    } target;

    binder_uintptr_t cookie;
    __u32 code;
    __u32 flags;
    pid_t sender_pid;
    uid_t sender_euid;
    binder_size_t data_size;
    binder_size_t offsets_size;

    union {
        struct {
            binder_uintptr_t buffer;
            binder_uintptr_t offsets;
        } ptr;
    } data;
};

发送请求时,用户态主要提供:

text 复制代码
target.handle:目标远端引用
code:接口方法编号
flags:同步 / oneway 等事务标记
data_size:普通数据区大小
offsets_size:特殊对象位置表大小
buffer / offsets:发送方用户态 Parcel 数据

sender_pidsender_euid 不是让发送方随意伪造后被服务端信任的普通业务字段。即使 UAPI struct 在发送侧也能看见同名存储,客户端写入的数值也不是接收端可信身份来源;驱动依据真实发送任务建立事务来源,并在交付接收侧时填入可信 calling context。第七篇再解释这个上下文怎样进入 IPCThreadState

target 是 union,同一块存储在不同阶段有不同解释,不能把 handleptrcookie 当成一笔事务里同时由客户端填写的三个目标字段:

阶段 target 的解释
客户端向驱动发送普通事务 target.handle:发送进程内的远端引用编号
驱动向目标进程返回事务 target.ptr + cookie:让目标进程 libbinder 恢复本地端点
reply 根据原事务和 transaction_stack 定位等待线程,不再把它当普通目标 handle

表中的 handle、binder_refbinder_node 对象身份已由第三篇建立;本篇只回答 binder_transaction() 怎样实际使用它们。

BINDER_WRITE_READ 同时承载写命令和读结果

binder_write_read 同时携带:

c 复制代码
struct binder_write_read {
    binder_size_t write_size;
    binder_size_t write_consumed;
    binder_uintptr_t write_buffer;

    binder_size_t read_size;
    binder_size_t read_consumed;
    binder_uintptr_t read_buffer;
};

它允许一次 ioctl 中同时表达:

text 复制代码
用户态还有哪些 BC_* 命令要写给驱动;
用户态准备了多少空间接收驱动返回的 BR_* 命令。

因此 BINDER_WRITE_READ 不是"只写一笔事务"的系统调用。IPCThreadState::talkWithDriver() 可以批量提交输出命令,也可以在同一次驱动往返中读取事务、reply、引用通知、死亡通知或线程池命令。

这也是为什么命名是:

text 复制代码
WRITE_READ

而不是:

text 复制代码
SEND_TRANSACTION

ioctl 进入 binder_transaction() 的入口链

固定内核基线中的调用入口是:

text 复制代码
binder_ioctl()                         # binder.c:6034
    ↓ BINDER_WRITE_READ
binder_ioctl_write_read()              # binder.c:5697
    ↓ 有 write_size
binder_thread_write()                  # binder.c:4340
    ↓ 读到 BC_TRANSACTION
binder_transaction()                   # binder.c:3248

源码入口:

binder_transaction() 才进入本篇主问题:目标是谁、数据放哪里、工作交给谁。

handle 经 binder_ref 找到 node 与目标进程

非 reply 的普通事务先检查目标 handle。

1. 非零 handle

固定基线的 binder_transaction() 目标解析 主干是:

c 复制代码
ref = binder_get_ref_olocked(
        proc,
        tr->target.handle,
        true);

if (ref) {
    target_node = binder_get_node_refs_for_txn(
            ref->node,
            &target_proc,
            &return_error);
}

注意查找条件同时包含:

text 复制代码
发送方 binder_proc
+
发送方提交的 handle

所以 handle 的进程内作用域在驱动代码中是实实在在的。

2. handle 0

tr->target.handle == 0 是上下文管理者的特殊入口:

c 复制代码
target_node = context->binder_context_mgr_node;

这为 ServiceManager 的引导发现保留了一个大家预先约定的根端点。第五篇再展开 handle 0 怎样建立以及服务名怎样解析成普通 Binder 引用。

3. node 为什么能确定目标进程?

binder_node 保存:

text 复制代码
node->proc

也就是拥有本地 Binder 实体的 binder_proc。驱动取得 node 后,就能得到目标进程并为其准备事务。

事务怎样变成目标进程的工作

为目标进程准备 Binder buffer

找到 target_proc 后,驱动不是把发送方用户指针直接交给服务端。

固定基线在 binder_transaction() 为目标进程分配 Binder buffer:

c 复制代码
t->buffer = binder_alloc_new_buf(
        &target_proc->alloc,
        tr->data_size,
        tr->offsets_size,
        extra_buffers_size,
        is_oneway);

然后把普通数据从发送方用户空间复制到目标进程的 Binder buffer:

c 复制代码
binder_alloc_copy_user_to_buffer(
        &target_proc->alloc,
        t->buffer,
        user_offset,
        user_buffer + user_offset,
        remaining_size);

服务端用户态稍后得到的是目标 buffer 在自己地址空间中的映射地址。

因此,不要用一句"Binder 零拷贝"概括整条路径。更稳妥的说法是:

驱动把发送方用户空间数据复制到目标进程可访问的 Binder buffer;接收侧通过映射访问这块目标 buffer,从而减少额外的接收侧复制,但事务仍然发生了发送方到目标 buffer 的数据复制。

普通字节复制与 Binder 对象转换是两件事

Parcel 中有两类内容:

text 复制代码
普通数据:int、String、Parcelable 字段等;
特殊对象:Binder 引用、文件描述符、buffer 对象等。

普通数据可以按字节复制。特殊对象不能。

驱动根据 offsets 表逐个识别对象类型,并进入不同转换分支,例如:

text 复制代码
binder_translate_binder()
binder_translate_handle()
binder_translate_fd()

其目标不是理解业务,而是维护跨进程语义:

text 复制代码
本地 Binder 实体 → 接收进程自己的 binder_ref / handle
远端 handle → 解析原 node,再映射为接收进程自己的引用
文件描述符 → 在目标进程 fd 表中安装对应 fd

这解释了为什么 Parcel 还要提交对象 offsets:驱动必须知道 buffer 中哪些位置不能当普通字节透明复制。

把事务放进明确线程或进程工作队列

数据和对象转换完成后,驱动要先选择工作的下一存放位置。对于当前可以交付的事务,驱动需要:

  1. 交给明确目标线程或目标进程工作队列;
  2. 必要时唤醒目标 Binder 线程。

工作可能交给:

text 复制代码
一条明确的目标 binder_thread
或
目标 binder_proc 的待处理工作队列

普通新事务通常进入目标 binder_proc 工作队列,由可用线程领取;嵌套事务、reply 或已经有明确目标线程的分支,也可以直接面向特定 binder_thread

对于客户端发出的、需要 reply 的同步请求,固定基线的 !TF_ONE_WAY 分支 还会:

text 复制代码
设置 need_reply;
通过 from_parent 接上发送线程原有的 transaction_stack;
把当前事务压入发送线程的 transaction_stack。

这条 transaction_stack 保存的是等待 reply 的同步调用关系,用于 reply 关联、嵌套事务和重入;它不是所有 Binder work 的通用队列。

对于 TF_ONE_WAY 请求,上述压栈分支不会执行。共享的 binder_proc_transaction() 会使用 node 的:

text 复制代码
has_async_transaction
async_todo

维护同一 node 的异步事务串行化:当前没有异步事务占用该 node 时,这笔 oneway 可以进入目标线程 / 进程队列;已有一笔时,后续 oneway 先留在 async_todo,等待前一笔释放串行位置。两种情况都不建立等待 reply 所需的发送线程事务栈。其并发语义留到第六篇。

服务端从 read 路径收到 BR_TRANSACTION

目标 Binder 线程通过自己的 BINDER_WRITE_READ 读取工作。驱动在 binder_thread_read() 中把事务整理为接收侧 binder_transaction_data

c 复制代码
if (t->buffer->target_node) {
    trd->target.ptr = target_node->ptr;
    trd->cookie = target_node->cookie;
    cmd = BR_TRANSACTION;
} else {
    cmd = BR_REPLY;
}

trd->code = t->code;
trd->flags = t->flags;
trd->sender_euid = ...;
trd->data.ptr.buffer = t->buffer->user_data;

然后把:

text 复制代码
BR_TRANSACTION
+
接收侧 transaction_data

复制到服务端线程的 read buffer。

服务端 IPCThreadState::executeCommand(BR_TRANSACTION) 才会:

text 复制代码
用目标 buffer 建立 Parcel;
通过 target.ptr / cookie 找本地 BBinder;
调用 BBinder / JavaBBinder 的 transact 入口。

所以驱动交付到用户态的不是 Stub 对象,而是一份足以让本进程 libbinder 找到本地端点的事务描述。

同步 reply 沿 transaction stack 返回

同步事务不是两条互不相关的消息。

发送请求时,驱动把事务挂到客户端线程的事务栈;服务端处理时,事务也记录当前接收线程和上层事务关系。服务端发送 BC_REPLY 后,驱动通过 in_reply_to 找到正在等待的原事务和目标线程:

text 复制代码
请求 T1
客户端 thread A 等待
    ↓
服务端 thread B 处理
    ↓ BC_REPLY(in_reply_to = T1)
驱动找到原客户端 thread A
    ↓
向 A 交付 BR_REPLY

这套事务栈还是嵌套 Binder 调用、线程复用和重入成立的基础。第六篇再把它放进线程模型。

用方向理解 BC_*BR_*

名字不是"客户端命令"和"服务端命令",而是相对驱动方向:

text 复制代码
BC_*:Binder Command
用户态写给驱动

BR_*:Binder Return
驱动返回给用户态

因此两边进程都可能发送 BC_TRANSACTION

text 复制代码
App 调 system_server:App 写 BC_TRANSACTION
system_server 回调 App:system_server 写 BC_TRANSACTION

两边也都可能收到 BR_TRANSACTIONBR_REPLY,取决于当前事务角色。

驱动能路由事务,却不理解业务方法

驱动能处理 驱动不理解
目标 handle / node Java 类名和方法名
transaction code / flags code 为什么代表 add()
数据 buffer 与 offsets int 的业务含义
Binder 对象和 fd Parcelable 的领域模型
sender UID / PID 服务端业务权限规则
目标进程、线程和队列 ActivityManager 的业务状态

边界如下:

text 复制代码
AIDL Proxy / Stub 理解接口协议;
Binder Driver 理解对象引用、事务和路由。

驱动拒绝事务的几个位置

阶段 典型问题 上层可能看到
目标解析 handle 无效 BR_FAILED_REPLY / RemoteException
node/proc 生命周期 目标已死亡 BR_DEAD_REPLY / DeadObjectException
buffer 分配 目标 buffer 空间不足 failed transaction / 事务过大
用户数据复制 指针或 offsets 非法 BR_FAILED_REPLY
特殊对象转换 Binder 对象或 fd 无法转换 事务失败
reply 路由 等待链目标死亡 dead reply

这说明"驱动收到 ioctl"不等于"服务端方法已经开始执行"。至少还要经过目标解析、buffer 准备、对象转换和工作排队。

Perfetto 证据:区分"已发送"与"已执行"

在可调试设备上,可以录制包含 Binder Driver 和调度信息的 Perfetto trace:

powershell 复制代码
adb shell perfetto `
  -o /data/misc/perfetto-traces/binder.perfetto-trace `
  -t 10s `
  sched binder_driver

adb pull /data/misc/perfetto-traces/binder.perfetto-trace

设备版本、权限和可用 atrace 类别会影响命令;官方 Perfetto Android tracing 也推荐同时采集 schedbinder_driver,以便把事务和线程调度放在一条时间线上。

观察时不要只找一条 Binder slice,而要对齐:

text 复制代码
客户端开始 transact 的时间;
驱动事务事件;
服务端 Binder 线程被唤醒和开始运行的时间;
服务端用户态方法 slice;
reply 返回客户端的时间。

这样才能区分:

text 复制代码
驱动已经接收,但目标线程尚未执行;
目标线程已经执行,但服务端业务尚未返回;
服务端已经返回,但客户端尚未恢复运行。

BinderLab API 36 证据包只有 Java 业务 marker,没有 Perfetto Binder flow,因此不能用于声称已经测得驱动路由或排队时间。要区分发送、目标排队、服务端执行和 reply 恢复,需要把完整 Binder flow、调度事件和用户态埋点放到同一时间线;单条 Binder slice 不足以确定业务根因。

BC_TRANSACTIONBR_TRANSACTION

text 复制代码
1. IPCThreadState 把 handle、code、flags 和 Parcel 组织成 BC_TRANSACTION;
2. BINDER_WRITE_READ 把用户态写命令和读缓冲一起交给驱动;
3. binder_transaction() 在发送方 binder_proc 中用 handle 查 binder_ref;
4. binder_ref 指向 binder_node,node 再确定 target_proc;
5. 驱动为目标进程准备 Binder buffer;
6. 普通数据复制到目标 buffer,Binder 对象和 fd 按接收方语义转换;
7. 当前可交付的 binder_transaction 进入明确目标线程或目标进程工作队列,并在需要时唤醒相应 Binder 线程;同一 node 后续 oneway 可能先等待在 async_todo;
8. 服务端 binder_thread_read() 把工作返回为 BR_TRANSACTION;
9. 同步 reply 再沿事务栈回到原客户端线程。

第 7 步中的目标线程 / 进程队列负责投递 Binder work;transaction_stack 只描述需要 reply 的同步调用关系,不能把两者合并成一条通用排队机制。第 9 步也只属于同步分支,oneway 不等待这条 reply。

这条路径解释了"已有 handle 时怎样投递事务"。系统刚启动或客户端第一次连接服务时,却还没有这个 handle;从名字取得首个 Binder 引用的问题,要回到 ServiceManager 的注册表和 handle 0。

源码与官方文档

相关推荐
AFinalStone2 小时前
Android 7系统休眠唤醒(二)开机全链路—BootROM到Launcher
android·电源管理·休眠唤醒
Mr YiRan2 小时前
Android NDK开发之统计到未被回收的图片
android
浮江雾3 小时前
Flutter第十七节-----路由管理(3)
android·开发语言·前端·javascript·flutter·入门
admin and root3 小时前
「移动安全」安卓APP 反编译&frida脱壳技巧分享
android·开发语言·python·web安全·微信小程序·移动安全·攻防演练
ihuyigui4 小时前
海外签收通知短信接口
android·java·开发语言·前端·数据库·后端
大尚来也5 小时前
老项目 PHP 5.6 升级 PHP 8 完整迁移步骤与兼容坑汇总
android·adb
Android研究员5 小时前
Android进阶之事件分发机制深度剖析
android·前端·面试
峥嵘life6 小时前
Android WiFi连接过程 wpa_supplicant 日志分析
android·开发语言·php
Android小码家7 小时前
OWASP 移动应用安全之授权(MASTG-DEMO-0090)
android·安全·frida