scp 命令详解与参数实测(Ubuntu 22.04)

以 Ubuntu 22.04 OpenSSH 8.9p1 为实测环境,逐参数验证 scp:本地与远程互传、-r 递归、-p 时间戳保留、-P 端口与大小写陷阱、-i 密钥免密、-3 远程对传、-O/-s 协议选择、-l 限速与进度条解读、-C 压缩、-c 加密算法与错误排障,每项附真实回显。

Linux命令Linux系统UbuntuUbuntu 22.04命令行网络工具2026/10/16

scp 命令详解与参数实测(Ubuntu 22.04)

目标:全面掌握 scp 的每一个常用参数——本地与远程互传、远程路径语法、递归复制、时间戳保留、压缩传输、端口指定、密钥免密、远程对传、协议选择、限速与进度条解读、加密算法指定与错误排障,全部命令与回显已在 Ubuntu 22.04(OpenSSH 8.9p1)实测通过。

一、环境信息

本节列出实测环境的完整信息,文中所有命令与回显均来自该环境,读者在同版本系统上可复现同样的结果。

  • 操作系统:Ubuntu 22.04.5 LTS(jammy),架构 x86_64,内核 6.8.0-138-generic
  • OpenSSH 套件:openssh-client / openssh-server 均已安装,版本 OpenSSH_8.9p1 Ubuntu-3ubuntu0.18,加密库 OpenSSL 3.0.2——scp 与 ssh 同属 OpenSSH 套件,版本一致
  • sshd 监听:默认实例监听 22 端口;为实测 -P 参数,另启动了一个监听 2222 端口的测试实例
  • 测试账号:普通用户 tester(密码 tester123)、tester1(密码 tester1pass)——两个账号均为公开演示环境专用的测试凭据
  • 密码自动化工具:sshpass 1.09(apt 安装),用于在非交互脚本中自动填充密码
  • 相关路径:/root/.ssh/(客户端私钥与 known_hosts)、/root/demo/scp/(演示数据目录)
  • 网络方式:传输目标全部使用本机回环地址 127.0.0.1 自连自收,本地与"远程"同机,全程不依赖公网

二、命令概览

2.1 语法与测试数据

scp(secure copy)是 OpenSSH 套件中基于 SSH 加密通道的文件复制工具,语法与传统的 cp 类似,但源或目标可以是远程路径。其基本语法为:

scp [选项] 源路径 ... 目标路径

路径写法是 scp 的核心规则:

  • 本地路径:直接写,如 /root/demo/scp/note.txt 或 ./note.txt;
  • 远程路径:[user@]host:路径 冒号形式,冒号前是主机(可带用户名),冒号后是远端文件系统路径,如 tester@127.0.0.1:~/note.txt;
  • 远程到远程:源与目标都是远程路径,如 scp user1@host1:/a.txt user2@host2:/b.txt。

由此构成三种最常用的形态:

  • scp 本地文件 user@host:路径——本地推送到远程;
  • scp user@host:远程文件 本地路径——从远程拉取到本地;
  • scp user1@host1:文件 user2@host2:文件——两台远程主机之间复制。

⚠️ 注意:远程路径的冒号是硬规则——tester@127.0.0.1~/note.txt 少了冒号会被当成一个奇怪的本地文件名;而 scp 的端口参数是大写 -P,与 ssh 的小写 -p 恰好相反(因为 scp 的小写 -p 已经被「保留时间戳」占用,见 3.5 与 3.7 节)。

本文的测试数据约定:

  • 目标主机:127.0.0.1(回环自连),默认端口 22,测试实例端口 2222;
  • 登录账号:tester,密码 tester123,用 sshpass -p tester123 前缀实现非交互传输(仅演示环境如此使用,生产环境推荐密钥认证,见 3.8 节);
  • 演示文件:/root/demo/scp/src/note.txt(内容三行、35 字节)等,每个实验块自带数据准备与清理,可独立复现。

2.2 usage 用法总览

不带任何参数直接执行 scp,会打印完整的 usage 用法说明(逐行照抄如下):

$ scp
usage: scp [-346ABCOpqRrsTv] [-c cipher] [-D sftp_server_path] [-F ssh_config]
           [-i identity_file] [-J destination] [-l limit]
           [-o ssh_option] [-P port] [-S program] source ... target

从中可以看到 scp 的参数全集,本文逐项实测其中最常用的一批,对应关系如下:

参数作用实测小节
(无参数)本地↔远程互传3.1
(远程路径写法)相对路径 / 绝对路径 / 复制到目录3.2
(同名目标)静默覆盖行为3.3
-r递归复制目录3.4
-p保留文件时间戳3.5
-C压缩传输3.6
-P指定连接端口3.7
-i指定私钥文件3.8
-3远程→远程经本地中转3.9
-O / -s传统 SCP 协议 / SFTP 协议3.10
-l限速传输(含进度条解读)3.11
-q / -v静默 / 调试输出3.12
-c指定加密算法3.13
-o向底层 ssh 透传参数3.14

💡 说明:usage 中还有 -A、-B(批处理模式)、-D(指定 sftp-server 路径)、-F(指定 ssh 配置文件)、-J(跳板机)、-R、-S(指定 ssh 程序)、-T(禁用严格文件名检查)、-4/-6(强制 IPv4/IPv6)等参数,使用频率较低,本文不逐项展开。

2.3 版本信息

scp 自身没有独立的版本查询参数(usage 中无 -V),它与 ssh 共用同一套程序库,版本通过 ssh -V 查询:

ssh -V
OpenSSH_8.9p1 Ubuntu-3ubuntu0.18, OpenSSL 3.0.2 15 Mar 2022

