我最早遇到这个需求,是在一次线上磁盘告警的时候。应用日志目录把根分区吃到了90%以上,我第一反应就是先看看到底是哪个子目录在“疯狂增长”,但当时我下意识敲出来的命令是 du -sh *——结果输出了一堆单文件的大小和一个小到离谱的目录汇总,完全不是我想要的。后来才把 du --max-depth=1 这个组合彻底用熟。这篇文章就专门讲清楚:怎么用一个命令,只列出当前目录下第一层子目录各自占了多少空间,以及这个命令背后那些不看文档就很容易踩的坑。
1. 为什么明明有 du -sh,却还是看不到每个子目录的大小
先解决一个最基础的疑惑:很多人不是不知道 du,而是用错了参数。du 的核心逻辑是“递归计算磁盘占用”,所以它的默认行为是一层一层往下钻,把每个子目录、孙目录全部列出来。真正让你“只看一层”的,不是 -s,而是 --max-depth。
1.1 du -sh 到底在算什么
拆开看:
-s是--summarize的缩写,意思是“只显示总计,不显示细节”。-h是--human-readable,自动把字节数换算成 K、M、G。
所以 du -sh /data 的结果是 /data 这一整个目录树的磁盘占用总和,它不会告诉你 /data/log、/data/lib、/data/tmp 分别多大。-s 存在的意义是“我只要一个总数”,而不是“我要看明细”。
那为什么有人会觉得 du -sh * 能看子目录大小?因为 * 在这里的语义是把当前目录下的每个条目作为独立参数传给了 du。系统会逐个计算 du -sh /data/log、du -sh /data/lib……从结果上看确实列出了每个子目录的大小,但这里隐藏着至少三个问题:
- 如果没有关闭 glob 展开或者目录条目特别多,
*展开后的参数列表可能超过ARG_MAX,直接报 “Argument list too long”。 *只能匹配非隐藏条目,.开头的目录会被自动忽略。- 如果当前目录下还有普通文件,
du -sh *会把它们也混在一起输出,你拿到的是“目录 + 文件”的混合列表,字段内容还说不上混乱,但确实不够纯粹。
1.2 反直觉的事实:裸 du 会递归到每一层
很多人第一次用 du 的时候,会顺手敲一个 du /data,结果发现输出几十行甚至几千行,每个子目录都带着一个数字。这不是命令坏了,而是 du 的默认深度就是“无穷大”,它会递归完整个目录树。
而且 du 的输出顺序也不是按大小排的,基本是文件系统目录项的遍历顺序。目录项的顺序由文件系统创建时间、删除后的重用情况决定,完全没有规律可循。所以如果你直接看裸 du 的输出,想从中找到最大的子目录,基本等于大海捞针。
1.3 ls -l 里的目录大小为什么是假的
还有一个容易混淆的点,就是 ls -ld /data 显示的大小。那个数字通常是 4096 或 4096 的整数倍,很多人误以为这就是目录的真实大小。
实际上,ls 显示的目录大小,只是目录文件本身(目录项数据结构占用的空间),也就是存放“子目录名、文件名、inode 号”这些元信息的空间。真正的内容——文件数据块、子目录里的所有递归内容——都不在目录文件里。所以 4096 这个数字基本是固定不变的,跟目录里装了多少数据没有关系。这也解释了一个挺常见的热搜问题:ls -l 第一行输出的 total 到底是什么意思。它表示当前目录下所有条目占用的磁盘块总数,单位通常是 1024 字节块,但这里的计算结果也不等于 du -sh 的汇总值,两者计算逻辑不同,别混着用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. du --max-depth=1:只输出第一层子目录的完整用法
这才是正题。--max-depth 是 GNU coreutils 里 du 提供的一个递归深度控制参数,语义非常直观:最多递归到第几层。
2.1 深度参数与控制逻辑
完整写法是:
bash复制du -h --max-depth=1 /data
也可以简写成 -d:
bash复制du -h -d 1 /data
两条命令完全等价。--max-depth=1 的含义是:计算 /data 下的第一层子目录的大小,并输出 /data 本身的总大小。
深度值的规律:
| 参数 | 输出内容 |
|---|---|
-d 0 |
等价于 -s,只输出当前目录树的总大小 |
-d 1 |
输出当前目录本身 + 第一层子目录的大小 |
-d 2 |
输出当前目录本身 + 第一层子目录 + 第二层子目录的大小 |
如果你只想要子目录的明细,不想要当前目录那一行汇总,可以配合 tail 或 grep 过滤。使用 GNU du 时,最后一行通常是当前目录的总大小(实际顺序取决于 shell 通配符和参数顺序),一般习惯是:
bash复制du -h --max-depth=1 /data | sort -hr
sorted 之后第一行经常就是当前目录本身,因为它的总大小通常是最大的。如果你不想看到它,加一个 grep -v '/data$' 即可:
bash复制du -h --max-depth=1 /data | grep -v '/data$' | sort -hr
需要注意,这里的正则 '/data$' 要和你实际传入的路径一致才行。
2.2 一个更稳的写法:指定路径时加不加点
很多习惯性写法是:
bash复制du -h --max-depth=1 .
点号代表当前目录,所有输出路径都会以 . 开头,比如 ./log、./lib。这样写的好处是相对路径不会写错,缺点是过滤当前目录那一行时,grep -v '/.$' 或 grep -v '\.$' 比较绕。
如果你是在排查根目录 / 的第一层子目录,我建议用绝对路径:
bash复制du -h --max-depth=1 /
这时候输出里会出现 /proc、/sys、/dev 这些特殊目录。它们的数值可能非常大(特别是 /proc,会动态反映内核和进程信息),会干扰判断,此时强烈建议加上 -x(--one-file-system),避免跨文件系统统计。加了 -x 之后,/proc、/sys、/dev 这些独立文件系统会被排除在外,输出干净很多。
2.3 加了 -h 之后怎么排序
-h 虽然让数字变成了 1.5G、230M 这种可读格式,但它有个副作用:无法直接用 sort -n 正确排序。因为 sort -n 只按数值比较,1.5G 会被当成无效数值,最终排序结果完全错误。
正确做法是用 GNU sort 提供的 -h 参数,它能识别 K、M、G 这些单位:
bash复制du -h --max-depth=1 /data | sort -hr
-r 是倒序,这样最大的目录会排在最前面,配合 head -20 就能快速锁定大目录:
bash复制du -h --max-depth=1 /data | sort -hr | head -20
如果你用的系统比较老,sort 不支持 -h,就退回传统方案:先用 du -k 输出以 KB 为单位的数字,排序后再人工换算:
bash复制du -k --max-depth=1 /data | sort -n -r | head -20
这种写法在任何 Linux 发行版上都能跑,不容易踩兼容性坑。
3. 输出顺序、单位换算与“为什么子目录相加不等于总数”
用 du 的过程中,有几个非常容易让新手困惑的细节。这里我把它们单独拎出来说清楚。
3.1 顺序问题:不要依赖 du 的原始输出顺序
前面提到过,裸 du 的输出顺序由目录项遍历顺序决定,不是按大小或字典序排列的。即使是带 --max-depth=1 的输出,顺序同样不可预测。
比如你执行:
bash复制du -h --max-depth=1 /opt
输出可能是:
text复制12G /opt/project_a
3.2M /opt/backup
850M /opt/tools
108K /opt/log
顺序看起来没有任何规律。这是正常现象,不必纠结。正确做法永远是把输出交给 sort 处理。如果你希望排序结果的第二字段(路径)也是稳定的,可以用:
bash复制du -h --max-depth=1 /opt | sort -k1 -h
但实际使用中,我用得最多的还是 sort -hr,因为排在前面的就是最大的,肉眼扫过去效率最高。
3.2 单位换算:-h 的单位其实不是“字节/千字节”
du -h 输出的 1.5G、230M,严格来说单位是二进制倍数,也就是 1.5 GiB、230 MiB。系统里的人通常直接称为“G”和“M”,不会刻意区分十进制 GB 和二进制 GiB。绝大部分场景下,这个差异不影响判断,因为磁盘告警看的是量级,不是精确值。
但如果你在写脚本,需要解析 du 的输出来做阈值判断,我建议不要用 -h,直接用 du -k --max-depth=1 /data,输出的数字固定以 KB 为单位。这样脚本里比较大小就非常方便:
bash复制du -k --max-depth=1 /data | awk '$1 > 1048576 {print $2}'
这段脚本的含义是找出超过 1GB(1048576 KB)的子目录。用固定单位永远是脚本开发里更稳的选择。
3.3 为什么子目录相加不等于 du -sh 的总数
我经常收到类似反馈:du -d 0 显示 /data 总共 20G,但把第一层每个子目录的大小相加,只有 18G,差的 2G 去哪了?
可能性有不少,逐个排查:
- 子目录条目本身占用的空间。
du -d 1输出里,每个子目录的大小已经包含了该目录数据块的开销;但如果你把当前目录下的那些普通文件也忽略掉,就会漏掉一部分。 - 硬链接的重复计算。
du默认把硬链接文件都计算在内,同一个 inode 如果出现在不同位置,会被重复计入。这就可能导致子目录相加的结果反而大于du -sh的总数。 - 被删除但仍然打开的文件。一个文件被删除后,只要有进程还持有它的文件描述符,磁盘空间就不会真正释放。
du遍历目录树时看不到这个文件,但df看到的空间占用仍在。这是“目录加起来不够”的最常见原因。 - 权限不足。
du在遇到无权限读取的子目录时,输出该目录占用为 0,并且会在 stderr 提示Permission denied。很多人习惯把错误信息扔到/dev/null里,结果就是漏算了。
这里最值得警惕的就是权限问题。拿生产服务器举个例子:某个业务目录下有一个 db_data 子目录,运行用户是 mysql,而你的系统账号不在同组,du 可能算出 db_data 为 0,但实际上它占了好几个 G。
建议:排查大目录时,不要一开始就 2>/dev/null,先看一遍 stderr。确认没有权限报错,再决定是否静默。如果你只是想快速知道“哪个子目录最大”,可以用 sudo du -h --max-depth=1 /data,避免权限导致的漏统计。
3.4 大目录下的速度问题
du 慢是另一个绕不开的话题。它的原理是递归遍历所有目录项,统计每个文件的磁盘块数。对于几百 GB、几百万文件的目录,整棵树的遍历可能需要几分钟甚至更久。
有人会想:我只想看第一层子目录,能不能不遍历所有子孙目录?答案是不能。因为第一层子目录的大小,本身就是它下面所有层级的递归总和。除非你已经知道某些子目录的内容可以忽略,否则 du 只能完整扫描。
有几个实际可行的提速思路:
- 用
--exclude排除掉那些确定不重要的大目录。比如排查应用日志时,/data/old_logs已经归档,可以从统计中排除:
bash复制du -h --max-depth=1 /data --exclude=old_logs
- 对于机械盘或 NFS 挂载,并发扫描可能有明显效果。把第一层子目录通过
xargs -P并行交给多个du -s处理:
bash复制ls -1 /data | xargs -P 4 -I {} du -s /data/{}
不过这个写法要小心:ls 的输出如果带空格,-I {} 会把每个路径当作一个参数块处理,倒是比较安全。但 ls 会忽略隐藏目录,而且它本身也不是为了给脚本传参设计的。更稳的写法是用 find -maxdepth 1 -type d -print0 | xargs -0 -P 4 -I {} du -s {}。
- 如果是跨网络文件系统(NFS、CIFS),
du的耗时大部分花在网络往返上,此时并发效果更好。但要注意尽量别在业务高峰期执行,避免加大磁盘或网络负载。
4. 顺着“只看一层大小”延伸出的高频场景
只看第一层目录大小本身是个基础动作,但实际工作中,它通常不是终点,而是起点。下面几个场景我都实际跑过,命令和技巧也都是验证过的。
4.1 找出当前目录下最大的几个目录
最经典的组合命令:
bash复制du -h --max-depth=1 /data | sort -hr | head -10
这条命令的意思很直白:先输出每个第一层子目录的大小,按大小倒序排列,取前 10 个。一次执行,就能定位到占用空间最多的目录。
如果你还想排除当前目录本身的那一行汇总,可以在管道中间加过滤:
bash复制du -h --max-depth=1 /data | sort -hr | grep -v '/data$' | head -10
grep -v '/data$' 会把路径结尾为 /data 的那一行丢掉,剩下的就是纯第一层子目录。
4.2 排除不需要统计的目录
如果 /data 下面有些目录你不需要关注,比如已知的备份目录 backup、临时目录 tmp,可以用 --exclude 把它们排除掉:
bash复制du -h --max-depth=1 /data --exclude=/data/backup --exclude=/data/tmp | sort -hr
还有一种更常见的情况:排查根目录时,有些挂载目录不能跟着一起统计。如果没有加 -x,du 会把挂载到 /data 下的另一块磁盘也算进 /data 的大小里。下面的用法可以确保只统计根分区本身:
bash复制du -x -h --max-depth=1 /
加了 -x 之后,/proc、/sys、/dev 和独立挂载的数据盘都不会被递归进去。
4.3 为什么 du 看到的数字和 ls -l 差很多
这要从“磁盘占用”和“文件表观大小”的差异说起。
ls -l里的size是文件表观大小,也就是文件里有多少字节数据。du默认统计的是磁盘占用,计算的是实际分配到的磁盘块数量。
对于普通文件,两者基本一致。但有两个场景差异非常大:
- 稀疏文件。比如某些数据库文件、虚拟机磁盘镜像,文件表观大小可能写的是 10G,但实际分配的块只有 2G。
ls -l会告诉你 10G,du会告诉你 2G。 - 压缩文件系统。如果文件系统启用了压缩,实际磁盘占用可能远小于表观大小。
如果想让 du 输出表观大小而不是磁盘占用,可以加 --apparent-size:
bash复制du -h --apparent-size --max-depth=1 /data
这个参数在磁盘告警排查时用得少,但在确认某个文件“真正占用多少磁盘”时很有用。
4.4 df 满了,但 du 加出来却远远不到
这个场景在服务器运维里非常经典。df -h 显示根分区已经用了 80G,但你在根目录下跑 du -x -h --max-depth=1 / | sort -hr,把所有目录加起来却只有 40G,剩下的 40G 去哪了?
最常见的元凶就是“已被删除但仍被进程占用的文件”。进程打开了一个文件,然后把文件删掉,磁盘上没有这个文件条目了,du 不会再统计它,但内核还保留着这个文件的数据块,直到进程关闭文件描述符。这个空间在整个文件系统层面是占用着的,所以 df 看得到,du 看不到。
排查方法:
bash复制lsof +L1
+L1 表示查找 link count 为 0 且仍然打开的文件。定位到进程后,重启或让其释放文件描述符,空间就会立刻回收。
4.5 快速识别日志目录是不是真的异常增长
有一次我监控到某个服务的日志目录占用了 20G,但业务同事坚称日志量不大,是误报。我直接用了两条命令验证:
bash复制du -h --max-depth=1 /var/log/app | sort -hr
ls -l /var/log/app | head -20
du 显示 app.log 占了 18G,但 ls -l 显示的 app.log 只有 3G。差异巨大,基本可以断定:这个文件曾经有更大体积,后来被 truncate -s 0 或 logrotate 截断,但占用的磁盘块并没有完全释放。再用 lsof +L1 | grep app.log 一看,果然是 logrotate 截断后还残留了进程句柄。
这种排查链路用到的命令非常基础,但组合在一起,就能快速定位别人怎么看都看不出来的问题。
5. 实操踩坑:我见过的最常见的几个反面用法
最后专门写一节,把我这些年看到过的、自己踩过的 du 使用坑都列出来。每一条都是真实场景,不是凭空想出来的。
5.1 du -sh * 在文件数量超大的目录里报参数过长
这个坑在上面已经提过,但值得重复一次,因为这个报错非常吓人:
text复制-bash: /usr/bin/du: Argument list too long
在一个包含几十万文件的目录里执行 du -sh *,Shell 在展开 * 时,会把所有文件名拼成一个超长的命令行参数,直接突破系统上限。轻则命令失败,重则把终端卡住。
正确姿势是不要依赖 shell 的 glob,直接让 du 自己遍历:
bash复制du -h --max-depth=1
如果确实需要只统计目录,不统计文件,可以用 find 配合:
bash复制find . -maxdepth 1 -type d -exec du -s {} \;
不过这种写法比较慢,实际场景中很少用到。
5.2 du -sh */ 在只有文件没有子目录时输出为空
这也是一次真实的终端体验。我在一个目录里跑:
bash复制du -sh */
结果什么输出都没有。原因很简单:这个目录下根本没有子目录,*/ 这个 glob 模式没有任何匹配项,Shell 会把原始字符串 */ 传给命令,然后 du 发现这个路径不存在。更隐蔽的是,如果目录下有普通文件但没有子目录,你可能会奇怪为什么没有输出。
如果你当前目录下既有文件又有子目录,*/ 在 Bash 里只会展开成子目录名,因为文件不满足 / 结尾的匹配条件。这样倒是能规避前面提到的“参数里混入文件”问题,但它依然无法处理隐藏目录。
想要彻底解决隐藏目录问题,可以开 dotglob:
bash复制shopt -s dotglob
但更推荐的做法还是回到 du -h --max-depth=1 .,一步到位,别用通配符猜。
5.3 sort -n 排不了 -h 的输出
这是一个排序陷阱。-h 输出的 800M、1.2G 不是纯数字,sort -n 无法识别单位后缀,会把它们全部算成 1.2、800 这样的“无效数字”,然后按字典序或内部规则排。于是你可能看到:
text复制1.5G
800M
850M
这种完全乱掉的顺序。
解决方案也很简单,用 sort -h。如果实在没有 -h 支持,就用 du -k 输出纯数字再排序:
bash复制du -k --max-depth=1 /data | sort -n -r | head -10
5.4 权限导致的“静默漏统计”
生产环境里,很多关键目录的权限是 700 或 750,属于特定业务用户。你用普通系统账号执行 du 时,可能根本没有权限读取其中的子目录。此时 du 的行为是:把该目录大小显示为 0,同时在 stderr 输出一行 Permission denied。
如果执行命令时没有重定向 stderr,你还能看到提醒;一旦加了 2>/dev/null,那就完全无声无息了。结果就是一个大目录被统计成 0,子目录加总远小于实际占用。
后来我的习惯是先跑一遍不加静默的版本,确认没有权限问题,再决定是否 2>/dev/null。如果你对目标目录有管理权限,最稳的方法是直接用 root 执行:
bash复制sudo du -h --max-depth=1 /data | sort -hr
5.5 不同操作系统下 -d 和 --max-depth 的兼容性
大部分 Linux 发行版使用的都是 GNU coreutils 的 du,同时支持 -d 和 --max-depth,没问题。但如果你在 macOS 或者 BSD 环境,--max-depth 可能不被识别,只能用 -d。反过来,有些极简容器镜像里只装了 BusyBox 的 du,它的参数支持又是另一套。
我推荐的做法是:在 Linux 上写死用 --max-depth,跨平台时先 du --help | grep depth 探一下,或者直接用 -d 1,因为 BSD 和 GNU 都认识这个简写。这算是在不同环境之间比较平衡的选择。
还有一点:du -d 1 在 GNU 环境下等价于 du --max-depth=1,但 BusyBox 的 du 不支持递归深度参数(必须用 du -s 加子目录拼接),所以写 Dockerfile 里的容器内脚本时,要留意基础镜像的 busybox 版本。遇到这种情况,我一般就直接在容器里执行:
bash复制for d in /data/*/; do
echo -n "$d: "
du -s "$d" | awk '{print $1}'
done
虽然繁琐,但至少不会因为参数不兼容而报错。
6. 一个我反复使用的最终命令模板
说了这么多,最后给一个通用性最强的命令模板。我平时在 Linux 服务器上排查大目录时,基本固定用这一段:
bash复制sudo du -h --max-depth=1 /data 2>/dev/null | sort -hr | head -20
拆开看每个部分的作用:
sudo:确保权限足够,避免漏统计。-h:人类可读,省心。--max-depth=1:只看第一层。2>/dev/null:静默掉权限告警等干扰信息(前提是前面已经确认无碍)。sort -hr:按大小倒序排列。head -20:只取前 20 个,屏幕不会刷爆。
如果你要排查根目录,把路径改成 /,再补一个 -x:
bash复制sudo du -x -h --max-depth=1 / 2>/dev/null | sort -hr | head -20
如果你想看某个业务目录下哪个子目录最大,把 /data 换成业务目录路径即可。这个模板我个人用了很多年,遇到绝大多数“目录空间异常”问题,先跑一遍,再结合 lsof +L1 查已删未释放文件,基本就能定位到根因。
7. 把“只看一层”扩展成日常运维习惯
用过几次 du --max-depth=1 之后,你会发现它对目录结构的理解会越来越深。以前遇到磁盘告警,我可能会慌张地到处翻文件,现在拿到一台服务器,第一件事就是列一下根目录的第一层空间占用,对整个系统的布局先有个数。这个习惯在排查问题的时候能省下大量时间,因为你脑海里已经有一张“哪些目录是大头”的认知地图。
有一次接手一台陌生服务器,登录之后没有任何信息,我就执行了 du -x -h --max-depth=1 / | sort -hr | head -15,30 秒内就知道 /var/lib/docker 占了 60G,/home/ubuntu 占了 100G,/var/log 占了 8G。接下来该看谁、该清谁,心里完全有数。这种“先看结构,再定位问题”的思路,我觉得比背任何命令参数都重要。
如果你刚开始用这些命令,建议也别急着背参数,先拿一台测试机跑几遍,把 -d 0、-d 1、-d 2 分别输出什么看明白,再把 -x、--exclude、--apparent-size 这些参数逐个试一遍。亲手验证过的命令,在关键时刻才记得住、敢用。
