Skip to content

ADB文件传输

追踪 adb push、pull、sync 和 exec-in/out 的路由、同步协议、文件状态与失败清理。

基于android-17.0.0_r1
AndroidADB文件同步调试协议

ADB文件传输 ​

adb push、adb pull 和 adb sync 都在“复制文件”,但它们并不只是把字节写进 USB:host 先取得设备 feature,再连接 sync: service,按照文件同步协议交换路径、元数据、数据块和最终 结果。另一方面,adb exec-out 也常被拿来“导出文件”,它走的是 exec: raw 子进程,不经过 sync:,因此没有文件名、权限和时间戳语义。

本文面向已经读过 ADB架构 和 ADB shell机制 的读者:你需要知道 host client、host server、transport、adbd 和 raw stream 的边界,但不要求 预先记住同步协议的消息编号。本文不展开 USB/TCP transport 建立、shell protocol、APK 安装事务或 JDWP 调试协议;网络连接见 ADB网络与转发,安装事务见 ADB安装与权限管理。读完后,你应能从一条 push 或 exec-out 命令找到真实源码入口,判断结果何时才算提交,并解释失败时为什么目标文件可能被删除。

1. 桥接模型 ​

1.1 sync ​

commandline.cpp 把 push、pull、sync 分派到 do_sync_push、do_sync_pull 或同步分区逻辑。 file_sync_client.cpp 中的 SyncConnection 随后调用 adb_connect("sync:")。adbd 的 services.cpp 看到 sync: 前缀后创建独立的 file_sync_service 线程:

源码文件:packages/modules/adb/daemon/services.cpp

相关函数/类型:service dispatch

cpp
} else if (name.starts_with("sync:")) {
    return create_service_thread("sync", file_sync_service);
}

这个线程拥有本次同步连接的设备端 fd、读写缓冲和当前请求,但不拥有 host 的源文件,也不负责 transport 握手。目标文件的 owner 是设备文件系统和 file_sync_service 的写入逻辑;host 侧的 SyncConnection 只负责发送请求、读取结果和报告进度。

1.2 exec-in/out ​

adb exec-out command 在 client 中组装 exec:command,再连接到 adbd:

源码文件:packages/modules/adb/client/commandline.cpp

相关函数/类型:exec-out dispatch

cpp
std::string cmd = "exec:";
cmd += argv[1];
...
unique_fd fd(adb_connect(cmd, &error));
if (exec_in) {
    copy_to_file(STDIN_FILENO, fd.get());
} else {
    copy_to_file(fd.get(), STDOUT_FILENO);
}

adbd 的 services.cpp 对 exec: 调用 StartSubprocess(..., kRaw, kNone)。这里没有远端 shell、 没有 shell protocol packet,也没有文件属性提交;stdin/stdout 只是进程的字节流。

命令service目标 owner返回能证明什么
adb push a /data/local/tmp/async:设备路径、权限和元数据文件同步请求得到 OKAY
adb pull /data/local/tmp/a .sync:host 输出文件收到完整 RECV 数据并写入本地
adb exec-out cat /data/local/tmp/a > aexec:cat 进程 stdoutraw 字节被读到 host,不证明属性复制
cat input | adb exec-in catexec:远端 cat stdinhost 输入被送到进程,不证明文件已落盘

2. 同步协议与消息 ​

2.1 path_length ​

file_sync_protocol.h 定义了 v1/v2 共用的消息 id。每个请求通常先有固定大小的 header,随后 是路径或数据;路径不是 NUL 结尾,而是由 path_length 指定长度,最大为 1024。

消息方向作用关键后继
STAT / STAT2host → adbd查询单路径属性stat_v1/v2
LIST / LIST2host → adbd列目录多个 DENT,最后 DONE
SEND / SEND2host → adbdhost 写设备DATA…DONE,最后 OKAY/FAIL
RECV / RECV2host → adbd设备读给 hostDATA…DONE,或 FAIL
DATA双向一块数据,最大 64 KiB下一个 DATA 或结束帧
DONE双向传输结束,并携带时间戳或 0OKAY / 接收结束
QUIThost → adbd结束 sync 连接orderly shutdown
FAILadbd → host失败及错误文本host 返回非零