💡 说明:ssh -V 的输出打印在标准错误上(用 ssh -V 2>&1 才能重定向捕获)。排障时报版本号报这一个就够——scp 与 ssh、sftp 同版本发布。特别注意:OpenSSH 9.0 起 scp 的默认传输协议从传统 SCP 协议切换为 SFTP 协议(8.9 仍默认传统协议,可用 -s/-O 显式指定,见 3.10 节),跨版本迁移脚本前先确认两端版本。

三、参数逐项实测

3.1 基础形态:本地→远程与远程→本地

先验证 scp 最基础的推、拉两个方向。一个重要观察:传输成功时 scp 完全静默,不输出任何内容,只有退出码可以确认成败。

准备一个三行的演示文件,并清理远端旧数据:

mkdir -p /root/demo/scp/src /root/demo/scp/dst && printf "hello from scp\nline two\nline three\n" > /root/demo/scp/src/note.txt && chmod 644 /root/demo/scp/src/note.txt && cat /root/demo/scp/src/note.txt
hello from scp
line two
line three
sshpass -p tester123 ssh tester@127.0.0.1 "rm -f ~/note.txt; echo remote-cleaned"
remote-cleaned

本地→远程推送(源是本地路径,目标是 tester@127.0.0.1:~/):

sshpass -p tester123 scp /root/demo/scp/src/note.txt tester@127.0.0.1:~/; echo rc=$?
rc=0

登录远端核对文件内容与大小:

sshpass -p tester123 ssh tester@127.0.0.1 "ls -l ~/note.txt; cat ~/note.txt"
-rw-r--r-- 1 tester tester 35 Oct 15 20:09 /home/tester/note.txt
hello from scp
line two
line three

远程→本地拉取(源是 tester@127.0.0.1:~/note.txt,目标是本地路径):

rm -f /root/demo/scp/dst/note.txt; sshpass -p tester123 scp tester@127.0.0.1:~/note.txt /root/demo/scp/dst/note.txt; echo rc=$?
rc=0
cat /root/demo/scp/dst/note.txt
hello from scp
line two
line three

验证结论:

  • 推、拉只是源与目标的位置互换,命令结构完全对称;
  • 成功时无任何输出,rc=0 是唯一的成功信号;出错的回显与退出码见 3.15 节;
  • 目标以 / 结尾时表示"复制到该目录下"(保持源文件名),~/ 即远端家目录。

💡 说明:sshpass -p tester123 是演示环境的自动化辅助手段(把密码喂给 scp 底层的 ssh 认证)。生产环境不建议在命令行明文传密码(会留在 Shell 历史与进程列表中),推荐 3.8 节的密钥认证。

3.2 远程路径语法:相对路径、绝对路径与复制到目录

远程冒号后的路径支持三种写法:相对远端家目录的相对路径、绝对路径、以斜杠结尾的目录。先在远端准备一个子目录文件:

sshpass -p tester123 ssh tester@127.0.0.1 "mkdir -p ~/docs && printf deep > ~/docs/deep.txt && echo prepared"
prepared

写法一:相对路径(docs/deep.txt 相对远端家目录 /home/tester):

sshpass -p tester123 scp tester@127.0.0.1:docs/deep.txt /root/demo/scp/dst/deep.txt; echo rc=$?; cat /root/demo/scp/dst/deep.txt
rc=0
deep

写法二:绝对路径:

sshpass -p tester123 scp tester@127.0.0.1:/home/tester/docs/deep.txt /root/demo/scp/dst/deep2.txt; echo rc=$?; cat /root/demo/scp/dst/deep2.txt
rc=0
deep

写法三:目标以 / 结尾,复制到远端目录下(文件名保持不变):

sshpass -p tester123 scp /root/demo/scp/src/note.txt tester@127.0.0.1:/home/tester/docs/; echo rc=$?
rc=0
sshpass -p tester123 ssh tester@127.0.0.1 "ls -l ~/docs/"
total 8
-rw-rw-r-- 1 tester tester  4 Oct 15 20:09 deep.txt
-rw-r--r-- 1 tester tester 35 Oct 15 20:09 note.txt

💡 说明:注意 ls -l 里两个文件的权限不同——deep.txt 是 -rw-rw-r--(远端 shell 用 printf 创建,受远端账号 umask 影响得到 664),而传输过来的 note.txt 是 -rw-r--r--。这是因为传统 SCP 协议在传输时会把源文件的权限位一并携带(3.10 节 -v 输出中的 Sink: C0644 35 note.txt 就是在传递「权限 0644、35 字节」这一信息),远端按该权限落盘。

3.3 覆盖行为:静默覆盖,无任何确认

scp 没有交互确认,也没有"跳过已存在文件"的选项——同名文件直接覆盖。用两版内容做对照:

printf "version-1\n" > /tmp/over.txt; sshpass -p tester123 scp /tmp/over.txt tester@127.0.0.1:~/over.txt; sshpass -p tester123 ssh tester@127.0.0.1 "cat ~/over.txt"
version-1
printf "version-2-overwritten\n" > /tmp/over.txt; sshpass -p tester123 scp /tmp/over.txt tester@127.0.0.1:~/over.txt; echo rc=$?
rc=0
sshpass -p tester123 ssh tester@127.0.0.1 "cat ~/over.txt"
version-2-overwritten

⚠️ 注意:第二次传输零提示覆盖了远端文件——没有 overwrite? 提问,没有备份。脚本化批量部署时要意识到 scp 是"无条件覆盖"语义;需要保护旧文件时应先在远端改名或备份,或改用支持 --backup 的 rsync。

