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_TRANSACTION 到 BR_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 才会继续调用 BBinder、JavaBBinder 和 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_pid 和 sender_euid 不是让发送方随意伪造后被服务端信任的普通业务字段。即使 UAPI struct 在发送侧也能看见同名存储,客户端写入的数值也不是接收端可信身份来源;驱动依据真实发送任务建立事务来源,并在交付接收侧时填入可信 calling context。第七篇再解释这个上下文怎样进入 IPCThreadState。

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

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

表中的 handle、binder_ref 和 binder_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_TRANSACTION 或 BR_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 也推荐同时采集 sched 与 binder_driver,以便把事务和线程调度放在一条时间线上。

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

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

这样才能区分:

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

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

从 BC_TRANSACTION 到 BR_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。

源码与官方文档

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui