
聊一个特别基础但又特别容易被问倒的问题Linux 里怎么拷贝目录可能很多人秒答“cp -r 呗”但真到生产环境里备份几百 G 的数据目录或者把一台机器的软件目录完整搬到另一台机器时这四个字母往往不够用。我见过因为少写一个 -a 导致线上权限错乱见过因为目标目录存在与否没确认备份后被多套了一层目录还见过把符号链接当普通文件拷成一大包副本的。所以这篇把拷贝目录这件事从头捋一遍有哪些姿势每种姿势适合什么场景文档里没写的坑在哪儿。适合刚接触 Linux 的新手也适合每天跟服务器打交道的运维按需取用。1. 一句话说透cp 是复制文件cp -r 才是复制目录1.1 第一个坑不带 -r 会怎样先上结论cp是复制文件用的复制目录必须加-rrecursive递归。不加的时候系统直接拒绝干活$ cp /etc/nginx /opt/backup cp: -r not specified; omitting directory /etc/nginx这条报错我见过太多次了。新手最容易犯的错就是觉得“目录本身就是文件”或者想当然地以为不加也默认递归。实际上 cp 的安全设计就是——要复制目录必须显式说“我要递归进去”防止你把整个目录树不小心当成文件塞进别的地方。加了-r之后复制能跑通了但“能跑通”和“正确”是两码事。更严谨的递归参数其实是-R在 Linux 的 GNU coreutils 里-r和-R基本等价但在某些老 Unix 上-r遇到特殊文件socket、FIFO时的行为可能不完全一样。日常写命令我建议直接用-R或者直接上-a尽量别让自己依赖“历史习惯”。举例说明cp -R /etc/nginx /opt/nginx_backup这样会把整个 nginx 配置目录复制到/opt/nginx_backup。这里注意如果/opt/nginx_backup不存在复制出来的目录就叫这个名字如果它已经存在结果会变成/opt/nginx_backup/nginx。这个细节后面单独讲。1.2 参数这么多真正该记住的是 -a很多人用cp -r复制完目录然后ls -l一看怪事来了文件所有者变了时间戳全部是当下时间权限跟源目录对不上。原因很简单-r只负责“递归”不负责“保真”。Linux 的文件不只是“内容名字”它身上还挂着 mode权限位、uid/gid所有者/属组、mtime/atime修改/访问时间、capability、xattr、ACL 这些元数据。你只递归复制内容等于把一个有身份证的人带到一个新城市但给他重新拍了一张全新的身份证。真正保真的手段是-acp -a /etc/nginx /opt/nginx_backup-a其实是“archive”的意思等价于-dR --preserveall展开解释就是-d不跟踪符号链接把链接本身复制过去-R递归复制目录--preserveall保留权限位、所有者、时间戳、ACL、扩展属性、硬链接结构等我写备份脚本时只要目标是本地目录几乎无脑用cp -a。需要显示进度就用cp -av需要在覆盖前确认就用cp -ai。至于-p、--preservemode这些更细粒度的控制日常运维很少单独用因为-a已经把大多数场景都覆盖了。但如果你只是想保留时间戳和读取时间并不想保留所有者比如跨用户目录复制那cp -p可能是更合适的选择。这也是为什么我建议先理解参数而不是死记“用 -a 准没错”。注意cp -a并不等于“全平台备份工具”它不能解决所有问题。跨文件系统时硬链接无法保留跨机器时 uid/gid 可能失去意义这些下面展开。2. 拷贝目录前先搞懂目标路径和链接的脾气2.1 目标目录存在与否结果差一层这是我见过最隐蔽的坑之一。同一个命令目标目录是否存在最后的结构完全不同。假设源目录是/opt/app/config你要把它拷贝到/backup/下如果/backup不存在cp -a /opt/app/config /backup # 结果是 /backup内容等于 config 的内容如果/backup已经存在cp -a /opt/app/config /backup # 结果是 /backup/config/...为什么因为 cp 的语义是当目标路径已存在且是一个目录时把源目录“放进去”当目标路径不存在时把源目录“重命名/创建”成那个路径。这和其他工具的行为非常不同。rsync 也有类似问题但规则稍有区别。一个保险做法复制前先ls -ld /目标目录看一眼甚至用find确认类型。如果想避免歧义最好先mkdir -p /backup再执行cp -a /opt/app/config /backup/这样结果一定是/backup/config或者明确目标全路径/backup/config这样至少你心里有预期。2.2 软链接和硬链接在拷贝中的不同命运目录里最常见的“特殊文件”就是软链接symlink。默认情况下cp会沿着软链接走把链接指向的“真实内容”复制过去。什么意思比如/data/link是指向/etc/hostname的软链接你执行cp /data/link /tmp/得到的是一个/tmp/hostname文件的副本而不是一个同样指向/etc/hostname的链接。这经常导致两类问题链接的“指针”含义丢失复制后的副本是独立的原文件改了副本不会跟着变。如果链接指向的路径在目标机器上不存在复制出来的不是一个失效链接而是“扑空了”以后得到的错误结果甚至可能把一个 0 字节文件留在那儿。所以备份包含大量软链接的目录时一定要用cp -a或cp -d。cp -d专门保留符号链接把链接本身复制过去而不是内容。这样目标环境里依然能看到同样的链接关系。硬链接更麻烦。硬链接意味着多个文件名共享同一个 inode在同一个文件系统内当然没问题但跨文件系统复制时硬链接的“多文件名指向同一个数据块”这一关系无法保持。cp -a会尽量在目标端重建硬链接关系但受文件系统支持限制如果硬链接数量很大性能也会受影响。所以对大目录做跨盘迁移我更倾向用 rsync 或 tar 管道保留硬链接的能力更稳。2.3 权限、用户和时间戳才是“拷贝保真”的关键拷目录时经常被忽略的还有“所有者”和“权限位”。普通cp -r复制出来的文件拥有者默认是执行命令的用户更准确地说取决于当前 umask 和命令运行者的身份时间戳全部刷新成当前时间。这在备份可执行程序、服务配置时非常致命。举个例子你把/usr/local/bin下的程序目录用cp -r复制到另一台机器结果所有权变成当前用户名权限从 755 变成 700程序直接没法给其他用户执行。这就是典型的“备份了但又没完全备份”。需要保留原用户身份时使用cp -a或cp -p。需要提醒的是只有 root 用户才能把所有 uid/gid 整份保留。普通用户复制别人的文件时即使加了-a所有者也会变成自己因为普通用户没有 chown 权限。跨机器同步时也有同样问题如果两台机器用户 ID 体系不一样比如一台是 1000 的 alice另一台是 1001 的 bob直接复制过去 uid 就错乱了。所以跨机器通常结合 rsync 的-o/-g选项或 tar 的--same-owner并保证以合适权限执行。3. 从 cp 到 rsync小目录用复制大目录用同步3.1 一条命令解决大量复制cp -a 的适用边界小目录、一次性拷贝、不打算重复执行cp -a足够。它的优点是好用、直觉、不依赖额外命令缺点是没有增量能力每次都是全量复制目录一大就很浪费时间。没有“续传”机制中断了就得重来。虽然重新执行 cp 会覆盖已有文件但已复制的部分并不会被刻意跳过大目录中断后重跑效率很低。很难做选择性排除cp不支持排除模式除非你用 shell 通配符。要排除.git或node_modules这种目录cp 就不太顺手。所以我的经验是目录小于 1 GB、一次性拷贝随便用cp -a目录大于 1 GB或者要定期备份或者要跨网络传输直接换 rsync。别贪图简单。3.2 rsync 凭什么成为运维首选先看一个最常用的目录同步命令rsync -avP --delete /src/ /dest/解释一下参数-a归档模式等价于-rlptgoD也就是保留递归、符号链接、权限、时间戳、组、所有者、设备文件等。-v输出过程。-P等于--partial --progress支持断点续传并显示进度条。大文件中断后再次执行可以接着传。--delete删除目标端存在但源端不存在的文件让目标变成源端的“镜像”。注意这个参数会给备份场景带来风险后面避坑里讲。rsync 最核心的优势是增量同步。它通过检查文件的长度、修改时间和可选校验和来决定哪些文件需要更新不是一股脑全拷一遍。比如一个 100 GB 的目录第一次同步要 30 分钟第二次只改了一个文件可能几秒就完成。还有一点rsync 支持 exclude 规则这几乎是运维标配rsync -avP --exclude.git --excludenode_modules /project/ /backup/project/也可以把规则写到--exclude-from文件里适合排除列表很长的场景。cp想做到这个要么用 find xargs要么用 tar 管道绕来绕去远不如 rsync 直接。3.3 远程拷贝、管道传输与增量备份rsync 还能直接在本地目录和远程主机之间同步。前提是两边都装了 rsync且有 ssh 通道# 本地推到远程 rsync -avP /local/dir/ userserver:/remote/dir/ # 远程拉到本地 rsync -avP userserver:/remote/dir/ /local/dir/注意这里源路径结尾的/特别关键/local/dir/表示把目录里面的内容同步过去/local/dir表示把目录本身同步过去。每天都在有人因为这个斜杠多套一层目录。如果远程没有 rsync可以用 tar 管道tar czf - /local/dir | ssh userserver tar xzf - -C /remote/dest这种方式能在传输时压缩也能在 tar 层面做排除用--exclude而且不依赖 rsync。缺点是断点续传和增量能力没有中断只能整体重来。作为一次性迁移手段很合适作为常备备份手段不建议。4. 实操一条龙从评估大小到校验结果4.1 动手前先回答三个问题我在拷贝比较大的目录前会先在心里过三个问题源目录到底多大用du -sh /src看实际占用不要用ls -lh或stat因为目录大小不等于内容大小。我见过几个人看到 ls 显示 4K 就觉得是小目录结果磁盘瞬间被打满。目标位置剩余空间多少用df -h /dest看可用空间。别只看本机如果是挂载的网络存储还要考虑网络速率和存储占用。这次拷贝需要保真到什么程度只是临时用还是作为备份要不要保留链接、权限、所有者要不要排除某些子目录回答完这三个问题用哪个工具基本就定了。如果确认空间不足可以先清理临时文件或者改用 rsync 的--partial看看能否借助已有副本又或者把目标挂载点换到更大的磁盘。千万别在磁盘满时硬跑cp写到一半报No space left on device已经写入的文件可能还是完整的但目录结构和链接关系就不好说了。4.2 执行拷贝的标准流程示例以本地备份为例完整流程大致如下# 1. 查看源目录大小和文件数量 du -sh /data/project find /data/project -type f | wc -l # 2. 确认目标目录状态 mkdir -p /backup/project df -h /backup # 3. 执行拷贝小目录用 cp -a大目录用 rsync cp -a /data/project /backup/ # 或者 rsync -avP /data/project/ /backup/project/ # 4. 校验 diff -r /data/project /backup/project注意第三步里我用的是cp -a /data/project /backup/这样结果一定是/backup/project。如果用 rsync我会写成rsync -avP /data/project/ /backup/project/注意源目录后面有/这样把里面的内容同步到/backup/project/而不是生成/backup/project/project。执行期间建议加-v或者-P至少能看到当前传到哪了。如果文件特别多可以用nohup挂后台并记录日志nohup rsync -avP /data/project/ /backup/project/ rsync.log 21 日志里能看到出错的文件路径同步完排查也方便。4.3 拷贝完必须做的校验动作很多人拷贝完目录就收工这是不对的。至少要做三个动作数量对得上find /src -type f | wc -l和find /dst -type f | wc -l对比数量不一致说明有文件丢失。注意如果用diff -r它会把链接当成内容对比结果可能有误导。大小对得上du -sh /src和du -sh /dst对比如果相差太大检查是不是有符号链接没保留或者硬链接被拆成了多份。结构对不对用find /dst -maxdepth 2 -type d看看目录层级是否符合预期。多一层目录、少一层目录在这儿一眼就能发现。我个人的习惯是大目录同步后跑一遍rsync -avnc --delete /src/ /dst/如果输出为空说明两边完全一致。--delete加上-n只是试跑不会真的删除可以放心用。这个命令比diff -r快也比diff更懂链接和权限。5. 避坑清单目录拷贝常见问题与排查实录5.1 高频报错速查表报错或现象原因解决方案cp: -r not specified; omitting directorycp 不递归没加 -r加-R或直接用cp -acp: cannot access ...: Permission denied源或目标权限不够检查用户角色必要时用 sudo确认目标目录可写No space left on device磁盘空间不足先df -h清理空间或换挂载点目标目录多套了一层目标已存在且斜杠姿势不对先确认目标是否已存在统一用/dest/源内容的写法符号链接全部变成普通文件用了 cp -r 跟随链接改用cp -a或rsync -a所有者全部成了当前用户非 root 执行普通 cp -r用cp -a如果需要跨机器保留 uid 用 rsync-o -grsync 同步完目标端多了旧文件没有加 --delete 或不想删却删了手动控制 --delete 的启用心态这个表格不是教你怎么背而是让你遇到问题的时候能快速定位方向。很多时候报错信息不完整要结合前后命令和历史操作来判断。5.2 排查思路与工具组合遇到目录拷贝异常我会按这个顺序排查先看报错发生在哪个文件上。如果是整段中断多半是权限或空间。如果只有个别文件失败多半是文件名特殊、链接失效或目标端磁盘有小坏块。用df -h、mount | grep 目标路径确认目标文件系统状态。网络存储还要看延迟和丢包。用stat看源文件目录的关键元数据stat /src/file再用stat /dst/file对比重点看Access、Modify、Change和Uid。差太多就说明保真失败。用find /src -type l -print列出所有符号链接再去目标端对应路径ls -l检查链接本身是否还在。最后用rsync -avnc /src/ /dst/做一次一致性体检。在脚本自动化时我习惯给 rsync 加--itemize-changes输出里能清楚看出每个文件的更新类型f表示普通文件cL表示链接类型变化.....T表示时间戳不一致。配合归档日志能很快定位是哪类行为导致的偏差。5.3 我踩过的几个坑和对应的处理经验第一个坑用 cp -r 备份了含大量软链接的程序目录。当时图省事结果所有软链接都被“解引用”成了真实文件目标目录体积膨胀了两三倍而且原链接关系完全失效。后来我用cp -a重来一遍才算正常。教训是只要目录里有链接就别吝啬那一个-a。第二个坑执行 rsync 迁移时漏了源目录结尾的斜杠。我想把/data/www/下的东西同步到/backup/www/但命令写成了rsync -av /data/www /backup/www/结果在目标端生成/backup/www/www。因为目标目录已经存在rsync 判断源是目录本身就把它挂到目标目录下。这种问题对脚本自动化很致命不是报错而是静默多套层目录。现在我会在迁移前用rsync -avn试跑不直接信任记忆。第三个坑在网络存储上频繁使用 cp -a 备份又每次全量重拷。有个数据目录大概 200 GB我用 cron 每天cp -a一次后来发现一次执行要 40 分钟每天占带宽太高。换成 rsync 增量后第二次开始基本在 2-3 分钟内完成因为只更新变化文件。从那之后凡是“定时备份”场景我的默认首选就是 rsync不再碰 cp。第四个坑在磁盘空间不足时执行 cp导致目标目录处于半成品状态。中断后想恢复很难判断哪些文件完成了。所以现在我执行前几乎必跑du -sh和df -h并且用 rsync 的--partial续传。如果实在没法用 rsync我至少会先把目标路径单独挂在另一块磁盘或者准备好等价空间再动手。最后再分享一个小技巧手动拷贝目录前在命令前面加个echo干跑一遍把所有变量和路径打印出来很多低级错误当场就能发现。比如echo cp -a $SRC $DEST看着输出检查斜杠和目标路径成本极低收益很高。