Linux 工具实战:可靠下载、SSH 传输与定时任务

把文件下载下来、拷到机器人上、每天执行一次脚本,看起来都是一句命令。
真正麻烦的是:下载了一半却被下游使用,SSH 连到了错误的机器,或者定时任务只在手工启动时正常。

这篇讨论 wgetscpcrontab,重点不是开关数量,而是给操作定义可靠的完成条件。
完整工具导航见 Linux 工具学习地图
拿到程序后如果运行不了,接着读 ELF 与动态链接篇

复现与工程示例分开

在完整网站源码仓库根目录执行:

bash docker/linux-tools-lab/run.sh build
bash docker/linux-tools-lab/run.sh test
bash docker/linux-tools-lab/run.sh render
bash docker/linux-tools-lab/run.sh snapshot

下载 复现说明验证状态原始会话 可以核对实际执行范围。
动画使用真实输出重新排版,不是屏幕录像。
静态解释给出同样的操作逻辑,系统选择减少动态效果时使用 PNG。

实验的 wget 使用容器内回环 HTTP 服务,scp 使用隔离的临时 SSH 服务。
它们不上传到真实机器人,也不修改用户的 SSH 配置。
crontab 动画展示精简环境下执行任务脚本,不代表已经等待真实 cron 守护进程按时触发。
真实调度验收需要额外检查服务、时间、身份和日志;不能把 env -i 的成功当成完整调度通过。

下文 robot-lab/opt/robot/var/lib/robot-maint 等名称是工程模板。
使用前替换成自己有权限的目标,创建并检查对应目录,不要原样向未知机器发送文件。

wget:下载成功不等于文件可用了

把一个下载拆成三个结果

第一层是连接成功:DNS、TCP、TLS 等是否完成。
第二层是传输成功:服务器返回了什么状态,响应内容是否完整写入。
第三层是制品正确:内容是否确实是期望的版本、格式和校验值。

HTTP 200 也可能返回登录页或错误提示;扩展名是 .tar.gz 不会强迫响应变成压缩包。
一个可靠的脚本必须把“获得了字节”与“获得了正确制品”区分开。

以有权访问的发行包为例,先在独立目录中试下载,不直接覆盖正在使用的文件:

download_dir="$(mktemp -d)"
artifact_url='https://downloads.example.org/releases/v1.2.3/model.tar.gz'

wget --server-response \
--timeout=20 \
--tries=3 \
--output-document="$download_dir/model.tar.gz.part" \
"$artifact_url"

这里的 example.org 地址是占位,不是可运行的下载源。
--output-document/-O 选择下载内容文件,--output-file/-o 选择日志文件,大小写不能混用。
-O 有截断输出文件的语义,不应把它当成无风险的“改个名字”。Ubuntu 22.04 的 GNU Wget 手册

wget 从隔离回环服务下载实验文件,再比较校验值,区分传输完成和内容正确

查看原尺寸动画

进度条以外还应该看什么

阅读响应信息时,先看最终状态码、重定向位置、长度与内容类型,再看本地落盘文件。
一个跳转到登录页的请求可以有完整进度条,但仍没有拿到模型。
长度相同也不是内容一致的证据。

file "$download_dir/model.tar.gz.part"
wc -c "$download_dir/model.tar.gz.part"
sha256sum "$download_dir/model.tar.gz.part"

校验值要与可信发布说明、签名验证后的清单或已经固定的制品记录比较。
如果校验值和制品都来自同一个被替换的不可信页面,“两者匹配”并不能确认发布者身份。
先建立信任来源,再用 hash 检测传输或存储中的变化。

最小练习:在实验副本里将文本文件改名为 model.tar.gz
比较 file、解包命令和文件扩展名,解释为什么脚本不能仅根据名称判断下载成功。

续传要求本地文件是同一实体的前缀

wget --continue \
--timeout=20 \
--tries=3 \
'https://downloads.example.org/releases/v1.2.3/model.tar.gz'

续传适合已经存在一部分内容且远端实体未变化的文件。
-c 不会替你证明本地前半段属于当前远端文件;同一个 URL 被换成更大版本时,拼接可能产生损坏结果。
服务器是否支持 Range、是否经代理、响应实体有没有变化,都需要考虑。

因此,优先选不可变版本地址,不要长期对 latest/model.tar.gz 盲目续传。
完成后仍做完整校验,失败则保留证据并重新获取,不把“这次没报网络错”当作正确性保证。
服务器不支持续传时,具体回退行为要看工具输出,别假设原文件一定不被覆盖。

