Chapter 03

文件操作

创建、复制、移动、删除、压缩——Linux 文件操作命令完全指南,理解 inode、软硬链接与文件系统底层原理

文件系统基础:inode 的概念

在 Linux 的 ext4、XFS 等文件系统中,每个文件由两部分组成:inode(存储文件元数据)和数据块(存储实际内容)。理解 inode 对于理解硬链接、权限、删除等操作至关重要。

inode(索引节点)
存储文件元数据的数据结构,每个文件有唯一的 inode 编号。inode 包含:文件大小、权限(rwx)、所有者(UID/GID)、时间戳(atime/mtime/ctime)、指向数据块的指针。注意:inode 不包含文件名!文件名存储在目录中,目录本质是"文件名 → inode 编号"的映射表。
目录项(dentry)
目录中存储的每一条记录:文件名 + 对应的 inode 编号。一个 inode 可以有多个目录项指向它(这就是硬链接的原理)。
链接计数(Link Count)
inode 中记录的、有多少个目录项指向它的计数。当链接计数归零时,文件才真正从磁盘删除(数据块被回收)。这就是为什么"删除"文件准确说法是"取消链接"(unlink)。
# 查看文件的 inode 编号
ls -li README.md         # -i 显示 inode 编号(第一列)
stat README.md           # 查看完整元数据(含 inode、链接计数)

# inode 查看示例输出:
#   File: README.md
#   Size: 1234       Blocks: 8    IO Block: 4096  regular file
# Device: fd00h  Inode: 2621441  Links: 1         ← 链接计数为 1
# Access: (0644/-rw-r--r--)  Uid: (1000/alice)  Gid: (1000/alice)
# Access: 2026-04-01 10:30:00(最后访问时间 atime)
# Modify: 2026-04-01 09:00:00(最后修改时间 mtime)
# Change: 2026-04-01 09:00:00(inode 变更时间 ctime,含权限变更)

# 磁盘 inode 使用情况(inode 用完会导致无法创建新文件!)
df -i                    # 查看各分区的 inode 使用情况
df -ih                   # 人类可读格式
inode 耗尽:比磁盘满更难察觉的问题

inode 数量在格式化时就固定了(ext4 默认每 16KB 一个 inode)。如果系统中有大量小文件(如邮件、日志片段、npm 缓存),即使磁盘空间还有剩余,inode 也可能耗尽,导致无法创建新文件并报错"No space left on device"。用 df -i 检查 inode 使用率,超过 90% 要警惕。

创建文件与目录

# touch:创建空文件或更新时间戳
# 原理:如果文件不存在,创建 0 字节空文件;
#       如果文件存在,更新 atime 和 mtime 为当前时间
touch newfile.txt               # 创建空文件
touch file1.txt file2.txt       # 一次创建多个空文件
touch -t 202401150900 file.txt  # 设置指定时间戳(YYYYMMDDhhmm 格式)
touch -r reference.txt file.txt # 将 file.txt 的时间戳设为与 reference.txt 相同

# mkdir:创建目录
mkdir mydir                     # 创建单个目录
mkdir dir1 dir2 dir3            # 一次创建多个目录
mkdir -p projects/app/src       # 递归创建多级目录(父目录不存在时自动创建,非常常用!)
mkdir -p logs/{2024,2025}/{jan,feb,mar}  # 大括号展开:批量创建目录树
mkdir -m 755 secured_dir        # 创建时指定权限(755 = rwxr-xr-x)
mkdir -v newdir                 # 显示创建过程

复制与移动

# cp:复制文件和目录
# 原理:读取源文件数据,在目标位置写入新文件(新 inode)
#       与 mv 不同,cp 消耗额外的磁盘空间
cp source.txt dest.txt          # 复制文件(dest 已存在则覆盖,无警告!)
cp source.txt /backup/          # 复制到目录(保留原文件名)
cp -r mydir/ backup/            # 递归复制目录(-r 或 -R 必须!)
cp -i source.txt dest.txt       # -i = interactive:覆盖前询问确认
cp -p source.txt dest.txt       # -p = preserve:保留原始时间戳、权限和所有者
cp -a src/ dest/                # -a = archive:等同 -r -p,还保留链接(备份首选)
cp -v source.txt dest.txt       # -v = verbose:显示每个操作
cp -u source.txt dest.txt       # -u = update:只在源文件比目标更新时才复制
cp file1.txt file2.txt /dest/   # 复制多个文件到目录
cp -l source.txt dest.txt       # -l = link:创建硬链接而非复制(同一 inode,不占额外空间)
cp -s source.txt dest.txt       # -s = symbolic:创建软链接

