1. 项目概述:为什么我们需要深挖du命令?
在 Linux 系统管理的日常工作中,磁盘空间管理是绕不开的基础课题。无论是排查服务异常、清理日志文件,还是规划存储扩容,第一个浮现在脑海的命令往往是df -h查看磁盘使用率,紧接着,你就会需要du来定位究竟是哪个目录、哪个文件在“偷偷”占用宝贵的空间。du(Disk Usage)命令,这个看似简单的工具,其内涵远比du -sh *这一行命令丰富得多。很多运维工程师和开发者对它停留在基础使用层面,遇到复杂场景(比如排除特定目录、只统计特定文件类型、找出前N个大文件)时,要么写一堆管道命令组合,要么求助于图形化工具,效率低下。
实际上,du命令配合find、sort、awk等工具,能构建出一套极其强大的磁盘空间分析体系。更重要的是,理解du的工作原理(统计文件占用磁盘块的数量),对于理解 Linux 文件系统、inode、硬链接等核心概念有直接的帮助。本文将从命令行实战出发,深入解析du的每一个常用选项,并探讨其背后的文件系统原理,最后,我们会用 Shell 和 Python 两种方式,动手实现一个简化版的du命令核心逻辑。这不仅是为了掌握一个命令,更是为了构建一套高效的磁盘空间问题排查方法论。
2.du命令核心选项深度解析
du命令的语法格式是du [选项] [文件或目录]。它的核心逻辑是递归地统计指定目录下所有文件和子目录占用的磁盘空间总和。这里的“磁盘空间”指的是文件实际占用的磁盘块(block)数量,乘以块大小(通常是 4KB),而非文件内容的大小(ls -l显示的大小)。理解这一点区别至关重要,它直接影响了统计结果。
2.1 基础选项:从概览到明细
-h(human-readable)这是最常用的选项,没有之一。它将字节数转换为易读的单位(K, M, G, T)。例如,du -h /var/log会显示类似4.5M ./apache2的结果。没有这个选项,输出是一串纯数字(以KB为单位,除非用-b、-k、-m、-g指定),对人工阅读极不友好。
-s(summarize)仅显示指定目录的总用量,而不列出其内部子目录的详情。du -sh /home会直接告诉你/home目录总共占用了多少空间,这是快速查看目录总大小的标准姿势。
-d或--max-depth=N控制显示的目录深度。这是进行层级分析的神器。
du -h --max-depth=1 /opt:显示/opt下一级子目录的大小,让你快速定位是哪个应用占用了主要空间。du -h --max-depth=0 /opt:等价于du -sh /opt,只显示总和。- 如果不指定
--max-depth,du会递归列出所有层级,在深层目录中输出会非常冗长。
注意:
-d是--max-depth的短选项,但请注意,在某些非常古老的系统或 BusyBox 环境中,可能只支持--max-depth。生产环境中建议使用长选项以保证脚本兼容性。
-a(all)默认情况下,du只显示目录的用量。使用-a选项后,会同时列出所有文件的用量。这在需要定位具体大文件时非常有用,但输出量会急剧增加,通常需要配合sort和head使用。
**-c(total) 在最后一行产生一个总计。当同时查看多个目录时,这个总计很有用。例如du -shc /var/log /var/cache会分别显示两个目录的大小,并在最后一行显示它们的总和。
2.2 进阶选项:应对复杂场景
--exclude=PATTERN排除符合指定模式(shell glob 模式)的文件或目录。这是清理和统计时的利器。例如,你想统计一个项目目录的大小,但不想计算其中的.git版本库和node_modules依赖目录:
du -sh --exclude=.git --exclude=node_modules /path/to/project或者使用一个模式:--exclude=“*.log”来排除所有日志文件。一个常见的坑是:模式需要用引号包裹,以防止 Shell 提前展开。
--time显示目录或文件的最后修改时间。可以配合--time=ctime(状态改变时间)或--time=atime(访问时间)使用。这在分析“哪些老旧文件可能可以清理”时能提供时间维度信息。
-b, -k, -m, -g以指定单位显示大小,分别代表字节、KB、MB、GB。-h是自动适配,而这些是强制单位。注意,-b(--bytes)显示的是文件实际占用的磁盘字节数,而不是ls -l看到的“逻辑大小”。对于稀疏文件(sparse file)或有很多小文件的目录,这两个值差异会很大。
--apparent-size这个选项很有意思,它让du显示文件的“表观大小”(ls -l看到的大小),而不是实际占用的磁盘块大小。对于文本文件、未压缩的图片,这个值可能和实际占用接近;但对于稀疏文件、有很多硬链接的文件,或者文件系统有压缩/去重功能时,这个值会和-b的结果有巨大差异。它帮助你从另一个角度理解存储。
-l和-L(小写L和大小L)
-l:统计所有文件的大小,即使这些文件是硬链接。默认情况下,du对于硬链接文件,只会在其原始 inode 位置统计一次,避免重复计算。-l选项会取消这个优化,多次统计,可能导致结果远大于实际磁盘用量。除非你明确知道自己在做什么,否则不要使用-l。-L:解除符号链接(软链接)的引用。默认情况下,du不会追踪符号链接指向的目标(避免循环链接等问题)。使用-L后,它会去统计链接目标的大小。这可以用来统计一个充满软链接的目录的实际空间占用。
2.3 原理剖析:du统计的是什么?
为什么du和ls -l看到的大小经常不一样?根源在于文件系统的最小分配单元——块(Block)。
假设你的文件系统块大小是 4KB(4096字节)。
- 你创建一个只有 100 字节的文本文件
tiny.txt。 ls -l tiny.txt会显示文件逻辑大小是 100 字节。- 但
du -b tiny.txt或du -h tiny.txt(默认)会显示 4KB。因为磁盘上即使只存了100字节的数据,系统也必须为其分配一个完整的 4KB 块。这 4KB 就被“占用”了,其他文件不能使用。
同理,一个 4100 字节的文件会占用 2 个块(8KB)。du统计的就是这些块的数量总和。因此,对于存在大量小文件的目录(比如 Docker 容器层、node_modules),du显示的空间占用会显著大于所有文件逻辑大小之和,因为每个小文件都至少浪费了(块大小 - 文件大小)的磁盘空间。这也是为什么有时候删除大量小文件后,释放的空间没有预期多的原因之一——磁盘碎片和块分配策略在起作用。
3. 经典组合拳:du与其他命令的协作
单独使用du往往不够,结合其他命令才能发挥最大威力。以下是一些经过实战检验的“组合技”。
3.1 定位目录空间占用排行
这是最常用的场景。假设你想找出/var下哪个子目录最大:
du -h --max-depth=1 /var 2>/dev/null | sort -hr | head -102>/dev/null:将du命令访问无权限目录产生的错误信息(stderr)丢弃,让输出更干净。sort -hr:-h选项让sort能正确排序人类可读的单位(K, M, G),-r是反向排序,即从大到小。head -10:只显示前10行。
3.2 查找系统中最大的N个文件
结合find和du可以精准定位大文件。但更高效的方法是直接使用find的-exec或-print0结合xargs:
# 方法1:在当前目录及子目录下查找大于100M的文件,并显示大小 find . -type f -size +100M -exec du -h {} \; | sort -hr # 方法2:使用 xargs 处理大量文件时效率更高 find / -type f -size +500M 2>/dev/null -print0 | xargs -0 du -h | sort -hr | head -20实操心得:在根目录
/下执行find时,务必加上2>/dev/null忽略权限错误,否则输出会被大量的 “Permission denied” 刷屏。同时,-print0和xargs -0使用空字符作为分隔符,可以安全处理包含空格或换行符的文件名,是生产环境脚本的必备写法。
3.3 按文件类型统计空间
如果你想统计某个目录下所有.log文件总共占用了多少空间:
find /var/log -name “*.log” -type f -exec du -ch {} + | tail -1-exec ... +与-exec ... \;的区别在于,+会将找到的多个文件一次性传递给du命令,而不是为每个文件启动一次du进程,效率高得多。最后用tail -1取出-c选项生成的总计行。
3.4 交互式浏览与清理(使用ncdu)
虽然这不是du命令本身,但ncdu(NCurses Disk Usage) 工具是du的终极增强版,它提供了交互式终端界面。安装后(apt install ncdu或yum install ncdu),运行ncdu /path/to/dir:
- 它会先扫描目录,这个过程类似
du。 - 扫描完成后,进入一个类似文件管理器的界面。
- 你可以用方向键导航,按
d删除文件/目录,按n按文件名排序,按s按大小排序。 - 界面顶部会显示当前目录大小,底部有操作提示。
对于需要深入探索和清理的复杂目录结构,ncdu的效率和体验远超反复执行du和rm命令组合。
4. 动手实现:用代码理解du的核心逻辑
理解原理最好的方式就是动手实现一个简化版。我们将分别用 Shell 脚本和 Python 实现一个mydu工具,核心功能是递归计算目录大小,并支持-h和--max-depth选项。
4.1 Shell 脚本实现
Shell 脚本实现的核心是递归函数和stat命令获取文件系统块信息。
#!/bin/bash # mydu.sh - 一个简化的 du 命令实现 block_size=1024 # 假设 1KB 块大小,可通过 `stat -f %k .` 获取实际值,这里简化 human_readable=0 max_depth=999 # 默认无限深度 # 解析参数(简易版,仅处理 -h 和 --max-depth=N) while [[ $# -gt 0 ]]; do case $1 in -h) human_readable=1 shift ;; --max-depth=*) max_depth="${1#*=}" if ! [[ "$max_depth" =~ ^[0-9]+$ ]]; then echo "错误:--max-depth 参数需要数字" >&2 exit 1 fi shift ;; *) target_dir="${1:-.}" shift ;; esac done target_dir="${target_dir:-.}" # 单位转换函数 format_size() { local size=$1 if [[ $human_readable -eq 0 ]]; then echo $((size / 1024)) # 输出为KB,模仿du默认行为 return fi local units=("K" "M" "G" "T" "P") local unit_index=0 while [[ $size -ge 10240 && $unit_index -lt ${#units[@]} ]]; do # 阈值设为10240以便更早转换单位 size=$((size / 1024)) ((unit_index++)) done if [[ $unit_index -eq 0 ]]; then echo "${size}K" else echo "${size}${units[$unit_index]}" fi } # 核心递归函数 calculate_size() { local dir="$1" local current_depth="$2" local total_blocks=0 local item if [[ $current_depth -gt $max_depth ]]; then return 0 fi # 处理当前目录下的所有条目 for item in "$dir"/*; do # 处理通配符未匹配的情况 [[ -e "$item" ]] || continue if [[ -f "$item" || -L "$item" ]]; then # 如果是文件或符号链接,获取其占用的块数(忽略链接指向) if [[ -f "$item" ]]; then # 使用 stat 获取块数,乘以块大小得到字节数,再除以1024得到我们的“块”数 local file_blocks file_blocks=$(stat -c "%b" "$item" 2>/dev/null) # %b: 分配的块数(文件系统块) if [[ -n "$file_blocks" ]]; then # 假设文件系统块是512字节,我们统一到1KB块。这是一个简化处理。 # 实际应获取块大小(stat -f %k),这里为演示简化。 ((total_blocks += file_blocks * 512 / 1024)) # 转换为我们的1KB单位 fi fi elif [[ -d "$item" ]]; then # 如果是目录,递归计算 local sub_dir_size sub_dir_size=$(calculate_size "$item" $((current_depth + 1))) ((total_blocks += sub_dir_size)) # 如果未超过最大深度,可以在这里输出子目录大小(模拟du) if [[ $((current_depth + 1)) -le $max_depth ]]; then echo "$(format_size $sub_dir_size) $item" fi fi done echo $total_blocks # 返回该目录的总块数(以1KB为单位) } # 主程序 echo "正在计算 '$target_dir' 的大小..." size_kb=$(calculate_size "$target_dir" 0) # calculate_size 函数内部已经输出了子目录详情,最后输出总计 echo "========================" echo "总计: $(format_size $size_kb) $target_dir"脚本解析与注意事项:
- 块大小处理:真实
du命令通过stat系统调用获取文件占用的“512字节块”数量。我们脚本中的block_size=1024和stat -c “%b”计算是一种简化。%b给出的是 512字节块的数量。我们将其转换为 1KB 单位以便与du的默认输出(KB)类比。 - 符号链接:脚本中通过
-L和-f判断,默认像du一样不追踪符号链接。实现追踪功能需要更复杂的逻辑。 - 性能:这个脚本为每个文件调用一次
stat,在文件极多时性能较差。真正的du命令是 C 语言编写,通过系统调用批量读取目录项,效率高得多。 - 错误处理:脚本使用
2>/dev/null静默stat的错误,实际应用可能需要更精细的权限错误处理。
4.2 Python 实现
Python 版本利用os模块,逻辑更清晰,也更容易扩展功能。
#!/usr/bin/env python3 # mydu.py import os import sys import argparse def parse_args(): parser = argparse.ArgumentParser(description='模拟 du 命令') parser.add_argument('path', nargs='?', default='.', help='目标目录') parser.add_argument('-h', '--human-readable', action='store_true', help='人类可读格式') parser.add_argument('--max-depth', type=int, default=999, help='最大显示深度') # 添加一个 --apparent-size 选项用于演示 parser.add_argument('--apparent-size', action='store_true', help='显示表观大小(逻辑大小)') return parser.parse_args() def convert_size(size_kb, human_readable): """将KB大小转换为可读字符串""" if not human_readable: return f"{size_kb:d}K" units = ['K', 'M', 'G', 'T', 'P'] unit_index = 0 size = size_kb while size >= 10240 and unit_index < len(units) - 1: size /= 1024.0 unit_index += 1 if unit_index == 0: return f"{size:d}K" else: return f"{size:.1f}{units[unit_index]}" def get_file_size_kb(filepath, apparent_size): """获取文件大小(KB)。apparent_size 为 True 时取逻辑大小,否则取磁盘占用。""" try: if apparent_size: # 逻辑大小 stat_info = os.lstat(filepath) # 用 lstat 不追踪符号链接 return stat_info.st_size / 1024.0 else: # 磁盘占用大小:块数 * 块大小(字节)/ 1024 -> KB # os.stat() 返回的 st_blocks 是 512字节块的数量 stat_info = os.stat(filepath) # 用 stat 获取磁盘块信息,对于链接会追踪 # st_blocks 是 512字节块的数量,转换为 KB return (stat_info.st_blocks * 512) / 1024.0 except (OSError, PermissionError): return 0.0 def calculate_dir_size(path, current_depth, max_depth, human_readable, apparent_size, output=True): """递归计算目录大小并打印""" total_size_kb = 0.0 try: entries = os.listdir(path) except PermissionError: if output and current_depth <= max_depth: print(f"权限不足: {path}") return 0.0 except FileNotFoundError: return 0.0 for entry in entries: full_path = os.path.join(path, entry) try: if os.path.islink(full_path): # 符号链接:如果不追踪,则其磁盘占用很小(存储路径信息) # 这里简化处理,如果是链接且不追踪,则按小文件或忽略处理。 # 我们选择忽略链接的磁盘占用(除非是损坏的链接,os.path.exists 为 False) if not os.path.exists(full_path): # 损坏的链接 pass elif apparent_size: # 表观大小模式下,链接本身大小很小,我们忽略 pass else: # 磁盘占用模式下,链接本身占用一个inode和少量数据块 total_size_kb += get_file_size_kb(full_path, False) elif os.path.isfile(full_path): file_size_kb = get_file_size_kb(full_path, apparent_size) total_size_kb += file_size_kb elif os.path.isdir(full_path): subdir_size_kb = calculate_dir_size(full_path, current_depth + 1, max_depth, human_readable, apparent_size, output) total_size_kb += subdir_size_kb if output and (current_depth + 1) <= max_depth: print(f"{convert_size(int(subdir_size_kb), human_readable):>8} {full_path}") except (OSError, PermissionError): continue # 忽略无法访问的条目 # 如果是根目录调用,且深度允许,打印当前目录(根目录)总大小 if output and current_depth == 0: print(f"{convert_size(int(total_size_kb), human_readable):>8} {path}") return total_size_kb def main(): args = parse_args() target_path = os.path.abspath(args.path) if not os.path.exists(target_path): print(f"错误:路径 '{target_path}' 不存在", file=sys.stderr) sys.exit(1) print(f"计算 '{target_path}' 的大小 (--max-depth={args.max_depth}, {'人类可读' if args.human_readable else 'KB单位'}, {'表观大小' if args.apparent_size else '磁盘占用'})...") total_kb = calculate_dir_size(target_path, 0, args.max_depth, args.human_readable, args.apparent_size) # 根目录的总计已在函数内打印 # 可以在这里打印一个最终总计,但为了模拟 du,我们在递归函数内控制输出更合适。 if __name__ == '__main__': main()Python 实现亮点与差异:
- 使用
argparse:提供了更健壮、更标准的命令行参数解析,支持长短选项。 os.lstat()vsos.stat():lstat不追踪符号链接,用于获取链接本身信息;stat会追踪链接。我们在获取表观大小时用lstat的st_size,获取磁盘占用时用stat的st_blocks。st_blocks:这是关键属性,它给出了文件分配的 512字节块的数量。du命令的核心就是对这个值进行递归求和。st_blocks * 512就是文件实际占用的磁盘字节数。- 异常处理:对权限错误(
PermissionError)和文件不存在(FileNotFoundError)进行了捕获,使脚本更健壮。 --apparent-size实现:通过一个选项轻松切换统计逻辑大小还是磁盘占用大小,清晰地展示了二者的区别。
通过这两个实现,你可以深刻理解du命令递归遍历文件树、调用stat获取块信息、以及进行单位换算的核心流程。自己动手写一遍,以后再使用du时,你会对它的输出结果有更直觉的理解。
5. 常见问题排查与实战技巧
即使掌握了命令,在实际使用中还是会遇到各种问题。下面是一些高频问题和处理技巧。
5.1du结果与df结果对不上?
这是最经典的问题。du -sh /统计的根目录空间,往往远小于df -h /显示的已用空间。原因主要有:
- 已删除文件被进程占用:如果一个文件被删除(
rm),但仍有进程打开它,其占用的空间不会被释放,df会显示为已用,但du找不到这个文件(因为目录项已删除)。用lsof | grep deleted可以找到这些“幽灵文件”。重启相关进程或系统即可释放。 - 文件系统预留空间:Ext4 等文件系统默认会为 root 保留 5% 的空间,这部分空间
du不会统计,但df会算在已用空间里。 - 文件系统元数据:Journal、inode 表等元数据占用空间,
du不统计。 - 容器和虚拟化层:在 Docker 或 LVM 环境下,某些存储层可能对
du不可见。 - 稀疏文件:
du默认按块统计,稀疏文件可能占用大量逻辑空间但实际磁盘占用很少。
排查步骤:首先检查lsof | grep deleted。如果问题依旧,尝试使用df -i查看 inode 使用情况,也许空间未满但 inode 用尽了。
5.2du执行速度慢,如何优化?
扫描大型目录(如数百万小文件)时,du可能非常慢。
- 使用
--time监控:time du -sh /large_dir了解实际耗时。 - 指定深度:用
--max-depth=1先看第一层,再针对性地深入大目录。 - 使用
ncdu:ncdu在首次扫描后,再次浏览同一目录几乎瞬时,因为它缓存了结果。 - 避开特定目录:用
--exclude跳过已知的、无关的或巨大的目录(如/proc,/sys,/mnt)。 - 终极方案:文件系统快照或只读挂载:如果允许,将文件系统以只读方式挂载,或在快照上执行
du,可以避免扫描期间文件变化的影响,有时也能提升一点 I/O 性能。
5.3 权限不足导致统计不全
du在遍历过程中遇到没有读取权限的子目录时会报错 “Permission denied”,并停止统计该目录。这会导致统计结果偏小。
- 使用
sudo:最直接的方式是用 root 权限运行sudo du -sh /path。但需谨慎,确保你理解在扫描什么。 - 忽略错误:如之前所述,用
2>/dev/null重定向错误输出,但这样你会不知道哪些目录被跳过了。 - 结合
find提权:可以编写脚本,利用find的-exec以特定权限执行du,但这比较复杂。
一个折中的办法是:sudo du -sh /path 2>&1 | grep -v “Permission denied”,这样既能以 root 运行,又能过滤掉烦人的错误信息,同时保留其他输出。
5.4 如何准确统计一个应用的真实占用?
比如你想知道 Docker 容器的磁盘占用,直接du -sh /var/lib/docker可能不准确,因为其中包含镜像层、卷、容器可写层等,存在很多硬链接和共享层。
- 使用专用工具:对于 Docker,
docker system df命令是更准确的选择。 - 理解存储结构:对于这类复杂应用,最好先了解其存储目录结构(例如,
/var/lib/docker/overlay2是存储驱动目录),再针对性地统计。可能需要结合docker ps -q | xargs docker inspect来查看各个容器的具体存储位置。
5.5 脚本中使用du的注意事项
在 Shell 脚本中捕获du的输出进行数值比较时,要注意-h选项输出的是字符串,不能直接用于计算。
# 错误示例 size=$(du -sh /dir | cut -f1) # size 可能是 “4.5G” if [[ $size -gt 1024 ]]; then ... # 这里会报错,因为字符串和数字比较 # 正确做法:使用不带 -h 的选项,得到 KB 数字 size_kb=$(du -sk /dir | cut -f1) # size_kb 是数字,单位KB if [[ $size_kb -gt $((1024 * 1024)) ]]; then # 判断是否大于 1GB echo “目录超过1GB” fi6. 性能对比与高级工具
当du无法满足需求时,还有一些更强大的工具。
dust一个用 Rust 编写的、更直观的du替代品。安装后(cargo install du-dust或使用包管理器),运行dust会以树状图形式展示磁盘使用情况,视觉效果更直观,能快速发现占比大的目录。
gduGo 语言编写的磁盘使用分析器,最大特点是并行扫描,在多核 CPU 和 SSD 上速度极快,远超传统的du。用法与du类似,gdu -h /path。
baobab(GNOME Disk Usage Analyzer)图形化工具,提供饼图和树状图。在终端输入baobab即可启动。对于不习惯命令行的用户,这是最好的选择。
fpart&fpdu用于将文件列表打包成块的工具,但其附带的fpdu命令在统计海量文件时,通过多进程并行化,速度有数量级提升,特别适合备份前统计等场景。
选择建议:日常交互式使用,ncdu或dust体验最佳;在脚本中或需要精确控制时,用原版du;需要极限速度扫描海量文件时,考虑gdu或fpdu。
理解并熟练运用du命令,是每一位 Linux 系统使用者、运维工程师和开发者的基本功。它不仅仅是一个查看空间的命令,更是你理解服务器存储状态、进行容量规划、排查系统问题的入口。结合find、sort、awk等工具,你能构建出各种定制化的空间分析脚本。而通过亲手实现其简化版本,你对文件系统如何分配和管理空间的认识会更加深刻。下次再遇到“磁盘空间不足”的告警时,希望你能从容地打开终端,用一连串精准的命令组合,快速定位到问题的根源。