限速与超时不是同一个维度

wget --limit-rate=2m \
--dns-timeout=10 \
--connect-timeout=10 \
--read-timeout=30 \
--tries=3 \
--output-document="$download_dir/model.tar.gz.part" \
"$artifact_url"

限速控制传输节奏,适合与其他业务共享网络;重试处理某些失败,不保证所有 HTTP 错误都会重新尝试。
read timeout 关注读操作的停滞,不是给整个下载设置一个绝对总时长。
需要任务总预算时,另由任务调度器或外层超时控制,并保留部分文件的状态。

如果下载在后台自动运行,优先保留适量错误日志,不要习惯性使用 -q 把所有线索消掉。
日志可能包含临时 URL、认证信息或内部地址,交给别人分析前需要脱敏。

TLS 错误应修原因

证书验证失败时,先检查系统时间、主机名、证书链与信任根。
企业内部 CA 应通过受控方式安装,或指定经核验的 CA 文件,而不是永久关闭证书校验。

wget --ca-certificate=/opt/company/certs/root-ca.pem \
--output-document="$download_dir/model.tar.gz.part" \
"$artifact_url"

这是使用已审核内部 CA 的模板,不是在教如何接受任意证书。
不要把 --no-check-certificate 写进通用下载函数,更不要紧接着执行刚下载的未知脚本。
实验 GIF 使用回环 HTTP 只是隔离教学选择,不证明公网 TLS 路径已经测试。

下载、校验、发布应分阶段

应用应该只读取已经验证的正式文件。
可以先写入同一目标文件系统中的独立临时文件,校验通过后再切换到新版本;失败时保留旧版本可用。

若采用 rename 切换,要认识到“名称切换原子”不等于“重启、数据刷盘、应用重新加载等全部都事务化”。
更不能用一个成功的 mv 证明正在运行的控制器已经使用新模型。
发布动作需要独立的版本确认与回退入口。

scp:身份、内容、目的地都要正确

先确认客户端版本

ssh -V
scp -v ./payload.txt robot-lab:/opt/robot/incoming/payload.txt

scp 是命令名,不保证底层永远使用老的 SCP 协议。
OpenSSH 从 9.0 开始默认改用 SFTP;Ubuntu 22.04 常见的 OpenSSH 8.9 客户端仍属于旧默认行为,需要按实际版本判断。
不能把新电脑上的成功命令无条件套回较老容器。OpenSSH 9.0 发布说明Ubuntu 22.04 scp 手册

在 Ubuntu 22.04 的 8.9 实现里,可按其手册使用 -s 选择 SFTP;新版本已经默认 SFTP,不应把这个旧版本开关到处追加。
新客户端的 -O 用于明确请求老协议,是兼容选择,不是遇到错误时必加的修复开关。
远端路径通配符、空格与用户主目录扩展在不同协议下也存在差异。

最常用的操作骨架

# 上传一个文件
scp ./controller.yaml robot-lab:/opt/robot/incoming/controller.yaml

# 下载一个确定的文件
scp robot-lab:/opt/robot/logs/incident.log ./incident.log

# 非默认 SSH 端口
scp -P 2222 ./payload.txt robot-lab:/opt/robot/incoming/payload.txt

# 已确认目录范围后递归传输
scp -r ./config-v3 robot-lab:/opt/robot/incoming/

大写 -P 指端口,小写 -p 是保留时间与模式位等属性;不要混用。
递归传输可能跟随源树里的符号链接,传输前要确认实际范围。
-p 也不意味着跨系统完整复制所有所有权、ACL 与扩展属性。OpenSSH scp 手册

scp 在隔离 SSH 实验中传输小文件,并对比源与目标校验值确认内容一致

查看原尺寸动画

known_hosts 不是烦人的弹窗记录

用户密钥认证回答“你是谁”,主机密钥校验回答“对面是不是你以为的机器”。
两个问题不能互相替代。
脚本不应该靠 StrictHostKeyChecking=no 或空的 known_hosts 文件绕过身份确认。

正式环境可以使用经过带外核验的专用 known_hosts 文件:

scp -B \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile=/opt/robot-maint/known_hosts \
-o ConnectTimeout=10 \
./payload.txt \
robot-lab:/opt/robot/incoming/payload.txt