# mv:移动(重命名)文件和目录
# 原理(同一文件系统):只修改目录项,inode 和数据不变(极快)
# 原理(跨文件系统):先 cp 再 rm,相对较慢
mv oldname.txt newname.txt      # 重命名文件
mv file.txt /newlocation/       # 移动到新位置
mv -i source dest               # 覆盖前询问(生产环境建议)
mv -n source dest               # -n = no-clobber:不覆盖已存在的文件
mv -v *.log /var/log/archive/   # 移动所有日志(显示详情)
mv dir1/ dir2/                  # 移动整个目录(不需要 -r)
rm 无法恢复,操作前三思

Linux 默认没有回收站,rm 删除的文件通常无法恢复(取决于文件系统和是否被覆盖)。最佳实践:① 重要操作前先备份;② 用 rm -i 开启交互确认;③ 脚本中删除前先用 ls 确认要删除的范围;④ 安装 trash-clisudo apt install trash-cli),用 trash 命令替代 rm

# rm:删除文件和目录
# 原理:从目录中删除目录项(减少 inode 链接计数),
#       当链接计数归零且没有进程打开该文件时,数据块才被标记为可用
rm file.txt                     # 删除文件
rm -i file.txt                  # 删除前询问确认(养成好习惯!)
rm -r mydir/                    # 递归删除目录及其内容
rm -rf mydir/                   # 强制递归删除(不询问,谨慎!)
rm -v *.tmp                     # 删除并显示每个被删文件名
rm -- -badfile.txt              # 删除以 - 开头的文件(-- 表示选项结束)

# rmdir:只能删除空目录(目录非空会报错)
rmdir emptydir                  # 删除空目录
rmdir -p a/b/c                  # 递归删除空目录链(c、b、a 依次删除,前提是都为空)

软链接与硬链接

链接原理与对比

软链接(Symbolic Link)
一个特殊文件,内容是目标路径的字符串(类似快捷方式)。有自己独立的 inode,存储的是目标路径。指向的文件被删除后,软链接变为"悬空链接"(dangling symlink)。优点:可以链接目录、可以跨文件系统、可以用相对路径。ln -s target link_name
硬链接(Hard Link)
直接指向文件 inode 的另一个目录项,是同一文件的另一个名字。删除原文件名只是减少链接计数,只有所有硬链接都删除,文件才真正消失。不能链接目录(防止循环),不能跨文件系统(不同分区)。ln target link_name
软链接 vs 硬链接 的 inode 关系 ────────────────────────────────────────────── 硬链接(link count = 2): file.txt ──┐ ├──▶ inode #1234 ──▶ 数据块 hardlink ──┘ (link count=2) 软链接: symlink ──▶ inode #5678 ──▶ "file.txt" (路径字符串) (独立 inode) ↓(解析路径) inode #1234 ──▶ 数据块 删除 file.txt 后: 硬链接仍可访问(inode 未消失,link count=1) 软链接变为悬空链接(目标路径无效)
# 创建软链接
ln -s /usr/local/bin/python3.12 /usr/bin/python3  # 典型用途:版本别名
ln -s ../config/nginx.conf ./nginx.conf            # 相对路径软链接(推荐,可移植)
ln -s /var/log/nginx nginx_logs                    # 链接整个目录

# 查看链接信息
ls -la nginx.conf         # 显示 lrwxrwxrwx ... nginx.conf -> target
readlink nginx.conf       # 显示链接目标(不解析相对路径)
readlink -f nginx.conf    # 解析为绝对路径(跟踪所有链接直到真实文件)
readlink -e nginx.conf    # 绝对路径,且目标必须存在(否则返回空)

# 覆盖已存在的软链接
ln -sf /new/target link_name   # -f = force 强制覆盖(更新链接时使用)

# 创建硬链接
ln file.txt hardlink.txt  # 创建硬链接(两个名字指向同一 inode)
ls -li file.txt hardlink.txt  # 查看:inode 编号相同,link count 都是 2

# 查找悬空软链接(目标不存在的链接)
find . -xtype l           # -xtype l 匹配软链接指向不存在目标的链接
find /usr -xtype l 2>/dev/null | head -20  # 查找系统中的悬空链接
软链接路径陷阱