3.4 -r:递归复制目录

复制目录必须加 -r,否则直接报错。准备一个两级目录并先清空远端:

mkdir -p /root/demo/scp/tree/sub && printf A > /root/demo/scp/tree/a.txt && printf B > /root/demo/scp/tree/sub/b.txt && find /root/demo/scp/tree | sort
/root/demo/scp/tree
/root/demo/scp/tree/a.txt
/root/demo/scp/tree/sub
/root/demo/scp/tree/sub/b.txt
sshpass -p tester123 ssh tester@127.0.0.1 "rm -rf ~/tree; echo remote-tree-cleaned"
remote-tree-cleaned

不带 -r 复制目录,报错形态:

sshpass -p tester123 scp /root/demo/scp/tree tester@127.0.0.1:~/ 2>&1; echo rc=$?
/root/demo/scp/tree: not a regular file
rc=1

加 -r 后整棵目录树落盘:

sshpass -p tester123 scp -r /root/demo/scp/tree tester@127.0.0.1:~/; echo rc=$?
rc=0
sshpass -p tester123 ssh tester@127.0.0.1 "find ~/tree | sort"
/home/tester/tree
/home/tester/tree/a.txt
/home/tester/tree/sub
/home/tester/tree/sub/b.txt

💡 说明:报错信息是 not a regular file 而不是 cp 的 omitting directory,脚本里判断这类错误要按 scp 的措辞来。另注意目标写法对结果结构的影响——scp -r tree tester@127.0.0.1:~/ 会把 tree 目录本身复制到家目录下;若目标写成 ~/tree2/ 则远端出现 tree2/ 目录,内容与源目录对齐。

3.5 -p:保留文件修改时间

-p(小写)保留源文件的修改时间(mtime)与访问时间(atime)。准备一个时间戳被改到 2026-01-01、权限 640 的文件:

printf preserve > /root/demo/scp/keep.txt && touch -d "2026-01-01 10:00:00" /root/demo/scp/keep.txt && chmod 640 /root/demo/scp/keep.txt && stat -c "%a %y %n" /root/demo/scp/keep.txt
640 2026-01-01 10:00:00.000000000 +0000 /root/demo/scp/keep.txt

对照组:不加 -p 传输,远端权限与时间:

sshpass -p tester123 scp /root/demo/scp/keep.txt tester@127.0.0.1:~/keep.txt; echo rc=$?
rc=0
sshpass -p tester123 ssh tester@127.0.0.1 "stat -c %a ~/keep.txt; stat -c %y ~/keep.txt"
640
2026-10-15 20:09:51.718524651 +0000

实验组:清理远端后加 -p 传输:

sshpass -p tester123 scp -p /root/demo/scp/keep.txt tester@127.0.0.1:~/keep.txt; echo rc=$?
rc=0
sshpass -p tester123 ssh tester@127.0.0.1 "stat -c %a ~/keep.txt; stat -c %y ~/keep.txt"
640
2026-01-01 10:00:00.000000000 +0000

验证结论:

  • 不加 -p:权限位 640 依然保留(权限随传统 SCP 协议的 C0640 指令传递),但 mtime 变成了传输时刻 2026-10-15 20:09:51;
  • 加 -p:mtime 精确保留为源文件的 2026-01-01 10:00:00.000000000。

⚠️ 注意:-p 与端口的 -P 只差大小写,两个参数经常在同一命令中出现(scp -P 2222 -p file host:~/),务必区分。备份归档场景建议始终带 -p,否则按 mtime 做增量判断的下游工具会全部失准。

💡 技术拓展:SFTP 模式下的属性行为与传统模式一致(3.10 节实测 -s 模式同样默认落盘权限 640、加 -s -p 后 mtime 精确保留为 2026-05-05 08:00:00),两个协议在这一点上无差异。需要更完整的属性保留(如属主、符号链接、设备文件)请看 rsync 的 -a 参数,它在 scp 之上做了更细的拆分。

3.6 -C:压缩传输

-C 让底层的 SSH 连接启用压缩,对高冗余数据(日志、重复文本)能显著减少网络字节量。本机环回无法直接测量网络字节,但压缩是否生效可以在 -v 调试输出的密钥交换行里看到证据。准备 500KB 高重复文本:

yes "the quick brown fox jumps over the lazy dog" | head -c 500000 > /root/demo/scp/pack.txt && chmod 644 /root/demo/scp/pack.txt && wc -c /root/demo/scp/pack.txt
500000 /root/demo/scp/pack.txt

不加 -C,抓底层加密协商行:

sshpass -p tester123 scp -v /root/demo/scp/pack.txt tester@127.0.0.1:~/pack.txt 2>&1 | grep "kex: client->server cipher"
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none

加 -C 再抓一次:

sshpass -p tester123 scp -C -v /root/demo/scp/pack.txt tester@127.0.0.1:~/pack.txt 2>&1 | grep "kex: client->server cipher"
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: zlib@openssh.com

💡 说明:compression: none 与 compression: zlib@openssh.com 就是 -C 生效的直接证据——它等价于给底层 ssh 加了 -o Compression=yes。已压缩数据(zip、jpg、视频)再开 -C 几乎无收益还白耗 CPU;WAN 慢链路上传日志类文本时收益最大。

3.7 -P:指定端口(大写!与 ssh 的 -p 相反)

scp 连接非默认端口用大写 -P。用监听 2222 的 sshd 测试实例验证:

sshpass -p tester123 scp -P 2222 /root/demo/scp/src/note.txt tester@127.0.0.1:~/port2222.txt; echo rc=$?
rc=0
sshpass -p tester123 ssh tester@127.0.0.1 "cat ~/port2222.txt; rm -f ~/port2222.txt"
hello from scp
line two
line three