sync_status 的 id 为 OKAY 时 msglen 必须为 0;为 FAIL 时,后面跟着错误文本。因此 host 不能只看 socket 是否关闭来判断成功,必须消费明确的协议结果。

2.2 push ​

单文件 adb push local /data/local/tmp/remote 的主要路径如下:

  1. commandline.cpp 解析参数,进入 do_sync_push。
  2. SyncConnection 连接 sync:,先对目标执行 STAT/STAT2,判断目标是文件还是目录。
  3. host 对源文件执行 stat,计算目标路径和 mode。
  4. 设备端 handle_sync_command() 收到 SEND/SEND2,进入 send_impl()。
  5. host 分块发送 DATA,最后发送 DONE 和源文件时间戳。
  6. adbd 写入目标 fd、应用 mode/uid/gid/capability,成功后发送 OKAY;host 读取 ack 后才把 该文件从 deferred acknowledgement 队列移除。

2.3 v1 ​

设备 feature 决定 SyncConnection 是否使用 sendrecv_v2,以及是否可用 Brotli、LZ4 或 Zstd。 SEND2 的 setup packet 携带 mode 和 flags;压缩器只改变 DATA 的编码,不改变最终文件的 路径、mode 或时间戳语义。设备不支持 v2 时,host 回退到 SEND,把路径和 mode 拼在同一字符串中, 并且没有 v2 的 dry-run 能力。

能力host 行为不能推出
sendrecv_v2使用 SEND2/RECV2不保证任意压缩算法可用
sendrecv_v2_zstd 等选择对应 encoder/decoder不改变文件校验语义
sendrecv_v2_dry_run_sendadb push -n 可只模拟写入不代表所有设备都支持 dry-run
无 v2使用 legacy SEND/RECV不代表传输一定失败

CompressionType::Any 的 Android 17 优先顺序是 Zstd、LZ4、Brotli,最终仍由 feature 集合决定。 脚本若需要可重复的性能比较,应显式使用 -z 或 -Z,否则不同设备可能选择不同编码器。

3. 设备写入 ​

3.1 目标创建 ​

send_impl() 并不是一开始就覆盖目标文件。它先判断目标类型:普通文件或需要替换的符号链接会被 删除,然后 handle_send_file() 以 O_CREAT|O_EXCL 尝试创建;已存在时再按实现路径打开。目录 不存在时,secure_mkdirs() 创建父目录,并根据 adbd_fs_config、SELinux 和 capability 规则设置 属性。

数据全部读完后,设备端才应用 capability 和时间戳并发送 OKAY。因此 OKAY 是本次 send 的提交 信号;在此之前看到目标路径存在,并不能证明内容完整。失败分支会继续读取剩余 DATA,直到看到 DONE 或连接结束,防止旧版本 host 持续写入时把错误响应留在 socket 中。

3.2 失败清理 ​

如果文件是本次传输新建的,或原目标是普通文件/需要替换的链接,do_unlink 为真。设备端失败时 会执行 adb_unlink(path);这意味着“push 失败后目标文件消失”可能是主动清理,而不是文件系统随机 损坏。对一个原本存在且保持原类型的目标,具体覆盖行为还受打开和权限分支影响,不能把所有失败都 概括成“保留旧文件”。

3.3 host延迟ack ​

为了不让设备端写 buffer 被频繁的 host 读取阻塞,SyncConnection 把已发送文件放进 deferred_acknowledgements_。只有队列达到上限、需要同步等待或传输即将结束时,才调用 ReadAcknowledgements() 读取 OKAY/FAIL。所以进度条显示“已发送字节”不等于远端已经提交; 脚本必须等待 adb push 进程成功返回。

4. pull / sync ​

4.1 pull ​