这个文件必须提前用可信方式准备;不存在或键不匹配就失败,才是无人值守场景应该有的行为。
-B 避免任务卡在密码询问,但不会自动创建凭据,也不会替你验证目标身份。SSH 主机密钥策略

ssh-keyscan 只能采集网络对端给出的 key,不能独立证明身份。
生产环境应通过管理员、受控控制台或其他可信渠道核对指纹,再纳入记录。
本实验使用自己刚启动的隔离回环服务收集临时 key,并与本轮生成的服务端公钥逐字段和指纹比对,通过后才写入临时 known_hosts。不能把它扩展成“对任何公网主机扫描后直接信任”。OpenSSH ssh-keyscan 安全说明

进阶:跳板机与带宽预算

scp -J gateway-lab \
./incident.tar.gz \
robot-lab:/opt/robot/incoming/incident.tar.gz

scp -l 8192 \
./incident.tar.gz \
robot-lab:/opt/robot/incoming/incident.tar.gz

跳板路径需要每一段身份和权限都已正确配置,不要为了方便默认转发整个 SSH agent。
-l 的带宽单位是 Kbit/s,与 wget 速率选项的字节单位不同;8192 Kbit/s 大约对应 1 MiB/s 的原始量级,协议开销会让实际有效速率有所不同。

压缩并不总会更快:JPEG、压缩包和很多模型归档已经压缩,追加 SSH 压缩可能只增加 CPU 成本。
目标小板 CPU 很忙时,限速、打包批量文件与避开高峰通常比盲目开启压缩更值得测试。

不把一次复制当成完整同步系统

scp 不提供通用的断点续传与双向同步语义。
文件比较大、网络容易中断、只想传增量时,应评估具备相关机制的工具,并单独学习它们的覆盖、删除和校验规则。
不要因为源文件夹少了一个文件,就假设 scp 会自动删除目标残留。

传完至少比较明确的对象:

sha256sum ./payload.txt
ssh robot-lab 'sha256sum /opt/robot/incoming/payload.txt'

这两条比较的是文件内容,不检查应用是否已经读取新文件。
配置与可执行文件的更新应先写入 staging 目录,核验后再按应用要求切换。
避免直接覆盖正在用于复现问题的唯一二进制或对应调试符号。

最小练习:只向实验目标复制一个文件,再改动本地副本。
重新计算两端 hash,区分“曾经复制成功”和“现在两端仍相同”;再模拟错误目标路径,确认脚本能将失败上报。

crontab:时间表达式只是任务的一小部分

用户表与系统表不是一种格式

日常查看和编辑自己账户的任务表:

crontab -l
crontab -e

没有任务表时,crontab -l 可能返回非零并提示没有 crontab,不等于整个 cron 服务坏了。
crontab -e 会改变真实调度,正式环境应先导出、审查差异并保留可恢复版本。
不要为了“清一下再写”先运行删除全部用户表的命令。

用户 crontab 的任务行为是五个时间字段后接命令:

# 分 时 日 月 星期 命令
15 2 * * * /opt/robot-maint/collect.sh

/etc/crontab/etc/cron.d/ 使用的系统表,在时间与命令之间还需要执行用户:

15 2 * * * robot /opt/robot-maint/collect.sh

把系统表这一行复制到用户表,会把 robot 当成命令开头,结果当然与预期不同。Ubuntu crontab 格式

日与星期为什么会出乎意料

0 9 1 * 1 /opt/robot-maint/report.sh

对这里讨论的 Ubuntu/Vixie cron,当“日”和“星期”两栏都受限制时,匹配关系是 OR。
因此这是每月 1 日或每周一的 9 点,不是“恰逢周一的每月 1 日”。
同一时刻同时匹配也不是为同一条记录运行两遍。

当一栏使用 * 时,不能机械套用“另一栏无所谓,反正星号永远真”。
要按具体实现对受限日字段的规则理解;不同 cron 家族的扩展也未必兼容。
如果要求复杂交集,宁可在明确的每日入口里写可测试的日期条件。

*/15 表示该字段中的步进匹配,不是“上次任务结束后再过 15 分钟”。
cron 也不是毫秒级控制循环调度器,不能拿它替代机器人实时定时机制。

查看 crontab 状态并在 env -i 精简环境下运行任务脚本;不是实际定时触发记录

查看原尺寸动画