创建相对路径软链接时,路径是相对于链接文件所在目录,而不是相对于当前工作目录。例如在 /home/alice 下执行 ln -s ../etc/config ./myconfig,链接 myconfig 会指向 /home/etc/config(相对于 /home/alice),而非 /etc/config。如不确定,建议使用绝对路径。

压缩与归档

tar:打包归档工具

tar 最初是"Tape Archive"(磁带归档),现在是 Linux 上最常用的打包工具。注意:tar 的归档(.tar)和压缩(.tar.gz 等)是两步操作,gz/bz2/xz 只是压缩算法。

# tar 选项速记:
# c = create(创建)   x = extract(解压)
# z = gzip(.gz)      j = bzip2(.bz2)   J = xz(.xz)
# v = verbose(显示)  f = file(指定文件)
# t = list(列出内容,不解压)

# ─────── 创建归档 ──────────────────────────────
tar -czf backup.tar.gz mydir/         # gzip 压缩(速度快,最常用)
tar -cjf backup.tar.bz2 mydir/        # bzip2 压缩(压缩率更高,慢)
tar -cJf backup.tar.xz mydir/         # xz 压缩(最高压缩率,最慢)
tar -cf archive.tar file1 file2 dir1  # 只归档,不压缩

# 排除某些文件/目录
tar -czf backup.tar.gz mydir/ --exclude='*.log' --exclude='node_modules'

# 显示进度(大文件有用)
tar -czf backup.tar.gz mydir/ --checkpoint=1000 --checkpoint-action=dot

# ─────── 解压归档 ──────────────────────────────
tar -xzf backup.tar.gz                # 解压 gzip 归档到当前目录
tar -xzf backup.tar.gz -C /dest/      # 解压到指定目录(目录必须存在)
tar -xzf backup.tar.gz specific/file  # 只解压特定文件(路径需与 tar 内一致)
tar -xzf backup.tar.gz --strip-components=1  # 解压时去掉最外层目录

# 查看归档内容(不解压)
tar -tzf backup.tar.gz | head -20     # 列出归档内文件(前20个)
tar -tJf backup.tar.xz | grep "\.py"  # 查找归档中的 Python 文件

# ─────── 更新归档 ──────────────────────────────
tar -uf archive.tar newfile.txt       # 将新文件追加到归档(只支持未压缩 .tar)

zip / gzip / bzip2 / xz

# zip/unzip(跨平台,Windows 兼容)
zip archive.zip file1 file2           # 压缩文件
zip -r archive.zip mydir/             # 递归压缩目录
zip -e secret.zip file.txt            # 加密压缩(会提示输入密码)
unzip archive.zip                     # 解压到当前目录
unzip archive.zip -d /dest/           # 解压到指定目录
unzip -l archive.zip                  # 列出内容(不解压)
unzip -p archive.zip file.txt         # 解压单个文件到标准输出

# gzip:只压缩单个文件(不归档)
gzip file.txt                         # 压缩(原文件会被替换为 file.txt.gz)
gzip -k file.txt                      # -k = keep:保留原文件
gzip -d file.txt.gz                   # 解压(或 gunzip file.txt.gz)
gzip -9 file.txt                      # 最高压缩级别(1-9,默认 6)
zcat file.txt.gz                      # 直接查看 gzip 压缩文件内容(不解压)
zgrep "error" app.log.gz              # 在压缩日志中搜索(非常实用!)

# 压缩率对比(以 100MB 文本为例):
# gzip  -1: 速度最快,压缩率低
# gzip  -9: 速度慢,压缩率中
# bzip2:    压缩率高于 gzip,速度较慢
# xz    -9: 压缩率最高,速度最慢(约 3-5x 于 gzip)

file 和 stat:文件类型与元数据

# file:识别文件类型(通过"魔数"识别,不依赖文件扩展名!)
# 原理:读取文件开头的几个字节(magic bytes),对照数据库识别格式
# 例如:PDF 文件以 %PDF 开头,PNG 以 89 50 4E 47 开头
file image.jpg          # JPEG image data, JFIF standard 1.01
file script.py          # Python script, ASCII text executable
file mybinary           # ELF 64-bit LSB executable, x86-64, dynamically linked
file archive.tar.gz     # gzip compressed data, was "archive.tar"
file /dev/sda           # block special (8/0)
file renamed_doc.pdf    # 即使改名为 .pdf,file 也能识别真实格式
file -i image.jpg       # 显示 MIME 类型(image/jpeg; charset=binary)
file *                  # 识别当前目录所有文件类型