写成小写 -p 2222 会发生什么——2222 被当成了源文件名(小写 -p 是"保留时间戳"参数,见 3.5 节):

sshpass -p tester123 scp -p 2222 /root/demo/scp/src/note.txt tester@127.0.0.1:~/ 2>&1; echo rc=$?
2222: No such file or directory
rc=1

指定一个没有服务监听的端口,报错形态:

sshpass -p tester123 scp -P 2221 /root/demo/scp/src/note.txt tester@127.0.0.1:~/ 2>&1; echo rc=$?
ssh: connect to host 127.0.0.1 port 2221: Connection refused
lost connection
rc=1

⚠️ 注意:Connection refused 表示主机可达但端口无人监听,排查方向是服务端 ss -tln;Connection timed out 则是网络层不通或防火墙丢包。lost connection 是 scp 层的收尾报错,真正的原因在它上面一行。

3.8 -i:指定私钥文件(ed25519 密钥免密传输)

与 ssh 一样,scp 支持用 -i 指定私钥做公钥认证,实现无密码自动传输。本节从零实测:生成密钥 → ssh-copy-id 分发公钥 → -i 免密推拉。

第一步,生成一把专用 ed25519 密钥(-N "" 表示不设口令):

ssh-keygen -t ed25519 -f /root/.ssh/demo_scp_ed25519 -N "" -C "scp-demo"
Generating public/private ed25519 key pair.
Your identification has been saved in /root/.ssh/demo_scp_ed25519
Your public key has been saved in /root/.ssh/demo_scp_ed25519.pub
The key fingerprint is:
SHA256:JsK2p87MOjUIrdYg4t7zrZ2dAg4aq5Lavesl0t8U8XU scp-demo
The key's randomart image is:
+--[ED25519 256]--+
|                 |
|                 |
| .      .   . E  |
|+...     o . .   |
|+oo.+ . S .      |
|.+.=o+ o .       |
|o.*.*.+ .        |
|o=.O Bo+o .      |
|* o=&=o+oo       |
+----[SHA256]-----+

第二步,把公钥装进远端账号(此后该账号接受这把私钥的认证):

sshpass -p tester123 ssh-copy-id -i /root/.ssh/demo_scp_ed25519.pub tester@127.0.0.1 2>&1 | tail -2
and check to make sure that only the key(s) you wanted were added.

第三步,-i 免密推送(不再需要 sshpass):

scp -i /root/.ssh/demo_scp_ed25519 /root/demo/scp/src/note.txt tester@127.0.0.1:~/key-sent.txt; echo rc=$?
rc=0
sshpass -p tester123 ssh tester@127.0.0.1 "cat ~/key-sent.txt"
hello from scp
line two
line three

-i 拉取同样免密:

scp -i /root/.ssh/demo_scp_ed25519 tester@127.0.0.1:~/key-sent.txt /root/demo/scp/dst/key-pulled.txt; echo rc=$?; cat /root/demo/scp/dst/key-pulled.txt
rc=0
hello from scp
line two
line three

错误形态:-i 误指向公钥文件(.pub),ssh 拒绝加载过于开放的"私钥":

scp -i /root/.ssh/demo_scp_ed25519.pub /root/demo/scp/src/note.txt tester@127.0.0.1:~/x.txt 2>&1; echo rc=$?
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/root/.ssh/demo_scp_ed25519.pub' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/root/.ssh/demo_scp_ed25519.pub": bad permissions
Permission denied, please try again.
Permission denied, please try again.
tester@127.0.0.1: Permission denied (publickey,password).
lost connection
rc=1

💡 说明:私钥文件权限必须是 600(仅属主可读),否则 ssh 会拒绝使用并报 WARNING: UNPROTECTED PRIVATE KEY FILE。密钥认证配好后,scp 自动化脚本、定时任务都不再依赖 sshpass,这是生产环境的标准做法。

3.9 -3:两台远程主机之间经本地中转

源与目标都是远程主机时,-3 指定数据经由本地主机中转(先从源主机收到本地,再发往目标主机)。前提是本地对两台主机都有认证凭据——实测中两个账号都装了同一把公钥(3.8 节方法对 tester1 再执行一次即可)。

把 tester 家目录的文件复制到 tester1 家目录:

scp -3 -i /root/.ssh/demo_scp_ed25519 tester@127.0.0.1:~/key-sent.txt tester1@127.0.0.1:~/three.txt; echo rc=$?
rc=0
sshpass -p tester1pass ssh tester1@127.0.0.1 "cat ~/three.txt"
hello from scp
line two
line three

用 -v 观察 -3 的连接结构——本地发起了两条独立连接:

scp -3 -v -i /root/.ssh/demo_scp_ed25519 tester@127.0.0.1:~/key-sent.txt tester1@127.0.0.1:~/three.txt 2>&1 | grep -c "debug1: Connecting to 127.0.0.1"
2

再看两条连接各自的用途(Executing 行显示本地让两个远端分别执行的程序):

scp -3 -v -i /root/.ssh/demo_scp_ed25519 tester@127.0.0.1:~/key-sent.txt tester1@127.0.0.1:~/three.txt 2>&1 | grep -E "Executing|Connecting to"
Executing: program /usr/bin/ssh host 127.0.0.1, user tester, command scp -v -f ~/key-sent.txt
Executing: 2nd program /usr/bin/ssh host 127.0.0.1, user tester1, command scp -v -t ~/three.txt
debug1: Connecting to 127.0.0.1 [127.0.0.1] port 22.
debug1: Connecting to 127.0.0.1 [127.0.0.1] port 22.