本轮容器里 crontab -l 实际返回了权限限制:隔离配置中的 no-new-privileges 阻止了该程序通过 setgid 读取任务表。动画保留了这一结果,没有放宽权限或安装任务表;后续通过的是 env -i 下的任务脚本,不是 cron 定时触发。

该演示只回答“脚本脱离交互终端后是否还能执行”。
没有启动守护进程并观察触发,就不能由这张 GIF 得出“每天 2:15 已经验证会运行”。

最常见故障是环境差异

cron 任务不应依赖你刚刚在终端里手工 source 的 ROS 工作空间、Python 虚拟环境或 shell 函数。
应在自己的脚本里明确解释器、工作目录、必要环境和输出路径。

例如,一个项目脚本可以明确这样组织:

#!/bin/bash
set -euo pipefail

cd /opt/robot-maint
source /opt/ros/humble/setup.bash
source /workspace/install/setup.bash

exec /workspace/install/diagnostics/lib/diagnostics/collect \
--output /var/lib/robot-maint/latest-report.json

这是 ROS 工程模板,不属于只有基础工具的实验镜像。
实际解释器、包名与参数需要替换;并且 source 顺序本身也是复现条件,不应偷偷依赖登录脚本再覆盖一遍。
如果目标输出必须不能被读到一半,应用应写入临时文件后完整提交,而不是让消费者读取正在写的 JSON。

用户表可以显式限定命令查找环境:

SHELL=/bin/sh
PATH=/usr/bin:/bin
15 2 * * * /opt/robot-maint/collect.sh >> /var/lib/robot-maint/collect.log 2>&1

cron 环境赋值不是普通 shell 脚本展开,不要写 PATH=$HOME/bin:$PATH 并期待它自动补全。
工作目录也要在脚本中处理;“终端里我刚好 cd 到那里”不是任务契约。
精简环境测试有用,但真实 cron 仍可能额外受到 PAM、服务配置、用户与权限影响。

百分号会先被 cron 处理

直接在用户表里写日期格式时:

0 8 * * * /usr/bin/date +\%F >> /var/lib/robot-maint/dates.log 2>&1

未转义的 % 在该实现中有特殊含义,后续内容会进入标准输入处理。
它不是先交给 shell 再按单引号决定含义,所以只给日期格式加引号并不能解决所有问题。
更简单的工程做法是把日期逻辑写进脚本,crontab 只负责调用脚本。

前一次没结束,不要悄悄启动十个副本

如果一个采集任务有时需要 8 分钟,却每 5 分钟启动一次,就可能重叠运行。
对同一台机器上遵守同一个锁约定的任务,可以使用 flock

*/5 * * * * /usr/bin/flock -n -E 75 /var/lib/robot-maint/collect.lock /opt/robot-maint/collect.sh >> /var/lib/robot-maint/collect.log 2>&1

锁目录需由管理员预先正确配置权限,不能假设任意用户可写 /var/lib
-n 表示拿不到锁立即返回;这里显式把竞争失败码设为 75,以便与业务错误区分。
实际监控还要把跳过次数收集出来,避免任务一直拿不到锁却被当成正常完成。util-linux flock 手册

这不是分布式锁,也不会约束没有遵守同一锁文件的其他程序。
文件系统与挂载方式可能影响锁语义;多机任务不能仅靠每台机器自己的本地锁避免重复执行。
应用还应考虑幂等:同一份输入重复处理,是否会重复扣费、覆盖报告或上传两次。

日志、时区与守护进程都要验收

正式验收至少包括:任务属于哪个账户、系统当前时间和时区、cron 是否在运行、上一次什么时候开始和结束、退出码与产物是否正确。
日志不是越多越好,需要限制敏感内容并配置容量或轮转。
也不要默认系统已配置邮件投递,因而“没有收到 cron 邮件”不能证明任务没失败。

Ubuntu 的 cron 对时间变化有自己的处理规则;业务要求涉及夏令时或跨时区时,必须阅读本机实现说明并测试相关边界。
在 shell 里设置一个 TZ 影响脚本输出,不自动等于更改该 cron 守护进程的所有调度时区。Ubuntu cron 守护进程说明

Docker 镜像里有 crontab,不代表有定时服务

命令安装成功只是有了客户端。
一个以实验脚本为入口、运行后退出的容器,不会凭空在明天准时醒来。
同样,容器里存在 systemctl 文件也不能证明 systemd 是 PID 1 并在管理服务。

