磁盘满、配置没备份、日志没清干净——这三个事故的共同点是:出问题的文件在 ls 里根本看不见。.bashrc、.env、.git 这类点文件默认被跳过,软链接被 du 重复计数,而 find 是唯一能按“时间/大小/权限/属主”这些元数据做批量判断的工具。
这篇把三件事串起来:点文件是什么、两种链接差在哪、find 怎么和 xargs 配合才不出事故。
1. 点文件:只是命名约定
$ ls # 看不到 .env
$ ls -a
. .. .env .git app.py logs
$ ls -A
.env .git app.py logs
.env 的“隐藏”不来自权限,也不来自文件属性,只来自一个约定:名字以 . 开头,列目录的默认行为就跳过它。
| 命令 | 显示 . 与 .. |
显示点文件 | 用途 |
|---|---|---|---|
ls |
否 | 否 | 日常 |
ls -a |
是 | 是 | 看到 . ..,适合排查“目录里到底有什么” |
ls -A |
否 | 是 | 脚本里优先用它,避免 . .. 干扰 |
ls -aR |
是 | 是 | 递归,注意输出量大 |
find . -name '.*' |
— | 是 | 按条件筛点文件 |
两个容易误解的点:
- shell 的通配符
*不匹配点文件。rm *删不掉.env,这是历史上的保护设计;要删点文件得显式写rm .*或rm -f .[!.]*。 find不受这个约定影响。find . -name '*.log'会命中.hidden.log,因为find自己拿fnmatch匹配,不经过 shell 展开。
所以“备份时漏了配置”的典型原因不是备份脚本写错,而是用了 cp -r app/* dest/——* 不展开点文件,.env 就丢了。改成 cp -a app/. dest/ 或 rsync -a app/ dest/ 才完整。
2. 软链接与硬链接的六维差异
$ ln -s /srv/app/releases/v2 current # 软链接
$ ln /srv/app/data.db data.db.bak # 硬链接
$ ls -l
lrwxrwxrwx 1 deploy deploy 20 Nov 9 05:02 current -> /srv/app/releases/v2
-rw-r--r-- 2 deploy deploy 8192 Nov 9 05:02 data.db.bak
| 维度 | 软链接(symbolic) | 硬链接(hard) |
|---|---|---|
| inode | 独立 inode,内容是目标路径字符串 | 同一个 inode,是目录项的另一个名字 |
| 跨文件系统 | 可以 | 不行,Invalid cross-device link |
| 指向目录 | 可以 | 不行(普通用户与 root 都不允许,避免目录环) |
| 目标删除后 | 变成悬空链接,ls 仍显示,访问报 No such file |
数据仍在,直到链接数为 0 |
| 大小 | 等于目标路径长度(上面 20 字节) | 与目标文件相同,不额外占空间 |
| 链接数 | 不影响目标的 ls -l 计数 |
每加一个硬链接,第二列 +1 |
| 权限 | lrwxrwxrwx 是占位,实际按目标判定 |
与目标完全同一份权限与属主 |
ls -l 的第二列就是硬链接计数。一个文件的链接数降到 0 才真正释放空间——这就是“rm 了文件但 df 不降”的经典原因:还有进程持有 fd,或者还有另一个硬链接。
3. 解析链接:readlink 与 realpath
$ readlink current # 单层,输出目标路径
/srv/app/releases/v2
$ readlink -f current # 递归解析到最终绝对路径
/srv/app/releases/v2.1.3/bin/app
$ realpath current # 同上,GNU coreutils 提供
$ readlink -e missing-link; echo $? # 目标不存在时返回非 0
| 需求 | 命令 | 注意 |
|---|---|---|
| 看链接指向哪 | readlink link |
只解一层,链式链接只给第一跳 |
| 拿最终真实路径 | readlink -f / realpath |
目标不存在时 -f 仍输出拼接结果,-e 会失败 |
| 判断是否悬空 | test -e link vs test -L link |
-e 跟随链接,悬空时假;-L 只看是不是链接 |
| 找出全部悬空链接 | find /srv -xtype l |
-xtype l 表示“解引用后类型是链接” |
发布目录用 current -> releases/vN 是标准做法,代价是任何脚本都必须先解析路径再比较,否则 cd current && pwd 拿到的是逻辑路径,pwd -P 才是物理路径。
4. find 的核心参数
$ find /var/log -maxdepth 1 -type f -name '*.log' -mtime +7
$ find /srv -type f -size +100M -printf '%s\t%p\n' | sort -rn | head
$ find /etc -newer /etc/hostname -type f
| 参数 | 语义 | 常见误用 |
|---|---|---|
-maxdepth N |
限制递归深度(1 = 只看本层) |
忘了它导致扫全盘 |
-mindepth N |
跳过前 N 层 | 与 -maxdepth 组合可精确框定层数 |
-name / -iname |
按文件名匹配,支持通配 | 忘了加引号,被 shell 先展开 |
-type f/d/l |
普通文件/目录/软链接 | -type l 与 -xtype l 语义相反 |
-mtime +7 |
修改时间早于 7×24 小时 | 误以为是“最近 7 天” |
-mmin -30 |
30 分钟内修改过 | 与 -mtime 混用时互相收窄 |
-size +100M |
大于 100M(向上取整到单位) | 单位大小写:M 是 1048576,k 是 1024 |
-perm -644 |
至少包含这些位(- 是全部满足) |
与 -perm 644(精确相等)混淆 |
-user / -group |
按属主/属组 | 数字 UID 更可靠 |
-newer ref |
比参照文件更新 | 适合“上次发布之后改过什么” |
-print0 |
以 NUL 分隔输出 | 必须配 xargs -0 |
-mtime +7 的方向最容易错:+n 是大于 n 天前,也就是更旧;-n 才是最近 n 天内。find /var/log -mtime +7 -delete 删的是老日志,方向没错;但如果想“保留 7 天”,条件就写反了。
5. -exec {} \; 与 -exec {} +:差一个分号,差几十倍
# 每个文件 fork 一次
$ find /data -name '*.tmp' -exec rm -f {} \;
# 批量传给一个进程
$ find /data -name '*.tmp' -exec rm -f {} +
# 等价写法,但要注意文件名安全
$ find /data -name '*.tmp' -print0 | xargs -0 rm -f
| 写法 | 进程数 | 命令行长度限制 | 适用 |
|---|---|---|---|
-exec CMD {} \; |
每个匹配一次 | 不受限 | 命令本身不支持多参数,或需要逐条输出 |
-exec CMD {} + |
分批(ARGG_MAX 上限内) | 受 getconf ARG_MAX 约束 |
大批量删除/改权限,默认选它 |
xargs |
分批,可用 -P 并行 |
同上,-n/-s 可调 |
需要并行或需要管道分流 |
一万个文件时,\; 是一万次 fork+exec,+ 是几十次。这个差距在机械盘或容器里尤其明显,因为每次 exec 都要走一遍路径解析与 inode 查找。
6. 文件名含空格:-print0 + xargs -0
$ find /data -name '*.log' | xargs rm -f # 遇到 "my file.log" 会拆成两个参数
$ find /data -name '*.log' -print0 | xargs -0 rm -f # 正确
xargs 默认按空白(空格、Tab、换行)切分,文件名里只要有一个空格就会被拆错,轻则删不掉,重则误删。
| 场景 | 正确写法 |
|---|---|
| 文件名含空格 | find ... -print0 | xargs -0 CMD |
| 文件名含换行 | 只有 NUL 分隔可靠,-print0 是唯一正解 |
| 需要看到将要执行什么 | find ... -print0 | xargs -0 -t CMD |
| 需要限制并发 | find ... -print0 | xargs -0 -P 4 CMD |
用 -exec 系列 |
不用管,{} 天然按文件边界传参 |
同理,for f in $(find ...) 也是错的写法,IFS 会把带空格的名字切开;要用 while IFS= read -r -d '' f 配 -print0。
7. 四组高频组合
7.1 清理 7 天前的日志
$ find /var/log/app -type f -name '*.log' -mtime +7 -print # 先看清单
$ find /var/log/app -type f -name '*.log' -mtime +7 -delete
-delete 隐含 -depth,所以它必须写在表达式末尾,且不能与 -prune 同用。永远先 -print 一遍,再换 -delete。
7.2 找出大于 100M 的文件
$ find / -xdev -type f -size +100M -printf '%10s %p\n' 2>/dev/null | sort -rn | head -20
$ du -ah /srv | sort -rh | head -20
-xdev 不跨文件系统,避免扫到 /proc、挂载的网络盘。du 更适合看目录总量,find 更适合单文件清单——两者都会重复计算硬链接,du 加 -l 才按链接计数。
7.3 按权限筛
$ find / -xdev -perm -4000 -type f 2>/dev/null # 全部 SUID
$ find /srv -type f -perm -o+w # 其他人可写
$ find /srv -type d ! -perm -o+x # 目录缺 x,进不去
7.4 按时间做增量
$ find /srv/app -type f -newermt '2026-11-01' -printf '%TY-%Tm-%Td %p\n' | sort
$ find /srv/app -newer /srv/app/.last-deploy -type f
-newermt 接受人类可读时间,比 -newer 配参照文件更直观;-printf 的 %T 系列能直接排出时间线。
8. 排错:六种“find 结果不对”
| 现象 | 原因 | 处理 |
|---|---|---|
-name *.log 报错 |
通配符被 shell 先展开 | 加引号:-name '*.log' |
-mtime +7 结果为空 |
文件其实不够旧,或时间戳被 touch 改过 |
用 -mmin 交叉验证;stat 看 atime/mtime |
-size +100M 与 du 不一致 |
find 看 apparent size,du 看实际块 |
稀疏文件/压缩文件会差很多 |
| 找不到明明存在的文件 | 权限不足,find 静默跳过 |
2>/dev/null 去掉看 Permission denied |
| 扫到软链接目录里 | 默认 -P 不跟随,但参数写成了 -L |
明确 -P(默认)或 -H(只跟命令行给的) |
-delete 报 Directory not empty |
与 -depth 顺序冲突 |
把 -delete 放在最后,或改用 -exec rm -rf {} + |
9. 边界:这些场景别硬用 find
| 需求 | 更合适的工具 | 理由 |
|---|---|---|
| 按内容搜文本 | rg / grep -r |
find 只看元数据,不看内容 |
| 实时监控文件变化 | inotifywait |
轮询有延迟且耗 IO |
| 全盘秒级定位 | locate(updatedb) |
走预建索引,代价是数据可能过期 |
| 按目录看占用 | du / ncdu |
find 输出需要自己聚合 |
| 大规模并行处理 | parallel / xargs -P |
单线程遍历是瓶颈 |
一句话收口:点文件是命名约定,不是权限;硬链接是同一个 inode 的别名,软链接是一个路径字符串;find 负责“按元数据选出集合”,-print0 + xargs -0 负责“安全地把集合交给下一个命令”。
10. 自测
ls -a与ls -A的差别是什么?脚本里该用哪个?rm *为什么删不掉.env?find会不会也漏掉它?- 软链接能跨文件系统、硬链接不能,根本原因是什么?
-mtime +7筛出的是更旧的文件还是最近的文件?- 一万个文件用
-exec {} \;和{} +差在哪里? - 文件名含换行时,为什么只有
-print0+xargs -0可靠?