不加 -3 的对照(8.9 的传统协议同样通过本地两条连接完成,远端命令相同):

scp -v -i /root/.ssh/demo_scp_ed25519 tester@127.0.0.1:~/key-sent.txt tester1@127.0.0.1:~/three.txt 2>&1 | grep -E "Executing|Connecting to"
Executing: program /usr/bin/ssh host 127.0.0.1, user tester, command scp -v -f ~/key-sent.txt
Executing: 2nd program /usr/bin/ssh host 127.0.0.1, user tester1, command scp -v -t ~/three.txt
debug1: Connecting to 127.0.0.1 [127.0.0.1] port 22.
debug1: Connecting to 127.0.0.1 [127.0.0.1] port 22.

💡 说明:-f(from)是源端的读进程,-t(to)是目标端的写进程——两条连接分别跑一个 scp,数据流经本地中转。在 OpenSSH 8.9 的传统协议下,-3 与不带的连接结构实测相同;-3 的差异主要在语义与报错定位(两端进度与错误都回到本地展示,且按手册说明第二台主机进入批处理模式、密码只提示一次)。更老版本的 OpenSSH 中,不带 -3 的远程对传是"源主机直接连目标主机",此时本地并不需要目标主机的凭据——跨版本使用时要核对实际行为。

3.10 -O 与 -s:传统 SCP 协议与 SFTP 协议

scp 的数据传输有两种底层协议,可用 -O(大写字母 O)与 -s 显式选择:

  • 传统 SCP 协议(-O):OpenSSH 8.9 及之前的默认,远端执行 scp 程序收发数据;
  • SFTP 协议(-s):OpenSSH 9.0 起的默认,远端走 sftp-server 子系统。

传统协议的 -v 特征——远端执行 scp -v -t,且有 Sink: 行(接收进程打印的文件属性指令,含权限与字节数):

sshpass -p tester123 scp -O -v /root/demo/scp/src/note.txt tester@127.0.0.1:~/o-mode.txt 2>&1 | grep -E "Executing|Sink:"
Executing: program /usr/bin/ssh host 127.0.0.1, user tester, command scp -v -t ~/o-mode.txt
Sink: C0644 35 note.txt

SFTP 协议的 -v 特征——远端执行的程序变成 sftp,走子系统通道:

sshpass -p tester123 scp -s -v /root/demo/scp/src/note.txt tester@127.0.0.1:~/s-mode.txt 2>&1 | grep -E "Sending file|sftp|debug1: Sending subsystem"
Executing: program /usr/bin/ssh host 127.0.0.1, user tester, command sftp
debug1: Sending subsystem: sftp

两种模式传输结果一致(均 rc=0),SFTP 模式下属性行为也一致(-s 不加 -p 时 mtime 为传输时刻、权限 640 保留;-s -p 后 mtime 精确保留,实测过程与 3.5 节相同)。

💡 说明:Sink: C0644 35 note.txt 正是 3.5 节"权限默认随源传递"的协议层证据——C 指令携带了权限(0644)与字节数(35)。协议切换的影响主要在脚本兼容性:SFTP 模式对远端 shell 通配符、引号的处理与传统模式不同,且要求服务端启用 sftp 子系统;迁移到 9.0+ 后老脚本行为异常时,可先用 -O 回退旧协议过渡。

3.11 -l:限速传输(单位是 Kbit/s)与进度条解读

-l 限制传输带宽,单位是 Kbit/s(千比特每秒)而不是 KB/s——这是最容易误判的一点。生成 5MB 测试文件,限速 1000 Kbit/s 理论速率约 125 KB/s:

dd if=/dev/zero of=/root/demo/scp/five.bin bs=1M count=5 2>/dev/null && chmod 644 /root/demo/scp/five.bin && ls -l /root/demo/scp/five.bin
-rw-r--r-- 1 root root 5242880 Oct 15 20:09 /root/demo/scp/five.bin

限速传输耗时(time 计时,5MB ÷ 125KB/s ≈ 41 秒):

time script -qec "sshpass -p tester123 scp -l 1000 /root/demo/scp/five.bin tester@127.0.0.1:~/five.bin" /dev/null

real	0m41.165s
user	0m0.029s
sys	0m0.034s

不限速对照,仅 0.195 秒:

time script -qec "sshpass -p tester123 scp /root/demo/scp/five.bin tester@127.0.0.1:~/five.bin" /dev/null

real	0m0.195s
user	0m0.015s
sys	0m0.008s

进度条解读:进度条只在标准错误连接到终端时显示(脚本重定向时看不到),用 script 命令分配伪终端模拟真实终端场景(进度条用回车符原地刷新,这里把回车符转换为换行以便逐帧阅读;认证改用 ControlMaster 复用连接,避免 sshpass 包装层吞掉进度条输出)。截取限速传输的前三帧:

script -qec "scp -o ControlPath=/tmp/cm-scp -l 1000 /root/demo/scp/five.bin tester@127.0.0.1:~/five.bin" /dev/null | tr "\r" "\n" | grep -a "%" | head -3
five.bin                                        0%    0     0.0KB/s   --:-- ETA
five.bin                                        2%  144KB 140.5KB/s   00:35 ETA
five.bin                                        5%  272KB 138.9KB/s   00:34 ETA

最后两帧:

script -qec "scp -o ControlPath=/tmp/cm-scp -l 1000 /root/demo/scp/five.bin tester@127.0.0.1:~/five.bin" /dev/null | tr "\r" "\n" | grep -a "%" | tail -2
five.bin                                       98% 5024KB 125.1KB/s   00:00 ETA
five.bin                                      100% 5120KB 124.9KB/s   00:40