可以由宿主机调度一次性容器,也可以在明确设计的服务里运行调度器。
选择哪一种,要同时决定日志留存、凭据注入、失败重试、并发策略和容器退出行为。
不要为运行一个日常脚本,直接把生产容器改成无约束的长期特权环境。

若系统本来就使用 systemd,可以评估 timer 与 service 的组合。
OnCalendar 表达日历触发,Persistent=true 可处理部分错过触发的补跑需求;它不是所有历史触发逐条重放,也不使任务自动具备业务幂等性。systemd.timer 说明

最小练习:先手工执行自己的脚本,再用精简环境执行,比较缺少了哪些假设。
随后在明确授权的测试环境安装一条短周期任务,等待真实触发,并核对开始、结束、退出码及输出文件。
完成后撤掉这条测试记录;不能把临时高频任务遗留在正式账户里。

联合流程:每天把机器人诊断包带回来

分清生成、传输与消费

一个可维护的流程可以分成三段:
机器人生成独立事件目录;传输脚本只复制已完成的包;分析端核验后才将它加入待分析队列。
三者最好通过一个明确的完成标记或不可变版本名衔接,而不是共同读写 latest.tar.gz

诊断包里应包含程序版本、配置、日志时间范围与必要的运行环境,不应默认打包整个用户目录。
core、网络抓包、参数文件都可能有敏感数据,发送前应检查范围。
密钥与 token 不应该随着“复现材料”进入公开笔记仓库。

给每种失败留不同的证据

下载 TLS 失败,检查信任与时间;scp 主机键变化,先核验远端身份;cron 没有触发,检查调度服务与时间。
如果任务确实启动了却报动态库缺失,应转向环境与 ELF 动态链接,而不是重复改 cron 时间表达式。

同样地,复制成功但分析机打不开库,可能是架构、符号或运行库不一致,不属于网络传输失败。
先对比 hash,再分析文件身份,可以把两层问题快速分开。

一份简洁的验收记录

记录本次任务 ID、输入版本、目标主机身份、开始结束时间、退出码、最终文件校验值与输出位置。
发生跳过、重试和超时也要记录,不仅记录成功路径。
这样下次问“昨晚到底有没有跑”,可以读事实,不需要靠终端历史猜测。

本系列选题参考用户提供的 Linux Tools Quick Tutorial 工具参考入口,示例按复现与运维问题独立编写。
继续观察运行中的进程可看 进程诊断篇;容量与性能问题可看 资源监控篇

3d打印 actor-critic adaptive sampling ai辅助设计 algorithm algorithms anymal apriltag ardupilot atlas attention automation axis-angle bang-bang belief encoder blender bode c++ cadquery calibration camera calibration camera-intrinsics chrome cmake cmakelists cnn colcon computer-vision conan control controller_manager cpp cpu d435i dagger data_struct db depth camera depth-camera design-pattern diagnostics direct collocation dots dtof economics eigen elevation map elf executor factory-pattern fcpx fiducial marker figure finance forge fourier fov freecad gae gazebo gdb geometry git gnu gru guitar hardware humanoid ibus imu interest isaac gym isaac lab isaaclab kdl laplace latent variable latex launch learning-notes legged locomotion legged robotics legged-robot legged_gym life linux linux-kernel linux-tools mac math matlab matrix memory mixture of experts mlp money motion imitation motion-control motor moveit mpc mujoco music-theory network neural mapping ocs2 ode onnx openscad operator optimal algorithm optimal-control perceptive locomotion perf performance personal-finance piano pinhole-camera pinocchio pixhawk pixhawk 6c point-cloud policy distillation ppo privileged learning profiling px4 python qgroundcontrol qos quadrotor realsense reinforcement learning representation learning reward tuning rnn robogauge robot robot parkour robotics ros ros2 ros2_control rsl_rl rtb security sensor-fusion shell signal-processing sim-to-real simulation socket soft dynamics constraints spot ssh stairs stl stm32 tcp-ip teacher policy teacher student teacher-student temporal convolution terrain reconstruction thread tools tron1 twist ubuntu uml uncertainty unitree unitree g1 urdf vae valgrind vcxsrv velocity vim web wifi wiring work workflow wsl z-transform zero-shot transfer 中文输入 交叉编译 人形机器人 依赖管理 分支管理 动力学 四旋翼 四足机器人 实验诊断 强化学习 接触动力学 数值计算 机器人
知识共享许可协议