1. 项目概述为什么我们需要深挖du命令在 Linux 系统管理的日常工作中磁盘空间管理是绕不开的基础课题。无论是排查服务异常、清理日志文件还是规划存储扩容第一个浮现在脑海的命令往往是df -h查看磁盘使用率紧接着你就会需要du来定位究竟是哪个目录、哪个文件在“偷偷”占用宝贵的空间。duDisk 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-depthN控制显示的目录深度。这是进行层级分析的神器。du -h --max-depth1 /opt显示/opt下一级子目录的大小让你快速定位是哪个应用占用了主要空间。du -h --max-depth0 /opt等价于du -sh /opt只显示总和。如果不指定--max-depthdu会递归列出所有层级在深层目录中输出会非常冗长。注意-d是--max-depth的短选项但请注意在某些非常古老的系统或 BusyBox 环境中可能只支持--max-depth。生产环境中建议使用长选项以保证脚本兼容性。-a(all)默认情况下du只显示目录的用量。使用-a选项后会同时列出所有文件的用量。这在需要定位具体大文件时非常有用但输出量会急剧增加通常需要配合sort和head使用。**-c(total) 在最后一行产生一个总计。当同时查看多个目录时这个总计很有用。例如du -shc /var/log /var/cache会分别显示两个目录的大小并在最后一行显示它们的总和。2.2 进阶选项应对复杂场景--excludePATTERN排除符合指定模式shell glob 模式的文件或目录。这是清理和统计时的利器。例如你想统计一个项目目录的大小但不想计算其中的.git版本库和node_modules依赖目录du -sh --exclude.git --excludenode_modules /path/to/project或者使用一个模式--exclude“*.log”来排除所有日志文件。一个常见的坑是模式需要用引号包裹以防止 Shell 提前展开。--time显示目录或文件的最后修改时间。可以配合--timectime状态改变时间或--timeatime访问时间使用。这在分析“哪些老旧文件可能可以清理”时能提供时间维度信息。-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。假设你的文件系统块大小是 4KB4096字节。你创建一个只有 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_modulesdu显示的空间占用会显著大于所有文件逻辑大小之和因为每个小文件都至少浪费了块大小 - 文件大小的磁盘空间。这也是为什么有时候删除大量小文件后释放的空间没有预期多的原因之一——磁盘碎片和块分配策略在起作用。3. 经典组合拳du与其他命令的协作单独使用du往往不够结合其他命令才能发挥最大威力。以下是一些经过实战检验的“组合技”。3.1 定位目录空间占用排行这是最常用的场景。假设你想找出/var下哪个子目录最大du -h --max-depth1 /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_size1024 # 假设 1KB 块大小可通过 stat -f %k . 获取实际值这里简化 human_readable0 max_depth999 # 默认无限深度 # 解析参数简易版仅处理 -h 和 --max-depthN while [[ $# -gt 0 ]]; do case $1 in -h) human_readable1 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_index0 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_blocks0 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_size1024和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, actionstore_true, help人类可读格式) parser.add_argument(--max-depth, typeint, default999, help最大显示深度) # 添加一个 --apparent-size 选项用于演示 parser.add_argument(--apparent-size, actionstore_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, outputTrue): 递归计算目录大小并打印 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} 不存在, filesys.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-depth1先看第一层再针对性地深入大目录。使用ncduncdu在首次扫描后再次浏览同一目录几乎瞬时因为它缓存了结果。避开特定目录用--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 21 | grep -v “Permission denied”这样既能以 root 运行又能过滤掉烦人的错误信息同时保留其他输出。5.4 如何准确统计一个应用的真实占用比如你想知道 Docker 容器的磁盘占用直接du -sh /var/lib/docker可能不准确因为其中包含镜像层、卷、容器可写层等存在很多硬链接和共享层。使用专用工具对于 Dockerdocker 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即可启动。对于不习惯命令行的用户这是最好的选择。fpartfpdu用于将文件列表打包成块的工具但其附带的fpdu命令在统计海量文件时通过多进程并行化速度有数量级提升特别适合备份前统计等场景。选择建议日常交互式使用ncdu或dust体验最佳在脚本中或需要精确控制时用原版du需要极限速度扫描海量文件时考虑gdu或fpdu。理解并熟练运用du命令是每一位 Linux 系统使用者、运维工程师和开发者的基本功。它不仅仅是一个查看空间的命令更是你理解服务器存储状态、进行容量规划、排查系统问题的入口。结合find、sort、awk等工具你能构建出各种定制化的空间分析脚本。而通过亲手实现其简化版本你对文件系统如何分配和管理空间的认识会更加深刻。下次再遇到“磁盘空间不足”的告警时希望你能从容地打开终端用一连串精准的命令组合快速定位到问题的根源。