ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux mv 命令底层原理、覆盖陷阱与脚本安全实践

Linux mv 命令底层原理、覆盖陷阱与脚本安全实践 mv大概是 Linux 里最容易被人会了但没全会的命令。几乎所有入门教程都会告诉你它是移动文件/目录一句话带过然后你就在真实场景里踩坑跨盘搬 200G 数据时被CtrlC打断收获一个半截文件脚本里用mv覆盖日志权限位从 600 变成了 644批量改名时通配符死活匹配不到隐藏文件把目录往另一个目录一塞结果路径多套了一层服务起不来。这些问题的根源都是一个mv把三种语义完全不同的操作塞进了同一套参数里——同一文件系统内的改名、跨文件系统的复制加删除、以及把 A 放进 B 目录里面。这三件事的开销、原子性、对 inode 的影响全都不一样但命令行长得几乎一样。这篇我按自己踩坑的顺序把mv从底层行为讲到参数细节从移动文件讲到移动目录再给出脚本里安全使用它的几条硬规则。刚接触 Linux 的同学能拿到一份可抄的操作清单天天写运维脚本的老手也能在这里找到几个平时没注意的边界情况。1. mv 到底在做什么一次改名或一次悄悄的完整拷贝1.1 同一文件系统内它只是一个 rename 系统调用先记住一句最有用的话在同一个文件系统同一个分区、同一个挂载点内部mv不搬运任何数据。它做的事情是给数据换一个目录项名字底层是一次rename()系统调用几乎瞬间完成哪怕文件有 50G。打个比方文件的数据存在磁盘上就像一套房子文件的名字是门口那块写着门牌号的牌子。同一个小区里改门牌号你不需要把房子拆了重盖只要把牌子摘下来换个位置。这就是mv在同一分区内做的事——inode 还是那个 inode数据块一个都没动只是父目录里那条文件名 → inode 号的记录被改了。这个认知直接决定了三件事。第一同一分区内mv一个几百 G 的目录速度是毫秒级别傻等进度条。第二同一分区内mv不会被磁盘空间不足阻断因为它不占新空间。第三也是最重要的同一分区内mv是原子操作。要么改名成功要么什么都没发生不存在改了一半的中间状态。这一点在配置文件热更新里非常关键后面第 6 节会展开。想亲眼验证可以在同一个目录下做个实验# 记录 inode 号 ls -i bigfile # 输出类似131075 bigfile mv bigfile bigfile_renamed ls -i bigfile_renamed # 输出还是131075 bigfile_renamedinode 号没变就说明数据没挪窝。反过来如果你把文件mv到另一个挂载点inode 号一定会变这就是下一节要讲的事。顺带说一个很多人不知道的细节mv一个文件不会改变它的硬链接计数也不会影响其他指向同一个 inode 的硬链接——因为它们本来就是同一个房子挂了几块牌子改掉其中一块别的牌子照旧。1.2 跨文件系统时它悄悄变成了 cp rm一旦源和目标不在同一个文件系统上——比如从/home搬到挂载在/data的独立数据盘或者把文件从本地磁盘移到/tmp很多系统里/tmp是挂载在内存里的 tmpfs——mv的行为会发生本质变化它会退化成先复制、再删除源也就是cp -a加rm -rf的组合。回到房子的比喻这回不是改门牌号了而是真的搬家。家具得一件一件抬上车、运过去、再摆好最后把老房子拆掉。这个过程有三个后果每一个都值得单独记一笔。第一耗时长。50G 的文件跨盘移动就是实打实的 50G 写入速度取决于磁盘带宽。很多人以为mv都是瞬时的结果一个mv敲下去卡了半小时其实它在干活。第二不是原子的。复制到一半断电、被kill -9、或者硬盘满了会留下一个不完整的目标文件。而且要注意源文件这时候通常还在因为删除发生在复制成功之后所以数据不至于丢但目标目录里会多出一个看起来正常、其实残缺的文件。很多人就是被这个残件坑了——它文件名正确、大小看起来也不小脚本拿去做后续处理直接出错。第三硬链接会断。如果源文件在别处还有硬链接跨文件系统复制之后新文件的硬链接计数是 1和其他链接彻底脱离关系而在同一分区内mv顶多算改名链接关系完好。做备份去重的场景下这个差别影响很大。提示不确定两个路径是否在同一文件系统时用df对比一下挂载点和设备号或者直接看stat输出的Device字段。动手前花两秒确认比事后收拾残局划算。1.3 移动文件、重命名、塞进目录三种语义共用一套语法mv的语法结构是mv [选项] 源... 目标看起来平平无奇但目标这两个字有三种完全不同的解释方式而且判断依据是目标路径存不存在。源目标实际行为常见用途文件不存在的路径改名同分区或复制后删除重命名文件文件已存在的文件覆盖目标更新配置、替换版本文件已存在的目录移入该目录名字不变归档整理目录不存在的路径目录改名重命名目录目录已存在的目录变成该目录的子目录层级归档多个源已存在的目录逐个移入批量整理这张表里最容易翻车的是第 2 行和第 5 行。第 2 行的覆盖意味着如果你手滑把mv a.txt b.txt敲成了本意想改名b.txt的旧内容就没了——而且默认没有任何提示。第 5 行则是另一个经典事故脚本里写mv /app/data /app/backup你以为是把data重命名成backup但如果/app/backup这个目录已经存在结果是/app/backup/data路径凭空多了一层应用启动时找不到数据目录报的错还特别不直观。要避免这两类问题一是养成用-i或-n的习惯下面会细讲二是在脚本里写目标路径时心里先过一遍这个目标现在存在吗半年后还会存在吗。很多事故不是当时写错而是环境变了——半年前不存在的backup目录被另一个流程创建出来了老脚本的行为就跟着变了。2. 常用参数逐个拆解哪些值得日常带上2.1 -i、-n、-f覆盖控制的优先级陷阱这三个选项管的是同一件事——目标已存在时怎么办但策略不同-iinteractive覆盖前先问一句mv: overwrite xxx?回y才动手。-nno-clobber目标存在就直接跳过不覆盖也不报错。-fforce无条件覆盖连只读文件也照盖不误并且不提示。一个必须知道的坑是这三个选项同时出现时只有最后写的那个生效。mv -i -f a b和mv -f -i a b的行为是相反的。这个设计来自 coreutils 的实现方式说实话不太直观所以我的建议是永远不要混着写一次只保留一个覆盖策略让命令一眼就能读懂。另一个坑是别名。很多发行版默认在用户的 shell 配置里塞了alias mvmv -i所以你在终端里敲mv是带交互的但放进脚本跑就完全不是这回事——脚本默认不加载交互式配置也不展开别名于是同一个命令在终端里安全、在 crontab 里变成静默覆盖。我遇到过最典型的一幕手工测试时每次都有提示上线后脚本把当天的数据文件覆盖了因为脚本里那个mv没有-i。我的处理方式是脚本里一律用绝对路径/bin/mv或command mv明确绕开别名同时在脚本头部决定清楚用-n还是-f不要指望默认行为。-n特别适合绝不能覆盖的场景比如往备份目录堆放归档-f适合必须替换的场景比如替换二进制文件、更新软链。至于-i它只适合人手工操作脚本里用了就是给自己埋雷——没人回答那个提示mv会读到 EOF 然后静默跳过你以为成功了其实什么都没动。2.2 -v、-u、-b、-S看得见、有节制、可回退-vverbose是我最推荐新手常开的选项。它会把每一步操作打印出来比如renamed a.txt - b.txt。批量操作时这一行输出就是你的操作日志出问题时它是唯一的证据。代价是输出变多脚本里可以重定向到日志文件。-uupdate的语义是只在源比目标新或者目标不存在时才移动。它的判断依据是修改时间戳。这个选项看着很美实际用起来要小心两点一是它只比较 mtime不看内容也不看大小二是如果两侧的文件系统时钟不同步或者时间戳被工具改过比如解压、touch判断就可能反直觉。它的正确用法是增量搬运比如定期把某个目录里的新文件搬到归档目录mv -uv /var/spool/reports/*.csv /data/archive/-bbackup加-Ssuffix的组合很少被提到但在替换文件但想留一手的场景非常好用。-b会在覆盖前把目标文件改名备份默认后缀是~用-S可以自定义mv -b -S .bak /tmp/new.conf /etc/app/app.conf # 结果是 /etc/app/app.conf 被替换旧文件变成 /etc/app/app.conf.bak这比先cp一份再mv少一个步骤也不会因为中间被打断而留下不一致状态。不过要提醒一句备份文件依赖旧目标存在如果目标本来就不存在-b什么也不会做这是符合预期的但如果你写的脚本假设总会有 .bak 文件可回滚那就错了。2.3 -t 与 -T目标到底是目录还是名字这两个选项解决的是上表里第 5 行那个目录被塞进目录的歧义问题。-t DIRtarget-directory让你把目标目录提到前面后面跟一串源路径mv -t /data/archive a.txt b.txt c.log好处有两个。一是语义无歧义——/data/archive明确就是目录不存在被理解成新名字的可能。二是当源文件很多、由find之类的命令生成时-t让参数顺序变得自然不用把目录写在最后。-Tno-target-directory走的是另一个方向强制把目标当成名字而不是目录。比如mv -T /data/a /data/b如果/data/b已经是一个目录不加-T时/data/a会成为/data/b/a加了-T命令直接报错因为目录不能被文件覆盖。这个行为在脚本里很有价值——你想要的是出错并停下而不是默默多套一层。我在写部署脚本时的习惯是凡是重命名的意图一律加-T凡是放进目录的意图一律用-t并显式写出目录。这样命令的意图和代码一致半年后回来看也不用猜。2.4 -a、-Z、--strip-trailing-slashes容易被忽略的三个-aarchive在mv里其实是个不太常用的选项因为同一文件系统内的mv完整保留所有属性根本不需要它它主要影响跨文件系统的复制阶段让权限、时间戳、软链接、设备文件等属性原样保留。GNU 的实现里mv跨设备时默认就会尽量保留属性但显式写上-a能让你在阅读脚本时更确定意图我在跨盘搬运目录时会带上它。-Z是跟安全上下文相关的选项用于在移动文件时设定或重置目标的安全标签。在启用了相应机制的服务器上跨目录移动后访问被拒、而权限明明是 755 的情况很多时候就是上下文没跟上。这类问题排查时用ls -Z看当前标签用-Z在移动时指定比事后手动刷省事。日常桌面环境用不到但如果你在管服务器最好知道有这么个东西存在。--strip-trailing-slashes处理的是路径末尾的斜杠。听起来无关紧要实际上影响语义mv dir1/ dir2里那个尾斜杠在某些工具眼里是这是一个目录必须是目录的强调。当源路径是由变量拼出来的比如SRC$BASE/logs/尾斜杠的存在与否会改变mv的判定逻辑而脚本里这种情况极其常见。我的做法是拼接路径变量时统一去掉尾斜杠需要强调目录的地方用-t不依赖斜杠来传达意图。这个习惯帮我省掉过好几次路径多套一层的困惑。3. 移动文件与目录的实操场景3.1 重命名文件和目录的差别没那么大重命名是最基础也最安全的用法因为目标不存在时mv不会覆盖任何东西。文件改名和目录改名在命令层面完全一样mv draft.md final.md # 文件改名 mv project-v1 project-final # 目录改名有一条容易被忽视的规则你不能把一个文件改名到一个不存在的目录下面。mv a.txt /no/such/dir/b.txt会直接报No such file or directory。这很合理——mv不会替你造目录。批量搬运前先确认目标目录存在是脚本里最常见的一行防御mkdir -p /data/archive/2024 mv -v *.log /data/archive/2024/目录改名时还有一个隐藏影响值得留意如果这个目录下有正在运行的程序它的当前工作目录会跟着变化但进程本身的句柄不受影响程序还能继续读写文件。真正会出问题的是那些用绝对路径在运行时反复拼接文件路径的程序——目录改名后它们下次打开文件就找不到地方了。所以服务在跑的时候改它的目录名风险比想象中高。3.2 把目录移进目录路径解释的三种写法这个场景值得单独讲因为它是事故高发区。假设你有一个/app/data想把它归到/app/backup下面有几种写法行为各不相同# 写法一目标目录存在结果是 /app/backup/data mv /app/data /app/backup # 写法二显式说明目标是目录结果同样是 /app/backup/data mv -t /app/backup /app/data # 写法三目标带尾斜杠很多人的习惯结果还是 /app/backup/data mv /app/data /app/backup/ # 写法四想在 backup 里改成别的名字直接写全路径 mv /app/data /app/backup/data-20240601问题出在对目标不存在的情况上。如果/app/backup当时还没被创建写法一会把data直接改名成backup得到/app/backup一个装着原 data 内容的目录而不是一个容器。看起来结果相似实际上路径结构完全不同而且后续再往/app/backup里搬东西时会变成往这个其实是老 data的目录里塞。我的习惯是归档类操作一律用目标全路径 明确名字的写法四比如mv -t /data/archive /app/data之后再改名或者直接mv /app/data /data/archive/data-$(date %F)。把日期戳进名字既避免了目录已存在造成的层级问题又天然保留了历史版本。这条规则看着笨但它把这个命令最容易出错的一类行为直接消掉了。3.3 通配符、隐藏文件与以 - 开头的名字mv *.log /tmp/是日常操作但通配符的展开是shell 干的活不是mv干的。理解这一点能解释好几个怪现象。第一*不匹配以点开头的隐藏文件。所以mv * /backup/不会搬走.env、.gitignore这些东西。要连隐藏文件一起搬得显式写mv -t /backup/ ./* ./.??* 2/dev/null./??*这个模式的意思是点开头且后面至少还有两个字符用来匹配隐藏文件同时排除.和..。末尾的2/dev/null是给那些目录里没有隐藏文件的情况兜底避免报错干扰日志。第二当通配符匹配不到任何文件时不同的 shell 行为不同。默认情况下sh会把*.log原样传给mv于是你收到一个cannot stat *.log的报错而在bash里打开nullglob后匹配不到就直接消失命令变成没有源文件报用法错误。脚本里这两种错误都很讨厌稳妥的写法是先判断再执行shopt -s nullglob files(/var/log/app/*.log) [ ${#files[]} -gt 0 ] mv -v ${files[]} /data/archive/第三文件名以-开头是灾难。mv -foo /tmp/会被解析成选项-f -o -o行为完全失控。解决办法是加--表示后面全是路径或者用./前缀mv -- -weird-name.txt /tmp/ mv ./-weird-name.txt /tmp/养成在脚本里给所有变量路径加./前缀或使用--的习惯成本极低但能挡住一批诡异问题。同理文件名里带空格、换行、引号的情况在脚本里一律用数组加双引号处理别用for f in $(ls)这种写法它在遇到空格时会把一个文件名拆成好几个。3.4 find mv 批量迁移的标准姿势当文件数量上万mv * /dest/会撞上参数列表过长的错误因为 shell 展开后的参数总量超过了系统限制。这时候应该让find直接执行命令find /data/logs -maxdepth 1 -type f -name *.log -mtime 7 \ -exec mv -v -t /data/archive/old/ {} 这里的{} 是关键它会让find把尽可能多的文件一次性拼成一条命令而不是每个文件调用一次mv那是{} \;的行为性能差几十倍。配合-t指定目标目录参数顺序天然正确。三个实战注意点-maxdepth 1很有必要否则你会把子目录里的文件一起翻出来移动后目录结构全乱。-mtime 7是修改时间在 7 天前用来做日志归档很合适。但它基于 mtime如果日志文件被追加写入mtime 会不断更新永远达不到阈值这点在排查为什么没归档时要想到。涉及删除或移动大量文件时先去掉-exec只跑find看输出列表确认范围正确再执行。这个预演步骤我已经形成肌肉记忆救过好几次。还有个坑find配合xargs时如果文件数量太多xargs会自动分批每一批调用一次mv这时候必须用-t指定固定目录否则第二批之后的命令里源和目标会错位。我一般直接用find -exec ... 绕开这个问题少一层心智负担。3.5 大目录跨设备搬运时间预估与断点思路跨盘搬大目录是mv最考验人的场景因为它同时具备耗时长和非原子两个特点而且中途没有进度显示。真要做我的流程是三步。第一步确认空间。用du -sh看源目录大小用df -h看目标剩余空间留出至少 20% 余量。跨盘移动本质是复制加删除全程需要两边都有空间不是搬过去省下来。第二步改用 rsync 做主体搬运。这是我最想强调的一点跨盘搬几十 G 以上的数据用rsync -a --remove-source-files比mv好得多——它支持断点续传、有进度显示、可以重复执行而不重复复制失败后重跑就能接着来。等rsync确认全部搬完、源目录只剩空壳再用一条find把空目录清掉。用mv一步到位看起来简洁但一旦中途失败你面对的是一个不知道完整不完整的目标目录得重新核对。第三步如果确实要用mv放到screen或nohup里跑别让它挂在你的 SSH 会话上。网络一抖mv收到信号跨设备复制会中断留下残件。这个细节听着琐碎但远程操作大目录时它是最常见的失败原因。顺便说一句/tmp的坑。不少系统把/tmp挂成 tmpfs也就是内存盘。往/tmp里mv一个大文件如果源在不同分区等于往内存里写两个后果慢而且可能直接把内存吃满触发 OOM。测试环境里放临时大文件要先确认/tmp到底是什么文件系统df -h /tmp一行搞定。4. 覆盖行为、权限与运行中的文件4.1 覆盖发生时inode 和权限位归属谁这一节是很多人踩过之后才想明白的。同一文件系统内mv a b而b已存在时实际发生的事情是先unlink掉b这个名字注意只是删掉这条目录项b原来的 inode 如果还有其他硬链接数据还在然后把a的 inode 挂到b这个名字上。结论很直接覆盖之后文件的权限、属主、属组、时间戳全部是源文件a的b原来的属性一点都不会留下。这跟很多人的直觉相反——他们以为替换内容、保留权限。所以如果你有个精心设置了 600 权限的配置文件被一个 644 的新文件mv覆盖上去权限就跟着变成 644 了。这也是我开头提到的那个事故的成因。跨文件系统时结果一样因为复制阶段是按源文件的属性创建的。两种情况都遵循源属性胜出这条规律记住这一条就够用了。还有两个细节值得知道。一是b的旧 inode 如果被别的硬链接指着那些链接仍然可以访问到旧内容你并没有删掉它只是把一个名字改了指向。二是覆盖只读文件时mv会问一句前提是有-i或者直接失败如果没有写权限而目标目录的权限也不够——具体行为取决于你有没有目标目录的写权限而不是文件的写权限。这一点经常让人困惑文件本身是 444但目录是 755 而你是属主那你就能覆盖它因为unlink是目录操作。4.2 属主、组、ACL、上下文同一分区内mv一个文件属主和属组完全不变因为 inode 没换。跨分区mv时普通用户没权限扮演别人所以复制出来的新文件归你所有只有 root 才能完整保留原属主。ACL访问控制列表也是同理。同一分区内mv不影响 ACL跨分区就不一定了取决于文件系统是否支持以及复制工具是否携带。而安全上下文在跨目录移动后更容易出问题——从用户家目录移到 web 根目录文件权限看着都对服务就是读不了这时候该看的是上下文而不是权限位。我处理这类问题的顺序固定是先ls -l看权限、ls -Z看上下文、getfacl看 ACL三步走完基本能定位。顺序反了会白折腾很久——先改权限改了半天发现是上下文的问题。4.3 移动被进程占用的文件这是mv和rm都有的一个特性而且特别有用在 Linux 里一个文件被进程打开着你依然可以移动或删除它的目录项。因为进程持有的是 inode 的引用不是路径。文件被mv之后进程继续正常读写写的还是同一份数据只是别人按老路径找不到了。这个特性撑起了一个经典运维模式——日志切割mv /var/log/app/access.log /var/log/app/access.log.20240601 kill -USR1 $(cat /var/run/app.pid) # 通知进程重新打开日志文件老日志的 inode 还在进程继续写进程收到信号后重新按路径打开创建新的access.log。整个过程零丢日志。但前提是程序支持这种重开日志的信号不支持的程序会一直往那个已经改名的文件里写你以为磁盘释放了其实没释放——df和du对不上时多半就是这个原因用lsof L1能找出那些已经删掉但还被打开的文件。还有一个常见误区mv正在运行的可执行文件不会报错。因为改名不涉及写入内容不会触发文本文件忙的错误。真正会报这个错的是cp覆盖正在运行的程序。所以替换线上二进制时正确姿势是先把新版本传成临时名字再mv覆盖——这样是原子的服务不会读到写了一半的文件。如果直接cp覆盖轻则报错重则服务崩溃。4.4 监控与备份工具眼中的 mv如果你在用文件监控、日志采集、自动同步之类的工具mv在它们眼里的样子值得了解否则会出现本地明明移动成功采集端却丢了文件的怪事。同一文件系统内的mv在监控层面表现为一次移动事件源路径的删除通知加目标路径的创建通知且系统会明确标记这是一次重命名。成熟的采集工具会把它识别为同一个文件换了位置日志不会重复采集。而跨文件系统的mv在监控眼里就是一个新文件诞生加一个老文件消失采集端会把目标文件当成全新文件重新读一遍源文件按删除处理。这就解释了为什么有些场景下日志会出现重复——你以为是移动系统认为是新建。对备份工具来说同一分区内mv一个目录目录及其下所有文件的 inode 都不变基于 inode 的去重机制依然有效不会重新备份一遍跨分区就完全不同了所有 inode 都是新的增量备份会把这些文件当成全新的重新传一遍。做大规模归档规划时这个差别能决定备份窗口是 10 分钟还是 3 小时。所以有一条经验能用同分区内的改名完成归档就不要跨分区复制最好从一开始就把大容量存储规划成同一个挂载点。5. 常见报错速查与排查实录5.1 报错对照表报错信息常见原因处理方式cannot stat xxx: No such file or directory源路径不存在或通配符没匹配到任何文件检查路径与拼写脚本里加空匹配判断cannot move a to a subdirectory of itself把目录往它自己的子目录里移检查路径变量加-T让意图明确target xxx is not a directory多源操作时目标不是目录用-t指定目录或先创建目录Permission denied对源目录或目标目录无写权限检查目录权限而非文件权限Text file busy覆盖正在运行的程序时被系统拒绝先传临时名再mv不要直接cp覆盖Invalid cross-device link少见通常是脚本误用了底层调用用mv而非直接调renameArgument list too long通配符展开的文件太多换用find ... -exec mv -t DIR {} 5.2 几个真实翻车现场现场一跨盘移动被中断目标目录留下半截文件。当时是把一批数据库备份从本地盘搬到外挂存储mv跑了一半我按了CtrlC。事后检查发现目标目录里有个文件大小只有源文件的三分之一但名字完全正常。后来我改了流程跨盘搬运一律用支持续传的工具搬完再校验大小和校验和最后才删源。这个教训就是看着像成功不等于成功。现场二脚本把一个已存在的目录当成了新名字。部署脚本里写的是mv /srv/app/current /srv/app/previous本意是滚动版本。某次/srv/app/previous被另一个流程提前创建了结果current变成了previous/current服务找不到发布目录。修复方法很干脆脚本里所有重命名操作加-T一旦目标已存在就直接报错退出而不是默默多套一层。宁可失败不要错位。现场三隐藏文件没搬走。用mv /home/old/* /home/new/迁家目录.ssh、.config、.gitconfig全留在原地用户登录后密钥全没了一脸茫然。现在我做这种迁移一律用两条命令先搬普通文件再搬隐藏文件最后用ls -a两边对比数量确认干净。现场四文件名乱码导致通配符失效。从别人那里拿到的一批文件名字是乱码mv *.csv /data/完全匹配不到。这种时候按名字操作是没戏的得按 inode 来ls -i # 找到那个乱码文件的 inode 号假设是 393221 find . -maxdepth 1 -inum 393221 -exec mv -v {} /data/fixed-name.csv \;这个方法不依赖文件名能被正确解析处理编码问题、非法字符问题都很稳。6. 脚本里安全使用 mv 的几条硬规则写了几年运维脚本我把关于mv的经验收敛成了六条规则基本覆盖了上面提到的大部分坑。第一明确覆盖策略。每个mv都必须带上-n或-f绝不依赖默认行为也绝不用-i跑脚本。选哪个取决于意图归档用-n宁可跳过也不覆盖替换用-f必须成功替换。第二绕开别名。脚本里用/bin/mv或command mv不让交互式配置影响执行结果。同一个脚本在开发者终端和 crontab 里跑出不同结果绝大多数原因就在这里。第三路径统一处理。变量拼出来的路径去掉尾斜杠重命名加-T放进目录用-t必要时给路径加./前缀。这套动作看着琐碎但它把mv最容易出错的那部分语义不确定性消掉了。第四跨设备操作先查空间再考虑换工具。df确认余量超过一定体量我的阈值是几个 G就换支持续传的方式并且放到能扛住断连的环境里跑。第五用原子替换更新配置和程序。正确姿势是把新内容写到同分区的一个临时文件比如app.conf.new确认内容无误后再mv -f app.conf.new app.conf。因为同分区内mv是原子的读取方要么看到老版本要么看到新版本永远不会读到写了一半的文件。这个技巧在配置文件热更新、发布二进制时价值极高比直接覆盖写安全一个数量级。第六批量操作先预演。把mv换成一个回显命令跑一遍看输出列表对不对确认了再真跑。多花三十秒换来的是一晚上不用回滚数据。注意跨设备移动在系统层面不是一步完成的操作任何中途被打断的场景都要按可能留下残件来排查别只看命令有没有报错。我个人在实际操作中的体会是mv这类基础命令的坑几乎都集中在它悄悄替你做了判断上——判断目标是不是目录、判断要不要覆盖、判断是不是跨设备。把这三件事都显式写清楚mv就从一个容易惹事的命令变成一个非常可靠的原子替换工具。最后分享一个小技巧养成用mv -v的习惯把每次操作打印出来重定向到日志出问题时那份日志比任何回忆都管用——你不需要记住自己五分钟前敲了什么只需要grep一下。
返回列表