你每天都会用 mv,但它比你想象的要复杂得多。移动文件和重命名文件在 Linux 里其实是同一回事------这听起来有点反直觉,但理解了这个前提,很多行为就都说得通了。
摘要 :
mv命令在 Linux 中既是移动工具也是重命名工具,其行为取决于是否跨文件系统。同文件系统内,mv仅修改目录项指针,瞬间完成;跨文件系统时则退化为cp + rm。本文从 inode 原理出发,详解mv的常见用法、边界情况、权限问题及rename()系统调用的原子性,帮助你彻底掌握这个看似简单实则复杂的命令。
mv 的本质:inode 操作
mv 并不真的"搬运"数据。它操作的是目录项(directory entry),也就是 dentry。
bash
# 重命名 = 同一文件系统内的 dentry 修改
mv old_name.txt new_name.txt
# 移动 = 跨文件系统才真正复制数据
mv /home/user/file.txt /mnt/usb/
同一个文件系统内,mv 只是修改目录项的指针,文件的 inode 号不变,数据块不动,所以是瞬间完成的。跨文件系统时,mv 实际上执行的是 cp + rm:先复制数据,再删除源文件。
可以用 strace 验证:
bash
# 同文件系统:只看到 rename 系统调用
strace mv file1.txt file2.txt 2>&1 | grep -E "rename|link"
# rename("file1.txt", "file2.txt") = 0
# 跨文件系统:看到 copy_file_range + unlink
strace mv /home/user/file.txt /mnt/usb/ 2>&1 | grep -E "copy|unlink"
# copy_file_range(3, ...) = 8192
# unlink("/home/user/file.txt") = 0
常见用法与坑
1. 批量重命名
mv 本身不支持批量重命名,但配合 for 循环就行:
bash
# 把所有 .txt 改成 .md
for f in *.txt; do
mv "$f" "${f%.txt}.md"
done
如果文件名里有空格,不加引号会翻车。${f%.txt} 是 bash 的参数展开,从末尾最短匹配删除 .txt。
更复杂的批量重命名用 rename 命令:
bash
# Perl 版 rename,支持正则
rename 's/\.txt$/.md/' *.txt
2. 移动目录
mv 移动目录不需要 -r 参数,这跟 cp 不一样:
bash
# 目录直接移动,不需要递归标志
mv mydir/ /target/path/
# cp 就需要 -r
cp -r mydir/ /target/path/
因为同文件系统内,移动目录只是修改父目录的 dentry,不涉及递归复制。
3. 覆盖确认
默认 mv 会静默覆盖目标文件,这很危险:
bash
# 交互模式:覆盖前确认
mv -i source.txt target.txt
# 不覆盖模式:目标存在则跳过
mv -n source.txt target.txt
# 备份模式:覆盖前备份
mv -b source.txt target.txt
# 会生成 target.txt~ 备份文件
建议在 .bashrc 里加 alias:
bash
alias mv='mv -i'
4. 更新模式
只在源文件比目标文件新时才移动:
bash
mv -u newer.log /var/log/
适合日志文件同步场景,避免覆盖更新的数据。
边界情况
目标是目录 vs 文件
bash
mkdir backup
mv file.txt backup/ # file.txt 移入 backup 目录
# 但如果 backup 是个文件...
mv file.txt backup # backup 文件被覆盖!
加 / 后缀可以明确意图:
bash
mv file.txt backup/ # 明确 backup 是目录,不存在则报错
目标已存在且是目录
bash
mkdir target
mv src/ target/
# 结果:target/src/,不是覆盖 target/
mv 把源目录移入目标目录,而不是替换。要替换得先 rm -rf target/; mv src/ target/。
权限问题
mv 需要源目录的写权限(删除旧 dentry)和目标目录的写权限(创建新 dentry),但不需要文件本身的写权限:
bash
# 你可以 mv 一个只读文件
chmod 444 readonly.txt
mv readonly.txt new_name.txt # 成功!
很多人以为文件只读就不能 mv,其实移动文件改的是目录,不是文件本身。
实现原理:rename 系统调用
Linux 内核的 rename() 系统调用是原子操作,要么成功要么失败,不存在中间状态:
bash
#include <stdio.h>
#include <errno.h>
int main() {
if (rename("old.txt", "new.txt") != 0) {
perror("rename failed");
// EXDEV: 跨文件系统,需要自行实现 copy + unlink
if (errno == EXDEV) {
printf("Cross-device link, need manual copy\n");
}
return 1;
}
printf("Renamed successfully\n");
return 0;
}
EXDEV 错误码(errno 18)就是跨文件系统的标志,mv 命令内部检测到这个错误后,自动切换到 copy + unlink 模式。
原子性有个实际好处:在并发场景下,用 mv 替换配置文件是安全的:
bash
# 原子更新配置文件
mv config.json.tmp config.json
# 其他进程要么读到旧文件,要么读到新文件,不会读到半截
这是很多配置热加载方案的基础。
总结
- 同文件系统内
mv是 dentry 修改,瞬间完成 - 跨文件系统
mv实际是cp + rm mv改的是目录权限,不是文件权限rename()是原子操作,适合并发安全的文件替换- 永远用
mv -i避免误覆盖
想在线试试 mv 的各种用法?可以看看这个 Linux mv 命令详解。
相关工具:Linux cp 文件复制 | Linux ls 目录遍历
