Linux-02 文件、链接与归档压缩
「一切皆文件」是 Linux 最核心的抽象。这一篇从 inode 讲起——理解了 inode,「df 和 du 对不上」「删了文件磁盘没释放」「硬链接为什么不能跨文件系统」这一类问题就全部自洽了,不需要背结论。
后半部分是文件操作和归档压缩的实战,重点在那些「用了很多年但语义其实理解错了」的地方。
1. 一个文件在磁盘上是三层结构
先说结论,这是本篇的地基:
目录项(dirent) inode 数据块(block)
+------------------+ +------------------+ +----------+
| 文件名 | | 权限、uid、gid | | 文件内容 |
| inode 号 --------+------->| 大小、时间戳 | | |
+------------------+ | 链接数 | | |
| 数据块指针 ------+------->| |
「目录」就是一张 +------------------+ +----------+
名字 → inode 号的表 元数据都在这里
三个关键推论,后面所有内容都由此展开:
- 文件名不属于文件本身,它存在「目录」这个特殊文件里。所以同一个 inode 可以有多个名字(硬链接)。
- 元数据(权限、时间、大小)在 inode 里,不在目录项里。所以
ls -l要读每个文件的 inode,这就是为什么ls -l比ls慢很多。 - inode 号只在单个文件系统内唯一。所以跨文件系统的硬链接从原理上就不可能。
1.1 inode 里到底有什么
ext4 的 inode 结构(内核源码 fs/ext4/ext4.h,节选):
struct ext4_inode {
__le16 i_mode; /* 文件类型与权限 */
__le16 i_uid; /* 所有者 uid */
__le32 i_size_lo; /* 文件大小 */
__le32 i_atime; /* access time 最近一次读 */
__le32 i_ctime; /* change time 最近一次改 inode */
__le32 i_mtime; /* modify time 最近一次改内容 */
__le32 i_dtime; /* deletion time */
__le16 i_gid; /* 所属组 gid */
__le16 i_links_count; /* 硬链接数 <- 删除逻辑的关键 */
__le32 i_blocks_lo; /* 占用块数 */
__le32 i_block[EXT4_N_BLOCKS]; /* 数据块指针 */
/* ... */
};
注意:里面没有文件名。这是 inode 结构最值得记住的一点。
ls -l 显示的每一列都能对应到上面的字段——权限来自 i_mode、用户来自 i_uid、大小来自 i_size、时间来自 i_mtime、第二列那个常被忽略的数字就是 i_links_count。
用 stat 可以直接看:
stat /etc/hosts
# File: /etc/hosts
# Size: 221 Blocks: 8 IO Block: 4096 regular file
# Device: fd01h/64769d Inode: 1179657 Links: 1
# ^^^^^^^^^^^ ^^^^^^^^^ 链接数
# Access: (0644/-rw-r--r--) Uid: (0/root) Gid: (0/root)
# Access: 2026-08-06 20:11:03.000000000 +0800
# Modify: 2026-03-15 09:22:41.000000000 +0800
# Change: 2026-03-15 09:22:41.000000000 +0800
1.2 三个时间:atime / mtime / ctime
这三个极容易混,用一张表说清:
| 时间 | 全称 | 什么操作会改它 | 举例 |
|---|---|---|---|
atime |
access time | 读文件内容 | cat f、grep x f |
mtime |
modify time | 改文件内容 | echo x >> f、编辑保存 |
ctime |
change time | 改 inode(含改内容) | chmod、chown、mv、改内容 |
判断口诀:mtime 管内容,ctime 管元数据,atime 管读取。
几个推论:
- 改内容一定会同时改
mtime和ctime(因为大小和块指针变了,inode 也变了) chmod只改ctime,不改mtime——所以mtime能可靠反映「内容何时变过」- 没有「创建时间」。ext4 的磁盘格式里其实有
i_crtime字段,但没有标准系统调用暴露它,stat显示的Birth经常是-
# 只改权限,观察哪个时间变了
touch f && stat -c "mtime=%y ctime=%z" f
# mtime=2026-08-06 20:15:01 ctime=2026-08-06 20:15:01
chmod 600 f && stat -c "mtime=%y ctime=%z" f
# mtime=2026-08-06 20:15:01 ctime=2026-08-06 20:15:09
# ^^^ 没变 ^^^ 变了
坑:
atime在现代发行版上默认不是每次读都更新。挂载选项默认是relatime——只在「atime 早于 mtime」或「atime 超过 24 小时」时才更新。因为每次读文件都写一次 inode 的开销太大了。所以想靠atime判断「这个文件最近有没有被访问」是不可靠的。用mount | grep relatime确认。
2. 硬链接与软链接
有了 inode 模型,这两个概念就是一句话的事:
- 硬链接:给同一个 inode 再加一个名字。目录项不同,inode 号相同。
- 软链接:一个全新的文件,它的内容是一段路径字符串。inode 号不同。
echo "hello" > a
ln a a_hard # 硬链接
ln -s a a_soft # 软链接
ls -li
# 1179657 -rw-r--r-- 2 lhx lhx 6 Aug 6 20:20 a
# 1179657 -rw-r--r-- 2 lhx lhx 6 Aug 6 20:20 a_hard <- inode 相同,链接数都是 2
# 1179702 lrwxrwxrwx 1 lhx lhx 1 Aug 6 20:20 a_soft -> a
# ^^^^^^^ inode 不同 ^ 类型是 l ^ 大小是 1 = 字符串 "a" 的长度
软链接的大小等于目标路径字符串的长度,这是个很直观的验证:ln -s /very/long/path x 之后 x 的大小就是 /very/long/path 的字符数。
2.1 两者的差异对照
| 维度 | 硬链接 | 软链接 |
|---|---|---|
| inode | 与源文件相同 | 独立的 inode |
| 内容 | 就是源文件本身 | 一段路径字符串 |
| 文件类型位 | -(普通文件) |
l |
| 能否跨文件系统 | 不能(inode 号只在本文件系统唯一) | 能 |
| 能否指向目录 | 不能(会形成环,破坏树形结构) | 能 |
| 能否指向不存在的目标 | 不能 | 能(悬空链接,ls 显示红底闪烁) |
| 删掉源文件后 | 仍可正常访问 | 失效 |
| 占额外空间 | 几乎不占(只多一个目录项) | 一个 inode + 路径字符串 |
为什么硬链接不能指向目录:如果允许 ln /a/b /a/b/c,文件系统就从树变成了有环图,find、du、rm -r 全部会无限递归。所以内核直接禁止普通用户这么做。
唯一的例外是 . 和 ..——它们本身就是内核自动创建的目录硬链接。这解释了目录的链接数规律:
mkdir -p d/{x,y}
stat -c "%h %n" d
# 4 d
# ^ 链接数 = 4 = d 自己的目录项 + d/. + d/x/.. + d/y/..
**规律:一个目录的链接数 = 2 + 它的子目录个数。**看到一个目录链接数是 2,就知道它没有子目录——find 就是用这个规律做优化的。
2.2 软链接的两个易错点
第一:cd 进软链接目录后,pwd 给的是哪个路径?
ln -s /var/log ll
cd ll
pwd
# /home/lhx/ll <- Shell 记住的「逻辑路径」
pwd -P
# /var/log <- 内核认的「物理路径」
cd .. # 回到 /home/lhx,不是 /var
cd -P .. 2>/dev/null; pwd -P # 用物理语义则回到 /var
这在脚本里是真实的坑:脚本自己算 SCRIPT_DIR 时如果被软链接调用,逻辑路径和物理路径不一致会导致找不到同目录的资源文件。稳妥写法:
# 拿到脚本所在的真实目录(解析所有软链接)
SCRIPT_DIR=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
第二:ln -sf 替换一个指向目录的软链接会出事
mkdir -p rel_v1 rel_v2
ln -s rel_v1 current
# ❌ 想把 current 指向 v2,结果在 v1 里面建了个链接
ln -sf rel_v2 current
ls current/
# rel_v2 <- 灾难:软链接被当成目录,链接建到了里面
# ✅ 加 -n(--no-dereference),把 current 当文件处理
ln -sfn rel_v2 current
-n 在切换发布目录时是必须的,忘了就会在旧版本目录里堆一堆垃圾链接。
3. 删除文件时内核做了什么
rm 调用的系统调用叫 unlink,名字就说明了一切——它删的是「链接」,不是「文件」。
unlink("a") 的完整逻辑:
1. 从目录里删掉「a -> inode 1179657」这条目录项
2. inode 的 i_links_count 减 1
3. if (i_links_count == 0 && 没有任何进程打开这个 inode)
-> 真正释放 inode 和数据块,磁盘空间归还
else
-> 什么也不做,数据还在磁盘上
**释放空间需要两个条件同时满足:硬链接数为 0,且打开引用数为 0。**这一条是后面所有现象的根源。
3.1 经典问题:df 说满了,du 加起来对不上
df -h /
# Filesystem Size Used Avail Use% Mounted on
# /dev/vda1 40G 39G 0G 100% / <- 满了
du -sh /* 2>/dev/null | sort -rh | head -5
# 8.2G /var
# 3.1G /usr
# ... <- 加起来只有 13G
差出来的 26G 就是**「已被 unlink 但仍被进程持有」的文件**:目录项没了所以 du 数不到,inode 没释放所以 df 仍然算它。
典型成因:日志文件被 rm 或被 logrotate 切走了,但应用进程还持有 fd 在往里写。
# 定位:找出所有被删但仍占空间的文件
lsof +L1
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
# myapp 12345 app 3w REG 253,1 27917287168 0 663214 /var/log/app.log (deleted)
# ^ NLINK=0 就是它
# 或者
lsof | grep deleted
解决办法按优雅程度排序:
# ✅ 最优:让进程重新打开日志文件(不中断服务)
kill -USR1 $(pgrep nginx) # nginx / 多数服务支持信号触发 reopen
systemctl reload myapp
# ✅ 可接受:重启进程
systemctl restart myapp
# ⚠️ 应急:不重启进程,直接把 fd 指向的内容清空
: > /proc/12345/fd/3
# 空间立刻释放,但进程的写偏移量还在原处,
# 后续写入会产生一个巨大的稀疏文件(看起来又满了,实际不占空间)
规律:df 和 du 对不上,先查 lsof +L1;根因几乎总是「删了正在被写的文件」。
3.2 顺带:inode 用完也会「磁盘满」
还有一种「磁盘满」是 inode 耗尽——空间还有,但 inode 分配光了,创建任何新文件都报 No space left on device:
df -h /data
# /dev/vdb1 500G 120G 380G 25% /data <- 空间还有 380G
df -i /data
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/vdb1 32768000 32768000 0 100% /data <- inode 用完了
# 找出 inode 大户(哪个目录文件数最多)
for d in /data/*; do echo "$(find "$d" 2>/dev/null | wc -l) $d"; done | sort -rn | head
成因几乎总是「海量小文件」:缓存目录、session 文件、按请求切分的日志。ext4 的 inode 数在 mkfs 时就固定了,事后无法扩容,只能删文件或重建文件系统(mkfs.ext4 -i 8192 调密度)。xfs 是动态分配 inode 的,没这个问题——这是选文件系统时值得考虑的一点。
排查磁盘满,df -h 和 df -i 要一起看。
4. 文件操作里那些隐藏语义
4.1 cp 默认会丢东西
# ❌ cp 默认:不保留权限属主时间、不跟软链接、不递归
cp -r src/ dst/
# ✅ 归档模式,等价于 -dR --preserve=all
cp -a src/ dst/
cp -a 保留权限、属主、时间戳、软链接原样、扩展属性。复制部署目录、备份配置时必须用 -a,否则文件全变成你自己的属主、时间戳全变成现在,出问题时无法判断「这文件是什么时候的」。
还有一个尾斜杠的经典陷阱:
cp -a src dst/ # dst 存在 -> 结果是 dst/src/
cp -a src/ dst/ # 结果同上(cp 对源的尾斜杠不敏感)
rsync -a src dst/ # -> dst/src/
rsync -a src/ dst/ # -> 把 src 的【内容】放到 dst/ 下 <- 差别在这
rsync 对源路径的尾斜杠敏感,cp 不敏感。这是数据同步时最常见的事故来源:多一个斜杠,文件就少一层目录,或者反过来。
生产建议:跨机器或大目录同步一律用 rsync 而不是 cp/scp:
rsync -avz --delete --progress src/ user@host:/dst/
# ||| |
# ||+- 压缩传输
# |+-- 输出详情
# +--- 归档模式(等于 -rlptgoD)
# --delete: 目标端删除源端没有的文件(做「镜像」时才加,很危险)
4.2 mv 在跨文件系统时不是原子的
mv /data/a /data/b # 同一文件系统 -> rename(2),原子操作,瞬间完成
mv /data/a /mnt/nfs/b # 跨文件系统 -> 实际是 copy + unlink,慢且非原子
这个区别很重要:只有同文件系统内的 mv 是原子的。利用这一点可以做「原子替换文件」,避免读方读到半个文件:
# ✅ 原子发布配置:先写临时文件,再 mv 覆盖
cat > /etc/myapp/config.json.tmp <<'EOF'
{"key": "value"}
EOF
mv /etc/myapp/config.json.tmp /etc/myapp/config.json
# rename(2) 是原子的,读方要么看到旧的完整版,要么看到新的完整版
# ❌ 直接重定向写:有一瞬间文件是空的或不完整的
cat > /etc/myapp/config.json <<'EOF'
...
EOF
跨文件系统时 mv 中途失败会留下半个文件,所以搬大数据到别的挂载点,用 rsync 校验后再删更安全。
4.3 rm 遇到海量文件会失败
rm -f /var/cache/app/*
# bash: /bin/rm: Argument list too long <- 参数超过 ~2MB 限制
* 是 Shell 展开的,几十万个文件名拼成的命令行超出了 ARG_MAX。正确做法:
# ✅ 用 find + -delete(最快,不 fork 额外进程)
find /var/cache/app -type f -delete
# ✅ 或 find + xargs 分批
find /var/cache/app -type f -print0 | xargs -0 rm -f
# ✅ 只删 7 天前的
find /var/log/app -name "*.log" -mtime +7 -delete
坑:删几百万个小文件时,磁盘 IO 会打满,
iostat里%util到 100%,同机的其他服务会被拖慢。生产上要限速:find ... -delete换成分批加 sleep,或者用ionice -c3 find ...把 IO 优先级降到 idle。
5. find:最该练熟的一个命令
find 的语法是「路径 + 条件 + 动作」,条件之间默认是 and:
find /var/log -name "*.log" -size +100M -mtime -7
# ^路径 ^按名字 ^大于100M ^7天内修改过
常用条件:
| 条件 | 含义 | 注意 |
|---|---|---|
-name "*.log" |
按名字(区分大小写) | 必须加引号,否则被 Shell 先展开 |
-iname |
同上,不区分大小写 | |
-type f / d / l |
普通文件 / 目录 / 软链接 | |
-size +100M |
大于 100MB | + 大于,- 小于,无符号=精确等于 |
-mtime -7 |
7 天内修改 | -7 是内,+7 是前,单位是天 |
-mmin -30 |
30 分钟内修改 | 分钟粒度用这个 |
-user app |
属主是 app | |
-perm -4000 |
有 SUID 位 | 安全审计常用 |
-maxdepth 2 |
只往下找两层 | 必须写在其他条件前面 |
-newer f |
比文件 f 更新 | 比记时间戳方便 |
三个高频实战:
# ① 找出占空间最大的 10 个文件(排查磁盘满)
find /var -type f -size +100M -exec du -h {} + 2>/dev/null | sort -rh | head -10
# ② 清理 30 天前的日志,但保留目录结构
find /var/log/app -type f -name "*.log.*" -mtime +30 -delete
# ③ 找出有 SUID 位的文件(安全审计)
find / -perm -4000 -type f 2>/dev/null
-exec {} + 和 -exec {} \; 的区别值得记住:
find . -name "*.txt" -exec grep hello {} \; # 每个文件 fork 一次 grep,慢
find . -name "*.txt" -exec grep hello {} + # 攒够一批一次调用,快得多
有一万个文件时,前者 fork 一万次,后者可能只 fork 十几次。默认用 +,只有当命令确实一次只能处理一个文件时才用 \;。
注意:
-maxdepth必须写在其他表达式之前,否则find会警告warning: you have specified the global option -maxdepth after the argument,且行为可能不符合预期。
6. 打包与压缩
6.1 先分清打包和压缩
这是本节最重要的概念:
- 打包(archive):把一堆文件合成一个文件,不减小体积。工具是
tar。 - 压缩(compress):用算法把一个文件变小。工具是
gzip/bzip2/xz/zstd。
Linux 的压缩工具绝大多数只能处理单个文件,所以要压缩一个目录,必须先 tar 打包再压缩——这就是 .tar.gz 这个双后缀的由来。
一堆文件 --tar--> a.tar --gzip--> a.tar.gz
+- 也写作 .tgz
zip 是个例外,它自己就能递归打包 + 压缩,这也是它在跨平台场景更常见的原因。
6.2 压缩算法怎么选
这是实际会影响成本的决策,不是背参数:
| 工具 | 后缀 | tar 参数 | 压缩率 | 压缩速度 | 解压速度 | 适用场景 |
|---|---|---|---|---|---|---|
gzip |
.gz |
-z |
中 | 快 | 快 | 默认选择,兼容性最好 |
bzip2 |
.bz2 |
-j |
较高 | 慢 | 慢 | 已被 xz/zstd 取代,不推荐新用 |
xz |
.xz |
-J |
最高 | 极慢 | 中 | 发布归档、只压一次下载很多次 |
zstd |
.zst |
--zstd |
中高 | 极快 | 极快 | 现代首选,日志归档、备份 |
生产建议:
- 日常传输、要求兼容性 →
gzip - 日志归档、数据库备份(每天压一次,压缩时间敏感)→
zstd - 发布包、容器镜像层(压一次分发无数次)→
xz - 新项目不要再用
bzip2,它在压缩率上不如xz、速度上不如zstd,没有存在的理由
zstd 值得特别推荐:它能在接近 gzip 压缩率的前提下快 3~5 倍,而且支持多线程(-T0 用满所有核):
# 压缩一个 10GB 的日志目录,对比一下
tar -czf logs.tar.gz logs/ # gzip: 约 180s,2.1GB
tar --zstd -cf logs.tar.zst logs/ # zstd: 约 45s,2.0GB
tar -I 'zstd -T0 -19' -cf logs.tar.zst logs/ # zstd 高压缩+多线程:约 60s,1.6GB
tar -cJf logs.tar.xz logs/ # xz: 约 900s,1.5GB
6.3 tar 的常用参数
tar 的参数记法:必须选一个动作,然后 -f 指定文件名。
# 核心动作(互斥,选一个)
# -c create 打包
# -x extract 解包
# -t list 只列出内容,不解开
# -r append 追加文件到已有包(不能用于压缩过的包)
# 常用修饰
# -f file 指定包文件名(几乎总是要写)
# -v verbose 显示处理的文件
# -z/-j/-J 调用 gzip / bzip2 / xz
# -C dir 切换到指定目录再操作
# 打包并压缩
tar -czf app.tar.gz app/
# 只看内容,不解开(拿到陌生压缩包必做这一步)
tar -tzf app.tar.gz | head
# app/
# app/bin/
# app/bin/myapp <- 确认有没有顶层目录
# 解压到指定目录
tar -xzf app.tar.gz -C /opt/
# 只解压其中一个文件
tar -xzf app.tar.gz app/config.yaml
# 打包时排除
tar -czf app.tar.gz --exclude='*.log' --exclude='node_modules' app/
现代 tar 可以自动识别压缩格式,解压时其实不需要指定 -z/-J:
tar -xf whatever.tar.gz # 自动认出是 gzip
tar -xf whatever.tar.xz # 自动认出是 xz
tar -xf whatever.tar.zst # 自动认出是 zstd
**记住 tar -xf 一个命令就够了。**打包时才需要指定用哪个算法。
坑:解压前一定先
tar -tf看一眼。有些包(尤其是别人手工打的)没有顶层目录,直接解压会把几百个文件散落到当前目录,清理起来很痛苦。安全做法是永远-C到一个空目录:mkdir tmp && tar -xf x.tar.gz -C tmp。
6.4 单文件压缩工具的一个共同陷阱
gzip a.txt
ls
# a.txt.gz <- 源文件 a.txt 没了!
# 用 -k 保留源文件
gzip -k a.txt
ls
# a.txt a.txt.gz
gzip/bzip2/xz 默认删除源文件,zip 默认保留。这个不一致坑过很多人。都支持 -k(keep)保留。
另外它们压缩目录时是「逐个压缩目录里的每个文件」,而不是压成一个包:
ls dir/
# a b
gzip -r dir/
ls dir/
# a.gz b.gz <- 变成两个独立的 .gz,不是一个 dir.gz
这再次说明为什么必须先 tar。
流式压缩才是这些工具真正的用法——不落地临时文件:
# 数据库备份直接压缩,不产生中间的巨大 .sql 文件
mysqldump mydb | zstd -T0 > mydb.sql.zst
# 边解压边处理,不占磁盘
zstdcat mydb.sql.zst | mysql mydb
# 压缩包里搜索,不解压
zgrep "ERROR" app.log.gz
zcat app.log.gz | awk '{print $1}' | sort | uniq -c
zcat/zgrep/zless 这一组是查历史日志的必备工具(xz 对应 xzcat/xzgrep,zstd 对应 zstdcat/zstdgrep)。
7. 实战:用软链接做零停机发布
把本篇的知识串起来。这是一个经典的发布目录布局:
/opt/myapp/
+-- releases/
| +-- 20260801_143022/ <- 历史版本
| +-- 20260805_091544/
| +-- 20260806_201133/ <- 新版本
+-- current -> releases/20260806_201133 <- 软链接
+-- shared/
+-- config/ <- 各版本共用的配置
+-- logs/
服务的 systemd unit 里 WorkingDirectory=/opt/myapp/current,发布时只需要切换这个软链接。
#!/bin/bash
set -euo pipefail
APP_DIR=/opt/myapp
REL="$APP_DIR/releases/$(date +%Y%m%d_%H%M%S)"
# ① 解包新版本到独立目录(此时线上流量还在旧版本)
mkdir -p "$REL"
tar -xf /tmp/app.tar.zst -C "$REL"
# ② 软链接引用共享的配置和日志目录
ln -sfn "$APP_DIR/shared/config" "$REL/config"
ln -sfn "$APP_DIR/shared/logs" "$REL/logs"
# ③ 原子切换:先建临时链接,再用 mv 覆盖
# mv 底层是 rename(2),同文件系统内是原子的,不存在「链接不存在」的瞬间
ln -sfn "$REL" "$APP_DIR/current.tmp"
mv -Tf "$APP_DIR/current.tmp" "$APP_DIR/current"
# ④ 重启服务
systemctl reload myapp
# ⑤ 只保留最近 5 个版本
ls -1dt "$APP_DIR"/releases/*/ | tail -n +6 | xargs -r rm -rf
三个关键点都用到了本篇的原理:
ln -sfn的-n:不加会把链接建到旧版本目录里(§2.2)mv -Tf而不是ln -sfn直接覆盖:ln -sf是「unlink 再 symlink」两步,中间有一瞬间current不存在;mv是单次rename(2),原子(§4.2)。-T强制把目标当成普通文件而不是目录- 回滚只需要把链接指回去,秒级完成,不需要重新解包
# 回滚到上一个版本
PREV=$(ls -1dt /opt/myapp/releases/*/ | sed -n 2p)
ln -sfn "$PREV" /opt/myapp/current.tmp
mv -Tf /opt/myapp/current.tmp /opt/myapp/current
systemctl reload myapp
**规律:需要「原子替换」时,用「写临时文件 + rename」,永远不要就地修改。**这个模式在配置发布、缓存文件更新、日志切割上都适用。
8. 面试题
Q:硬链接和软链接的区别?为什么硬链接不能跨文件系统、不能指向目录?
硬链接是给同一个 inode 增加一个目录项,inode 号与源文件相同;软链接是一个独立的新文件,内容是一段目标路径字符串,inode 号不同、文件类型是 l。不能跨文件系统是因为 inode 号只在单个文件系统内唯一,跨文件系统的目录项无法引用另一个文件系统的 inode。不能指向目录是因为那会让文件系统从树变成有环图,find、du、rm -r 这些依赖树形结构的遍历会无限循环——. 和 .. 是内核自己维护的例外。
Q:rm 一个文件之后,磁盘空间一定会释放吗?
不一定。rm 调用的是 unlink,只删除目录项并把 inode 的链接数减 1。只有当链接数为 0 且没有任何进程打开该 inode 时,数据块才真正释放。所以删掉一个正在被写入的日志文件,df 显示的已用空间不会下降,而 du 已经数不到它——这就是「df 和 du 对不上」的典型原因。用 lsof +L1 能列出所有这类文件,让持有它的进程 reload 或重启即可释放。
Q:atime、mtime、ctime 分别在什么时候变?有创建时间吗?
mtime 在文件内容改变时更新;ctime 在 inode 改变时更新(包括改内容、chmod、chown、mv);atime 在读取内容时更新。所以 chmod 只改 ctime 不改 mtime,而改内容会同时改 mtime 和 ctime。没有标准的创建时间——ext4 磁盘格式里有 i_crtime 字段,但没有 POSIX 系统调用暴露它。另外 atime 默认是 relatime 挂载模式,不是每次读都更新,不能用它可靠判断访问情况。
Q:磁盘空间还有很多,为什么创建文件报 No space left on device?
大概率是 inode 耗尽。df -h 看空间、df -i 看 inode,两者要一起看。成因通常是海量小文件(缓存、session、按请求切分的日志)。ext4 的 inode 总数在 mkfs 时固定,事后无法扩容,只能删文件或重建文件系统时用 -i 调整密度;xfs 是动态分配 inode 的,没有这个限制。另一个可能是磁盘配额(quota)打满,或者有进程持有大量已删除文件。
Q:tar 和 gzip 是什么关系?为什么会有 .tar.gz 这种双后缀?
tar 只负责打包——把多个文件合成一个,不减小体积;gzip 只负责压缩单个文件,不能把多个文件压成一个包。所以压缩目录必须两步:先 tar 成一个 .tar,再 gzip 成 .tar.gz。tar -z 参数只是帮你自动串起这两步。zip 不同,它自己同时做打包和压缩,所以只有单后缀。
Q:gzip、xz、zstd 怎么选?
看「压一次读几次」。日常传输和要兼容性用 gzip;日志归档、数据库备份这类每天压一次、压缩耗时敏感的场景用 zstd(速度是 gzip 的 3~5 倍,压缩率接近,还支持 -T0 多线程);发布包、容器镜像层这类压一次分发无数次的用 xz(压缩率最高但极慢)。bzip2 已经没有使用理由——压缩率不如 xz,速度不如 zstd。
Q:为什么 rm -f dir/* 会报 Argument list too long?怎么解决?
因为 * 是 Shell 展开的,不是 rm 的功能。几十万个文件名拼成的命令行超过了内核的 ARG_MAX 限制(约 2MB)。用 find dir -type f -delete 最好(不 fork 额外进程),或者 find dir -type f -print0 | xargs -0 rm -f 分批处理。注意:删海量小文件会打满磁盘 IO 拖慢同机其他服务,生产上要用 ionice -c3 降低 IO 优先级或分批加 sleep。
Q:怎么原子地替换一个配置文件或发布目录?
利用「同一文件系统内的 rename(2) 是原子的」这个性质:先把新内容写到同目录下的临时文件,再用 mv 覆盖目标。读方要么看到完整的旧版本、要么看到完整的新版本,不会读到半个文件。直接 > file 重定向不行,因为文件会先被截断为空。替换软链接则要用 mv -Tf tmp_link current,而不是 ln -sfn——后者是 unlink + symlink 两步,中间存在链接不存在的瞬间。注意:跨文件系统的 mv 退化为 copy + unlink,不再原子。
上一篇:Linux-01 认识 Linux 与目录结构 | 下一篇:Linux-03 用户、权限与安全模型
xingliuhua