不限速的完成帧,速率回到 77.5MB/s:

script -qec "scp -o ControlPath=/tmp/cm-scp /root/demo/scp/five.bin tester@127.0.0.1:~/five.bin" /dev/null | tr "\r" "\n" | grep -a "%" | tail -1
five.bin                                      100% 5120KB  77.5MB/s   00:00

进度条各列含义:

列含义
five.bin正在传输的文件名
2% / 100%完成百分比
144KB / 5120KB已传输字节数
140.5KB/s / 124.9KB/s瞬时速率
00:35 ETA / 00:40预计剩余时间(传输中);完成后变为已用总时长

💡 说明:限速完成帧的 124.9KB/s 与 -l 1000(1000 Kbit/s ÷ 8 ≈ 125 KB/s)几乎完全吻合——单位实锤。另外实测确认:用 sshpass 包装 scp 时进度条会被其认证包装层吞掉(伪终端下输出字节数为 0,而不经 sshpass 的 ControlMaster 方式能正常显示),自动化场景想看进度只能换认证方式或改用 -v。

3.12 -q 与 -v:静默与调试

-q 关闭进度条与诊断信息(脚本里防止进度输出污染日志);-v 打开底层 ssh 的调试信息(排障用)。-q 的效果——伪终端下本应有进度条,加了 -q 后输出字节数为 0:

script -qec "scp -q -o ControlPath=/tmp/cm-scp /root/demo/scp/five.bin tester@127.0.0.1:~/five.bin" /dev/null | wc -c
0

-v 的调试信息会经底层 ssh 全量打印,常见关键行如下(分别来自前几节的实测):

Executing: program /usr/bin/ssh host 127.0.0.1, user tester, command scp -v -t ~/o-mode.txt
debug1: Connecting to 127.0.0.1 [127.0.0.1] port 22.
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: zlib@openssh.com
Sink: C0644 35 note.txt

💡 说明:-v 输出里最值得先看的行是 Connecting to(连没连上、连的哪个端口)、kex: 行(加密与压缩协商结果)和 Executing: 行(远端实际执行的程序与参数)。-v 可叠加到 -vvv,但日常排障一级足够。

3.13 -c:指定加密算法

-c(小写)指定会话加密算法,算法名必须是服务端支持的列表之一。用 aes128-gcm@openssh.com 传输:

sshpass -p tester123 scp -c aes128-gcm@openssh.com /root/demo/scp/src/note.txt tester@127.0.0.1:~/cipher.txt; echo rc=$?
rc=0

-v 核对协商结果:

sshpass -p tester123 scp -c aes128-gcm@openssh.com -v /root/demo/scp/src/note.txt tester@127.0.0.1:~/cipher.txt 2>&1 | grep "kex: client->server cipher"
debug1: kex: client->server cipher: aes128-gcm@openssh.com MAC: <implicit> compression: none

不加 -c 的默认值对照:

sshpass -p tester123 scp -v /root/demo/scp/src/note.txt tester@127.0.0.1:~/cipher.txt 2>&1 | grep "kex: client->server cipher"
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none

💡 说明:默认的 chacha20-poly1305@openssh.com 在无 AES 硬件加速的机器上通常更快;老 CPU 上 aes128-gcm 配合 AES-NI 反而占优。写错算法名会直接被拒绝(报 No matching cipher found 类错误)。

3.14 -o:向底层 ssh 透传参数

-o 后面直接写 ssh_config 格式的键值对,可出现多次,全部转给底层的 ssh 进程。实测透传 StrictHostKeyChecking 与 ConnectTimeout:

sshpass -p tester123 scp -o StrictHostKeyChecking=no -o ConnectTimeout=5 /root/demo/scp/src/note.txt tester@127.0.0.1:~/opt-pass.txt; echo rc=$?
rc=0

💡 说明:scp 自身参数有限,主机密钥策略、超时、压缩、认证方式等全部通过 -o 透传(3.11 节进度条实验里的 ControlPath 复用连接也是用它实现的)。常用透传项:StrictHostKeyChecking(首次连接策略)、ConnectTimeout(连接超时)、Compression(等价 -C)、IdentityFile(等价 -i)。

3.15 错误形态汇总

把本节各实验触发的错误集中对比,便于排障时对号入座:

sshpass -p tester123 scp /root/no-such.txt tester@127.0.0.1:~/ 2>&1; echo rc=$?
/root/no-such.txt: No such file or directory
rc=1
sshpass -p tester123 scp /root/demo/scp/tree tester@127.0.0.1:~/dir-no-r 2>&1; echo rc=$?
/root/demo/scp/tree: not a regular file
rc=1
sshpass -p tester123 scp /root/demo/scp/src/note.txt tester@127.0.0.1:/no/such/dir/ 2>&1; echo rc=$?
scp: /no/such/dir/: No such file or directory
rc=1
sshpass -p tester123 scp /root/demo/scp/src/note.txt tester@127.0.0.1:/root/forbidden.txt 2>&1; echo rc=$?
scp: /root/forbidden.txt: Permission denied
rc=1
sshpass -p wrongpass scp /root/demo/scp/src/note.txt tester@127.0.0.1:~/ 2>&1; echo rc=$?
Permission denied, please try again.
rc=5
报错rc原因与排查方向
源路径: No such file or directory1本地源文件不存在,检查路径拼写
源路径: not a regular file1源是目录,忘加 -r
scp: /远端路径/: No such file or directory1远端目标目录不存在,先在远端创建
scp: /远端路径: Permission denied1远端账号对目标目录无写权限
Permission denied, please try again.(rc=5)5认证失败;rc=5 是 sshpass 的错误码(密码错误),不是 scp 的
ssh: connect ... Connection refused + lost connection1端口无人监听,核对 -P 与服务端端口
WARNING: UNPROTECTED PRIVATE KEY FILE1私钥权限过松(非 600),chmod 600 修复