do_sync_pull() 通过 STAT 或 LIST 解析源路径,再对每个远端文件发送 RECV。设备端 recv_impl() 打开远端 fd,以 DATA 块发送,最后发送 DONE;host 写入本地文件并在传输结束后 关闭 fd。pull 成功证明的是 host 输出文件接收完毕,不自动证明源文件在传输期间没有被其他进程 修改。

如果源路径是目录,host 会先列目录并为每个 entry 建立本地路径。目录中的符号链接、同名文件和 本地目标目录类型都会改变行为;不要把单文件 checksum 实验外推为所有目录语义。

bash
adb shell 'printf source > /data/local/tmp/bas022-source'
adb pull /data/local/tmp/bas022-source /tmp/bas022-copy
cmp /tmp/bas022-copy <(printf source)
adb shell rm /data/local/tmp/bas022-source

这里 cmp 证明的是字节内容一致;它不证明 mode、uid、gid、SELinux label 或 mtime 被复制到 host。

4.2 Sync实现 ​

adb sync [PARTITION] 不是新的 wire protocol。client 把 ANDROID_PRODUCT_OUT 下的 system、 vendor、product 等目录与设备对应路径比较,仍然通过 STAT/LIST/SEND 传输。-l 只列出将 要复制的文件,-n 请求 dry-run;实际是否能 dry-run 取决于 sendrecv_v2_dry_run_send。

adb push --sync 也只是让 host 在 copy_local_dir_remote() 中根据时间和大小跳过已有文件, 不是设备端的事务提交。多文件同步中某个文件失败,已经成功的前序文件不会自动回滚;恢复应重新 运行同步或显式删除目标,而不是假设整个目录具有原子性。

场景关键输入结果消费者不能证明
push 单文件host stat、目标 path、mode设备文件系统应用已读取新文件
pull 单文件远端 fd、host 输出路径host 文件远端传输期间未变化
push --sync时间戳与 size 比较copy planner目录级原子更新
sync -l/-nproduct out 与 feature计划输出/设备 dry-run实际写权限一定可用

5. exec-in/out ​

5.1 raw stream ​

exec-out 适合把 dumpsys、cat、自定义诊断程序的 stdout 直接导出到 host:

bash
adb exec-out getprop > getprop.txt
adb exec-out cat /data/local/tmp/trace.bin > trace.bin

它不会启动远端 shell,因此 shell 变量、重定向、管道和 stderr 合并都不适用。命令参数由 client 拼成 exec: service 字符串,再由 adbd 以 raw、无 shell protocol 的子进程执行。输出边界就是 进程 stdout;程序退出码不会像 shell v2 那样通过 exit packet 传回成为独立字段。

exec-in 方向相反:

bash
printf 'hello\n' | adb exec-in cat

它证明输入字节到达了 cat,不证明 cat 将数据写入持久文件。若要证明落盘,应让远端程序自己 写文件,再用 adb pull 或 adb shell stat 读取结果。

5.2 断开和清理 ​

copy_to_file() 只是循环读写 fd。host 关闭 stdin、server 被杀或 transport 断开时,远端 raw 子进程会收到 EOF、写错误或连接关闭;adbd 的进程回收路径负责关闭 fd。与 sync: 不同,exec: 没有 FAIL 消息和远端文件 unlink 语义,所以调试脚本必须自行处理部分输出和临时文件。

6. 传输定位 ​

6.1 失败分层 ​

现象首个源码入口典型 owner下一步
adb: error: failed to connect to syncSyncConnection 构造函数transport/service 路由查设备状态和 sync: service
path too longSendRequest / handle_sync_command协议 path length缩短路径,不要重试同一请求
Permission deniedhandle_send_file/recv_impladbd UID、文件 mode、SELinux先用 adb shell ls -lZ 查看 owner/label
传输中途失败ReadAcknowledgements、ID_FAIL设备 fd/文件系统记录 FAIL 文本,检查目标是否被清理
push 返回成功但应用看不到OKAY 后的消费者应用缓存/配置读取重新触发应用读取,不要只重复 push
exec-out 输出截断copy_to_file/transport closeraw 子进程或连接检查远端进程退出和连接断开,改用 pull 验证文件