# stat:查看文件详细元数据
stat README.md
# 输出解读:
#   File: README.md          ← 文件名
#   Size: 1234               ← 文件大小(字节)
#   Blocks: 8                ← 占用的 512B 数据块数量
#   IO Block: 4096           ← 文件系统的块大小
#   regular file             ← 文件类型
#   Device: fd00h/64768d     ← 设备编号
#   Inode: 2621441           ← inode 编号
#   Links: 1                 ← 硬链接计数
#   Access: (0644/-rw-r--r--)  ← 权限(八进制 + 符号表示)
#   Uid: (1000/alice)        ← 所有者 UID 和用户名
#   Gid: (1000/alice)        ← 所属组 GID 和组名
#   Access: 2026-04-01 ...   ← atime(最后访问时间)
#   Modify: 2026-04-01 ...   ← mtime(最后内容修改时间)
#   Change: 2026-04-01 ...   ← ctime(元数据变更时间,含权限/所有者变更)
atime 对性能的影响

默认情况下,每次读取文件都会更新 atime(访问时间),这意味着每次 catls、程序读取配置文件都会触发磁盘写操作(更新 inode),对高负载服务器影响显著。现代 Linux 内核默认使用 relatime 挂载选项(只有 atime 早于 mtime 时才更新 atime),或使用 noatime(完全不更新 atime)以提升 I/O 性能。

rsync:增量同步利器

rsync(Remote Sync)是比 cp 更强大的文件同步工具,核心优势是增量传输——只传输变化的部分,不是每次都全量复制。

# rsync 基本语法:rsync [选项] 源路径 目标路径
# 重要:源路径末尾的 / 意义不同!
# rsync -a src/ dest/  : 将 src 目录内的内容复制到 dest 内
# rsync -a src dest/   : 将 src 目录本身复制到 dest 内(dest/src/...)

# 本地目录同步
rsync -av src/ dest/            # -a=archive(保留权限时间等)-v=verbose
rsync -avz src/ dest/           # -z=compress:传输时压缩(网络同步必加)
rsync -av --progress src/ dest/ # 显示传输进度和速度
rsync -av --delete src/ dest/   # --delete:删除目标中源没有的文件(镜像同步)

# 排除文件
rsync -av --exclude='*.log' --exclude='node_modules/' src/ dest/
rsync -av --exclude-from='rsync-exclude.txt' src/ dest/  # 从文件读取排除规则

# 增量备份:--backup-dir 将修改的旧文件保存到备份目录
rsync -av --backup --backup-dir=/backup/$(date +%Y%m%d) src/ dest/

# SSH 远程同步(最常用场景)
rsync -avz -e ssh src/ user@server:/remote/path/      # 上传到远程服务器
rsync -avz user@server:/remote/path/ /local/dest/     # 从远程服务器下载
rsync -avz -e "ssh -p 2222" src/ user@server:/dest/   # 指定 SSH 端口

# 模拟运行(不实际修改,查看会做什么)
rsync -av --dry-run src/ dest/   # --dry-run 或 -n,强烈建议在生产环境先试运行

文件批量操作技巧

# 批量重命名(使用 rename 命令)
rename 's/\.txt$/.md/' *.txt    # 将所有 .txt 重命名为 .md(Perl 版本)
rename 's/IMG_/photo_/g' IMG_*  # 批量替换文件名前缀

# 使用 for 循环批量操作(通用方案)
for f in *.log; do
  cp "$f" "/backup/${f%.log}_$(date +%Y%m%d).log"  # 复制并加日期后缀
done

# 使用 find + exec 批量操作
find . -name "*.jpg" -exec cp {} /photos/ \;        # 复制所有 jpg 文件
find . -name "*.tmp" -mtime +7 -delete              # 删除 7 天前的 tmp 文件
find . -empty -type f -delete                       # 删除所有空文件

# 安全地处理含空格的文件名
find . -name "*.txt" -print0 | xargs -0 rm          # -print0 和 -0 用 NULL 分隔,处理空格
本章小结

Linux 文件操作的核心要点:① 理解 inode——文件名存在目录中,inode 存元数据,链接计数归零才真正删除;② cp -a 用于备份(保留权限时间),mv 在同一文件系统内只修改目录项(极快);③ 软链接(ln -s)灵活可跨文件系统,硬链接不能链接目录;④ 备份用 tar -czf,增量同步用 rsync -avz;⑤ file 命令通过魔数识别真实格式,不依赖扩展名;⑥ 使用 rm -itrash 避免误删文件。