⚠️ 注意:scp 的本地侧错误 rc=1,而认证失败时 sshpass 返回 rc=5;远端错误信息带 scp: 前缀,本地错误不带——看前缀就能判断错误发生在哪一侧。

四、实战示例

4.1 批量部署配置文件到多台服务器

场景:一份应用配置要推到多台机器(用 tester、tester1 两个账号模拟两台服务器),密钥免密是前提(3.8 节方法分发公钥)。

准备配置文件并在两台"服务器"上创建目标目录:

mkdir -p /root/demo/scp/app && printf "server.port=8080\napp.name=demo\n" > /root/demo/scp/app/app.properties && chmod 644 /root/demo/scp/app/app.properties
sshpass -p tester123 ssh tester@127.0.0.1 "mkdir -p ~/app"; sshpass -p tester1pass ssh tester1@127.0.0.1 "mkdir -p ~/app"; echo dirs-ready
dirs-ready

循环推送(密钥认证下无任何交互):

scp -i /root/.ssh/demo_scp_ed25519 /root/demo/scp/app/app.properties tester@127.0.0.1:app/; scp -i /root/.ssh/demo_scp_ed25519 /root/demo/scp/app/app.properties tester1@127.0.0.1:app/; echo rc=$?
rc=0

两端分别核对内容:

sshpass -p tester123 ssh tester@127.0.0.1 "cat ~/app/app.properties"
server.port=8080
app.name=demo
sshpass -p tester1pass ssh tester1@127.0.0.1 "cat ~/app/app.properties"
server.port=8080
app.name=demo

💡 说明:目标写 app/(目录加斜杠)保持源文件名;结合 Shell 的 for 循环可以一行推进任意多台主机。多机循环时建议加 -o ConnectTimeout=5,避免单台失联拖死整个部署。

4.2 通配符拉取远端日志

场景:从远端按日期批量拉取日志到本地归档。远端准备三天的日志:

sshpass -p tester123 ssh tester@127.0.0.1 "mkdir -p ~/app/logs && for d in 01 02 03; do printf \"log-day-\$d\n\" > ~/app/logs/app-2026-02-\$d.log; done && ls -l ~/app/logs/"
total 12
-rw-rw-r-- 1 tester tester 11 Oct 15 20:14 app-2026-02-01.log
-rw-rw-r-- 1 tester tester 11 Oct 15 20:14 app-2026-02-02.log
-rw-rw-r-- 1 tester tester 11 Oct 15 20:14 app-2026-02-03.log

通配符拉取——远程路径整体加引号,让 * 交给远端展开:

rm -f /root/demo/scp/dst/app-*.log; sshpass -p tester123 scp "tester@127.0.0.1:app/logs/*.log" /root/demo/scp/dst/; echo rc=$?
rc=0
ls -l /root/demo/scp/dst/
total 12
-rw-r--r-- 1 root root 11 Oct 15 20:14 app-2026-02-01.log
-rw-r--r-- 1 root root 11 Oct 15 20:14 app-2026-02-02.log
-rw-r--r-- 1 root root 11 Oct 15 20:14 app-2026-02-03.log
cat /root/demo/scp/dst/app-2026-02-02.log
log-day-02

💡 说明:加不加引号决定了通配符在哪一侧展开——加引号 "host:app/logs/*.log" 由远端 shell 展开(拉日志的正确姿势);不加引号时本地 shell 先尝试展开,本地无匹配文件则原样传给远端,结果相同但依赖"本地恰好无同名文件"这一巧合,写脚本时务必加引号消除歧义。

4.3 整目录迁移并保留时间戳

场景:项目目录整体搬到另一台机器,要求层级、权限、修改时间都原样保留——-r 加 -p 组合,-i 指定密钥。

准备一个两级项目目录(main.conf 的 mtime 指到 2026-03-15、权限 640):

mkdir -p /root/demo/scp/project/conf && printf "key=value\n" > /root/demo/scp/project/conf/main.conf && touch -d "2026-03-15 09:30:00" /root/demo/scp/project/conf/main.conf && chmod 640 /root/demo/scp/project/conf/main.conf && find /root/demo/scp/project | sort
/root/demo/scp/project
/root/demo/scp/project/conf
/root/demo/scp/project/conf/main.conf

整目录迁移:

sshpass -p tester123 ssh tester@127.0.0.1 "rm -rf ~/project"
scp -i /root/.ssh/demo_scp_ed25519 -rp /root/demo/scp/project tester@127.0.0.1:~/; echo rc=$?
rc=0

远端核对结构、权限与时间戳:

sshpass -p tester123 ssh tester@127.0.0.1 "find ~/project | sort; stat -c %a ~/project/conf/main.conf; stat -c %y ~/project/conf/main.conf"
/home/tester/project
/home/tester/project/conf
/home/tester/project/conf/main.conf
640
2026-03-15 09:30:00.000000000 +0000

💡 说明:目录层级、640 权限与 2026-03-15 09:30:00 时间戳全部原样落地。-rp 是 scp 迁移场景的固定搭配;符号链接在 -r 下会按 scp 的默认策略处理,链接指向的内容与链接本身如何取舍若要求严格,应改用 rsync -a。