6.2 源码路径 ​

bash
rg -n "do_sync_push|do_sync_pull|class SyncConnection" /path/to/aosp/packages/modules/adb/client
rg -n "ID_SEND_V2|ID_RECV_V2|ID_OKAY|ID_FAIL|ID_DONE" /path/to/aosp/packages/modules/adb/file_sync_protocol.h /path/to/aosp/packages/modules/adb/daemon/file_sync_service.cpp
rg -n "exec-in|exec-out|StartSubprocess" /path/to/aosp/packages/modules/adb/client/commandline.cpp /path/to/aosp/packages/modules/adb/daemon/services.cpp

读到 ID_DATA 时继续追两侧的 owner:host 是 SyncConnection 的文件读取和 encoder,设备侧是 handle_send_file_data 或 recv_impl 的 fd 读写。读到 OKAY/FAIL 时再追 ack 队列和清理分支; 只看协议常量表无法解释失败后目标文件的状态。

6.3 可执行验证 ​

Android ADB 的设备测试覆盖了比最小实验更宽的边界:test_push/test_pull 使用 checksum 验证 内容,test_push_error_reporting 和 test_pull_error_reporting 验证权限错误文本,test_push_sync 验证目录比较,dry-run 测试断言目标文件不被修改。测试输入、断言和边界可在 packages/modules/adb/test_device.py 的对应方法中逐项核对。

读者可以先执行下面四组最小实验:

bash
printf 'bas022\n' >/tmp/bas022-host
adb push /tmp/bas022-host /data/local/tmp/bas022-device
adb shell 'cat /data/local/tmp/bas022-device'
adb shell 'stat -c "%s %Y" /data/local/tmp/bas022-device'

adb pull /data/local/tmp/bas022-device /tmp/bas022-pull
cmp /tmp/bas022-host /tmp/bas022-pull

adb push -n /tmp/bas022-host /data/local/tmp/bas022-device
adb exec-out cat /data/local/tmp/bas022-device | cmp - /tmp/bas022-host

adb shell rm -f /data/local/tmp/bas022-device
rm -f /tmp/bas022-host /tmp/bas022-pull

第 1、2 组证明文件内容路径上的双向传输;stat 只证明设备看到的 size/mtime,不证明 SELinux label 或应用已经重新读取。第 3 组在设备不支持 dry-run 时应明确失败,不能把失败当作文件未改变的证明。 第 4 组证明 raw stdout 字节一致,不证明 exec-out 复制了 mode、uid、gid 或 mtime。

没有设备时,仍可由固定的 Android 17 源码确认 wrapper、协议字段、feature 分支、FAIL 清理和 exec: 路由;但不能证明目标产品的 SELinux 策略、文件系统 quota、压缩性能、厂商 adbd 修改或 transport 断开时序。把这些条件写清楚,才能区分“源码已证明”和“设备运行结果待验证”。

7. 命令到消费者 ​

拿一条具体命令复述下面的链路:

  1. commandline.cpp 选择了 sync: 还是 exec:,是否先做 feature/stat/list 查询?
  2. 哪个对象拥有传输状态:SyncConnection、adbd sync service,还是 raw 子进程?
  3. 哪个消息或 EOF 表示数据阶段结束,哪个结果表示提交成功?
  4. 失败时 host 看到什么,设备端是否删除新建文件,连接由谁关闭?
  5. 最终消费者是设备文件系统、host 输出文件,还是调试工具的 stdin/stdout?

例如 adb push 的完整答案必须包含 do_sync_push → SyncConnection → SEND2/DATA/DONE → send_impl → OKAY/FAIL,并指出 host 进度和 OKAY 的时间不同;adb exec-out cat 则应回答 exec: → StartSubprocess(raw) → stdout copy,并明确它没有文件属性和独立退出码协议。这种复述 能够把命令实践连接回真实源码,而不是停留在“push 是上传、pull 是下载”的词汇记忆。