4.4 密钥免密前提下的两台主机中转(-3 全流程)

场景:A 机数据要转到 B 机,本地只是中转站。完整流程四步:生成专用密钥 → 公钥分发到两端 → -3 中转 → 验证。

第一步生成密钥并分发公钥到两台主机(ssh-copy-id 需要各输一次密码,此处用 sshpass 演示):

ssh-keygen -t ed25519 -f /root/.ssh/demo_scp_ed25519 -N "" -C "scp-demo"
Generating public/private ed25519 key pair.
Your identification has been saved in /root/.ssh/demo_scp_ed25519
Your public key has been saved in /root/.ssh/demo_scp_ed25519.pub
The key fingerprint is:
SHA256:JsK2p87MOjUIrdYg4t7zrZ2dAg4aq5Lavesl0t8U8XU scp-demo
The key's randomart image is:
+--[ED25519 256]--+
|                 |
|                 |
| .      .   . E  |
|+...     o . .   |
|+oo.+ . S .      |
|.+.=o+ o .       |
|o.*.*.+ .        |
|o=.O Bo+o .      |
|* o=&=o+oo       |
+----[SHA256]-----+
sshpass -p tester123 ssh-copy-id -i /root/.ssh/demo_scp_ed25519.pub tester@127.0.0.1 2>&1 | tail -2
and check to make sure that only the key(s) you wanted were added.
sshpass -p tester1pass ssh-copy-id -i /root/.ssh/demo_scp_ed25519.pub tester1@127.0.0.1 2>&1 | tail -2
and check to make sure that only the key(s) you wanted were added.

第二步把一份文件先推到源主机(tester),随后从源主机中转到目标主机(tester1):

scp -i /root/.ssh/demo_scp_ed25519 /root/demo/scp/src/note.txt tester@127.0.0.1:~/key-sent.txt; echo rc=$?
rc=0
scp -3 -i /root/.ssh/demo_scp_ed25519 tester@127.0.0.1:~/key-sent.txt tester1@127.0.0.1:~/three.txt; echo rc=$?
rc=0

第三步登录目标主机验证:

sshpass -p tester1pass ssh tester1@127.0.0.1 "cat ~/three.txt"
hello from scp
line two
line three

💡 说明:-3 中转的两条连接都从本地发起(3.9 节 -v 证据),所以本地必须同时持有 A、B 两端的凭据——这正是第一步把同一把公钥分发到两端的原因。数据量很大且两台远程主机之间网络更好时,应登录 A 机直接 scp 文件 B机:路径,让数据走 A→B 直连而不绕行本地。

五、注意事项与常见问题

Q1:scp 报 not a regular file 是什么错? 源是目录但没加 -r。scp 对目录不递归,必须显式 -r(见 3.4 节实测);cp 的同类错误措辞是 omitting directory,两者报错不同,脚本匹配要区分。

Q2:为什么 scp 的端口参数是大写 -P,ssh 却是小写 -p? scp 的小写 -p 在 OpenSSH 时代之前(rcp 血统)就被"保留时间戳"占用了,端口只能用 -P;ssh 没有时间戳需求,用小写 -p。实测把 -p 2222 写进 scp,2222 会被当成源文件名报 2222: No such file or directory(见 3.7 节)——同一条命令里 -P 2222 -p 同时出现是合法且常见的(指定端口 + 保留时间戳)。

Q3:-l 1000 限速后为什么远比预期慢? -l 的单位是 Kbit/s 不是 KB/s:1000 Kbit/s ≈ 125 KB/s,实测 5MB 文件用时 41 秒、完成帧速率 124.9KB/s(见 3.11 节)。想限 1MB/s 应写 -l 8192。

Q4:传输成功后没有任何输出,怎么确认文件真的传过去了? 三个手段:看退出码(echo $? 为 0);远端 ls -l 核对大小与时间;自动化脚本里加 -q 静默、出错才有输出。进度条只在标准错误连接终端时显示——重定向到文件或被 sshpass 包装时都看不到(3.11 节实测)。

Q5:权限和时间戳到底会保留哪些? 权限位默认随传统 SCP 协议的 C0644 指令传递(3.5、3.10 节实测),-p 的增量是 mtime/atime;不加 -p 时远端 mtime 是传输时刻,按时间做增量判断的工具会失准。属主与属组 scp 不处理(以目标账号落盘)。

Q6:OpenSSH 9.0 起 scp 默认协议变了,老脚本会受影响吗? 可能。9.0 前默认传统 SCP 协议(远端执行 scp 程序),9.0 起默认 SFTP 协议(远端走 sftp-server 子系统,见 3.10 节)。差异点:远端 shell 通配符与引号的处理、要求服务端启用 sftp 子系统、错误信息措辞变化。过渡期用 -O 强制旧协议、-s 显式新协议,先 -s -v 验证再全量切换。

Q7:脚本里如何安全地自动填密码? 推荐密钥认证(3.8 节),一次配置永久免密。临时演示可用 sshpass -p 密码 scp ...,但注意:命令行密码会进入 Shell 历史;认证失败时 sshpass 返回 rc=5(与 scp 自身的 rc=1 区分);sshpass 还会吞掉 scp 的进度条输出(3.11 节实测),需要看进度时改用 ControlMaster 复用连接等免交互认证方式。

Q8:通配符应该展开在哪一侧? 远程路径里的通配符加引号交给远端展开(4.2 节 "host:app/logs/*.log");不加引号本地 shell 会先尝试展开,可能产生意外匹配或不展开原样传递。多文件本地源则相反,直接写 *.log 由本地展开即可。

六、参考链接