1. 项目概述:从“磁盘空间去哪儿了”说起
作为一名在Linux世界里摸爬滚打多年的运维和开发者,我几乎每天都会和磁盘空间打交道。服务器报警磁盘空间不足,开发环境编译失败提示“No space left on device”,或者只是想清理一下个人目录,第一个浮现在脑海的命令,十有八九是du。这个看似简单的命令,却是我们洞察存储使用情况的“眼睛”。然而,我发现很多朋友对du的使用还停留在du -sh *的层面,面对复杂的目录结构、需要排序或过滤特定文件时,就显得力不从心,或者写出冗长低效的脚本。今天,我们就来彻底拆解du命令,不仅讲透它的每一个常用选项和组合拳,更重要的是,我会分享如何用 Shell 和 Python 去实现du的核心逻辑。理解其原理,不仅能让你在命令行中游刃有余,更能提升你解决实际存储问题的底层能力。无论你是刚接触 Linux 的新手,还是希望优化运维脚本的老兵,这篇详解都能让你对磁盘空间管理有全新的认识。
2.du命令核心机制与选项全解
du,全称 “disk usage”,用于估算文件和目录占用的磁盘空间。它的核心思想是递归地统计指定路径下所有文件的大小,并汇总。这里有一个关键概念:磁盘占用空间通常不等于文件大小。因为文件系统有“块大小”(Block Size,通常为 4KB)的概念,即使一个文件只有 1 字节,它也会占用一个整块(4KB)。du默认报告的就是这个占用空间,这也是为什么du报告的值通常略大于ls -l看到的文件大小之和。
2.1 基础选项:你的瑞士军刀
让我们从最常用、必须掌握的选项开始。记住,du的默认行为是递归显示目录及其所有子目录的磁盘使用情况,单位是 KB。
-h或--human-readable:这是使用频率最高的选项之一。它以人类易读的形式(K、M、G、T)显示大小,而不是一长串数字。例如,2048会显示为2.0K,1048576显示为1.0G。强烈建议在任何时候都加上-h,除非你在进行脚本化的数值比较。-s或--summarize:仅显示指定目录的总用量,而不列出其内部每个子项。当你只关心一个目录总共占用了多少空间时,这个选项能避免输出信息过载。du -sh /home就是一个经典组合,用于快速查看/home目录的总大小。-c或--total:在最后一行显示所有参数的总计。当你同时查看多个目录,并想知道它们的空间总和时非常有用。例如du -shc /var/log /var/cache会分别显示两个目录的大小,并在最后一行显示total。--max-depth=N:控制显示的深度。这是管理复杂目录结构的利器。--max-depth=0:等同于-s,只显示总计。--max-depth=1:显示指定目录及其直接子目录的大小。这是分析目录空间分布的黄金命令:du -h --max-depth=1 /path/to/dir。- 你可以指定任意深度 N,来查看更细粒度的分布。
注意:
-h和--max-depth是好朋友,但和某些排序操作在脚本中直接配合时需要小心,因为-h输出的是带单位的字符串,不利于机器排序。对于脚本,可以先使用不带-h的du获取原始 KB 数值进行处理。
2.2 进阶选项与组合技巧
掌握了基础,我们来看看如何用du解决更复杂的问题。
-a或--all:不仅显示目录,也显示每一个文件的大小。在需要定位具体大文件时非常有用。可以结合--max-depth使用,例如du -ah --max-depth=2 /path查看两层深度内的所有文件和目录。--exclude=PATTERN:排除符合 PATTERN(支持通配符)的文件或目录。这是清理和排查时的神器。例如,你想统计一个项目目录的大小,但排除所有的.git目录和.log文件:du -sh --exclude=.git --exclude=*.log /project。-x或--one-file-system:仅统计与指定参数在同一文件系统上的内容。如果你在/目录下执行du,它默认会跨文件系统统计/home、/boot等挂载点。加上-x可以避免这种情况,让你只关注当前文件系统(例如根分区)的使用情况。-t, --threshold=SIZE:仅显示大小超过(或使用-t配合--apparent-size时等于)SIZE 的条目。这对于寻找“大块头”非常直接。例如du -ah -t 100M /会找出根目录下所有大于 100MB 的文件和目录。
一个强大的组合实例:找出/var目录下最大的 5 个目录(深度为 1)。
du -h --max-depth=1 /var 2>/dev/null | sort -hr | head -n 6解释:
du -h --max-depth=1 /var:获取/var下所有一级子目录的大小(人类可读)。2>/dev/null:将du命令可能遇到的“权限不足”等错误信息丢弃,避免污染输出。sort -hr:-h参数让sort能正确排序人类可读的数值(如 2K, 1G),-r是反向排序,即从大到小。head -n 6:取前6行(第一行通常是/var自己的总计,加上后面5个子目录)。
2.3--apparent-size与-b/-k/-m/-g的差异
这是容易混淆的一点。
--apparent-size:报告文件的表观大小,即ls -l看到的大小,也就是文件内容的实际字节数,不考虑文件系统的块开销。-b、-k、-m、-g:这些选项强制du以指定单位(Bytes, Kilobytes, Megabytes, Gigabytes)输出磁盘占用空间。注意,-k是默认选项。- 关键区别:一个 1 字节的文件,
du -b可能报告 4096(因为占了一个 4KB 的块),而du --apparent-size -b会报告 1。在需要精确计算数据量(如网络传输)时,用--apparent-size;在关心磁盘实际消耗时,用默认行为或-b/-k等。
3. 从原理到实现:动手编写一个简易du
理解了du的选项,我们深入其核心,尝试用代码实现它的基本功能。这不仅能巩固对命令的理解,更能锻炼文件系统编程能力。我们将分别用 Shell 和 Python 实现一个简化版的du -s(仅统计总大小)。
3.1 Shell 脚本实现:深度优先遍历
Shell 脚本实现的核心是递归遍历目录。这里我们实现一个mydu函数,模拟du -sb(以字节为单位显示总计)的行为。
#!/bin/bash # 函数:计算目录的磁盘使用量(字节,类似 du -sb) function mydu() { local dir="$1" local total_size=0 # 安全检查:参数是否存在且为目录 if [[ ! -e "$dir" ]]; then echo "Error: Path '$dir' does not exist." >&2 return 1 fi if [[ ! -d "$dir" ]]; then # 如果是文件,直接返回其大小(磁盘占用,取块大小整数倍) local file_size=$(stat -c %s "$dir") # 获取表观大小 local block_size=$(stat -c %B "$dir") # 获取块大小(通常是512或4096字节) # 计算占用的块数,并转换为字节 local blocks=$(stat -c %b "$dir") echo $(( blocks * block_size )) return 0 fi # 递归处理目录内的每一项 for item in "$dir"/*; do # 处理通配符未匹配的情况 [[ -e "$item" ]] || continue if [[ -d "$item" ]]; then # 如果是目录,递归调用 sub_size=$(mydu "$item") total_size=$(( total_size + sub_size )) else # 如果是文件,使用 stat 获取其占用的磁盘块信息 local blocks=$(stat -c %b "$item" 2>/dev/null) local block_size=$(stat -c %B "$item" 2>/dev/null) if [[ -n "$blocks" && -n "$block_size" ]]; then total_size=$(( total_size + blocks * block_size )) fi fi done # 最后,加上目录项本身占用的元数据空间(这里简化处理,通常很小,可忽略或通过stat获取) # 为简化,我们假设目录元数据占用为0,或通过统计其自身属性获得 # 实际上,`du`会统计目录节点本身占用的空间。我们可以用类似文件的方法: local dir_blocks=$(stat -c %b "$dir" 2>/dev/null) local dir_block_size=$(stat -c %B "$dir" 2>/dev/null) if [[ -n "$dir_blocks" && -n "$dir_block_size" ]]; then total_size=$(( total_size + dir_blocks * dir_block_size )) fi echo $total_size } # 主程序 if [[ $# -eq 0 ]]; then target="." else target="$1" fi size_in_bytes=$(mydu "$target") # 转换为人类可读格式(简易版) if [[ $size_in_bytes -ge 1073741824 ]]; then printf "%.2fG\n" $(echo "$size_in_bytes / 1073741824" | bc -l) elif [[ $size_in_bytes -ge 1048576 ]]; then printf "%.2fM\n" $(echo "$size_in_bytes / 1048576" | bc -l) elif [[ $size_in_bytes -ge 1024 ]]; then printf "%.2fK\n" $(echo "$size_in_bytes / 1024" | bc -l) else printf "%dB\n" "$size_in_bytes" fi脚本解析与注意事项:
- 递归陷阱:脚本使用了深度优先递归。对于非常深或链接环(虽然已通过
-e检查部分避免)的目录结构,可能导致栈溢出。生产环境的du命令使用系统调用和栈管理来避免这个问题。 stat命令:-c %b获取分配的块数,-c %B获取块大小(字节)。这是计算磁盘占用空间的关键。-c %s获取的是表观大小。- 性能:这个脚本为每个文件都调用一次
stat,在文件数量巨大时性能远不如C语言实现的原生du命令。原生du会使用fts(文件树遍历)系列函数进行更高效的遍历。 - 符号链接:此脚本没有特殊处理符号链接。原版
du默认不跟踪符号链接(-L选项会跟踪)。如果需要,可以在遍历前用-L选项检查stat或使用readlink -f。 - 错误处理:我们对
stat命令进行了静默错误处理(2>/dev/null),因为对某些无权限访问的文件,stat会失败。原版du会报告这些错误。
3.2 Python 实现:更清晰的对象模型
用 Python 实现可以更清晰地组织代码,并利用os模块。这里我们实现一个类,同时支持磁盘占用和表观大小的统计。
#!/usr/bin/env python3 import os import sys from pathlib import Path class SimpleDU: def __init__(self, follow_symlinks=False): self.follow_symlinks = follow_symlinks # 缓存文件系统的块大小,假设统一(简化处理) self.block_size = os.statvfs('.').f_bsize if hasattr(os, 'statvfs') else 4096 def get_disk_usage(self, path): """返回路径的磁盘占用空间(字节)""" try: stat_info = os.stat(path, follow_symlinks=self.follow_symlinks) except (OSError, PermissionError): # 模拟du的行为,跳过无权限访问的条目 return 0 # st_blocks 是512字节块的数量,需要转换 # 注意:os.stat 返回的 st_blocks 单位是 512 字节,无论实际块大小是多少 return stat_info.st_blocks * 512 def walk_and_sum(self, start_path): """递归遍历并汇总磁盘使用量""" total_usage = 0 start_path = Path(start_path).resolve() # 解析为绝对路径 if not start_path.exists(): print(f"Error: Path '{start_path}' does not exist.", file=sys.stderr) return 0 if start_path.is_file() or (self.follow_symlinks and start_path.is_symlink()): # 如果是文件(或跟踪的链接),直接返回其占用 return self.get_disk_usage(start_path) # 遍历目录 try: entries = list(start_path.iterdir()) except (PermissionError, OSError): # 无法读取目录,返回目录自身的元数据占用 return self.get_disk_usage(start_path) for entry in entries: try: if entry.is_dir() and not entry.is_symlink(): total_usage += self.walk_and_sum(entry) else: # 文件、符号链接等 total_usage += self.get_disk_usage(entry) except (PermissionError, OSError): # 跳过无法访问的条目 continue # 加上当前目录本身的元数据占用 total_usage += self.get_disk_usage(start_path) return total_usage def format_size(self, bytes_size): """将字节数格式化为人类可读字符串""" for unit in ['B', 'K', 'M', 'G', 'T']: if bytes_size < 1024.0: return f"{bytes_size:3.1f}{unit}" bytes_size /= 1024.0 return f"{bytes_size:.1f}P" if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description='A simple du implementation.') parser.add_argument('path', nargs='?', default='.', help='Target directory (default: current dir)') parser.add_argument('-L', action='store_true', help='follow symbolic links (like du -L)') parser.add_argument('--apparent-size', action='store_true', help='print apparent sizes (not implemented in this simple version)') args = parser.parse_args() du = SimpleDU(follow_symlinks=args.L) total_bytes = du.walk_and_sum(args.path) print(f"{du.format_size(total_bytes)}\t{args.path}")代码实现要点与避坑指南:
st_blocks的坑:Python 的os.stat()返回的st_blocks属性,其单位固定是 512 字节,而不是文件系统的实际块大小。这是 Unix 历史遗留问题。所以计算磁盘占用的正确方法是stat_info.st_blocks * 512。这是很多自行实现du时容易出错的地方。Pathlib的优势:使用pathlib.Path对象让路径操作更安全、直观,避免了字符串拼接可能带来的错误。- 递归与性能:和 Shell 版本一样,深度递归有风险。对于生产环境,可以考虑使用
os.scandir()(比os.listdir()更高效,因为它返回DirEntry对象,通常包含 stat 信息)进行迭代,并使用栈(stack)或队列(queue)来管理遍历,实现广度优先或非递归遍历。 - 权限处理:必须妥善处理
PermissionError和OSError,模拟du命令跳过无权限访问项的行为,而不是让程序崩溃。 - 符号链接:通过
follow_symlinks参数控制是否跟踪,使用Path.is_symlink()和os.stat(follow_symlinks=...)来正确判断和处理。 --apparent-size:上述简易版未实现此功能。若要实现,需要修改get_disk_usage方法,当需要表观大小时,直接返回stat_info.st_size。
4. 实战场景与高阶组合应用
理解了原理和基础实现后,我们来看看du在真实工作流中如何大显身手。
4.1 场景一:定位磁盘空间消耗的“元凶”
这是最常见的场景。服务器/空间告急,你需要快速找到是哪个目录或文件在“野蛮生长”。
高效排查流程:
- 全局概览:首先,使用
df -h确认是哪个挂载点空间不足。 - 逐层深入:假设是根分区,从根目录开始,逐层使用
du -h --max-depth=1 <path> | sort -hr向下钻取。# 第一层:查看根目录下哪个一级目录最大 sudo du -h --max-depth=1 / 2>/dev/null | sort -hr | head -20 # 输出可能显示 /var 最大 # 第二层:进入 /var sudo du -h --max-depth=1 /var 2>/dev/null | sort -hr | head -20 # 输出可能显示 /var/log 最大 # 第三层:进入 /var/log,并查看大文件 sudo du -ah /var/log 2>/dev/null | sort -hr | head -30 - 处理特定类型文件:如果发现是日志文件(如
*.log),可以使用truncate或> file.log清空(确保服务支持),或配置日志轮转(logrotate)。如果是缓存文件,检查是否可以安全删除。
实操心得:使用
sudo是因为很多系统目录普通用户无权读取。2>/dev/null过滤错误信息让输出更干净。务必谨慎操作sudo rm,尤其是在根目录下。建议先ls -lh确认文件,或使用rm -i交互删除。
4.2 场景二:统计特定类型文件的总大小
例如,你想知道一个项目里所有.py文件总共占了多少空间,或者所有图片文件(*.jpg, *.png)的存储成本。
# 方法1:使用 find 结合 du find /path/to/project -type f -name "*.py" -exec du -cb {} + | tail -1 # 解释:find 找到所有.py文件,-exec ... + 将它们一次性传给 du -cb,du计算每个文件的大小(-c)和总字节数(-b),tail -1 取最后一行总计。 # 方法2:使用 find 结合 awk(更高效,避免为每个文件启动 du) find /path/to/project -type f -name "*.jpg" -o -name "*.png" -printf "%s\n" | awk '{sum+=$1} END {print sum}' # 解释:find 的 -printf "%s\n" 直接输出每个文件的字节大小,awk 累加并打印总和。这种方法性能最好。4.3 场景三:监控目录增长与定时清理
结合crontab和du,可以实现简单的目录空间监控。
#!/bin/bash # 脚本:monitor_dir_size.sh TARGET_DIR="/var/log/myapp" THRESHOLD_MB=1024 # 1GB LOG_FILE="/var/log/du_monitor.log" current_size_mb=$(du -sm "$TARGET_DIR" | cut -f1) timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[$timestamp] Size of $TARGET_DIR: ${current_size_mb}MB" >> "$LOG_FILE" if [[ $current_size_mb -gt $THRESHOLD_MB ]]; then echo "[$timestamp] ALERT: $TARGET_DIR exceeds ${THRESHOLD_MB}MB! Current: ${current_size_mb}MB" >> "$LOG_FILE" # 触发清理动作,例如:find /var/log/myapp -name "*.log.?" -mtime +7 -delete # 或者发送告警邮件/消息 fi然后将此脚本加入 crontab,例如每小时运行一次:0 * * * * /path/to/monitor_dir_size.sh。
4.4 场景四:du与sort,awk,xargs的管道交响曲
Linux 命令的强大在于组合。du经常是管道流水线的起点。
- 找出当前目录下最大的10个文件:
find . -type f -exec du -h {} + 2>/dev/null | sort -rh | head -10 - 交互式删除大文件(非常危险,请先在测试环境练习):
# 找出大于100M的文件,并逐一询问是否删除 find . -type f -size +100M -exec du -h {} \; | sort -rh | while read size file; do read -p "Delete $file ($size)? [y/N] " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Deleting $file..." rm -- "$file" fi done - 按用户统计家目录使用量(适用于多用户环境):
sudo du -sch /home/* 2>/dev/null # 或者更详细地,结合 awk 格式化输出 sudo du -sk /home/* 2>/dev/null | awk '{printf "%10.1fG\t%s\n", $1/1024/1024, $2}'
5. 常见问题、性能调优与替代工具
即使对du了如指掌,在实际使用中还是会遇到一些坑和性能瓶颈。
5.1 常见问题与排查
du结果与df显示的总量对不上?- 原因:这是最常见的问题之一。
df报告的是文件系统级别的磁盘使用情况,包括元数据(inode表、日志等)、保留块(通常5%给root)以及已删除但仍被进程占用的文件(lsof | grep deleted)。du只统计当前可见文件的总和。 - 排查:如果
df显示使用率高而du统计小,可以检查是否有僵尸文件:lsof +L1或lsof | grep deleted。也可以使用tune2fs -l /dev/sdXX | grep Reserved查看保留块空间。
- 原因:这是最常见的问题之一。
du执行速度慢,尤其是挂载了NFS或大量小文件时?- 原因:
du需要 stat 每个文件,I/O 操作是主要瓶颈。网络文件系统(NFS)延迟更高。海量小文件会导致 stat 系统调用次数爆炸。 - 优化:
- 使用
-x避免扫描其他文件系统。 - 使用
--exclude跳过已知的非目标目录(如.git,node_modules)。 - 对于 NFS,可以考虑在服务器端执行
du并缓存结果(如果支持),或者使用基于快照的统计。 - 终极方案:使用文件系统自带的配额(quota)或树状使用量统计工具,如
ncdu(下文介绍)。
- 使用
- 原因:
du: cannot access ‘./.snapshot’: Permission denied之类的错误刷屏?- 处理:这是正常现象,说明你对该路径下的某些文件或目录没有读取权限。使用
2>/dev/null重定向标准错误来屏蔽这些信息:du -sh /path 2>/dev/null。如果确实需要知道哪些路径被跳过,可以将错误重定向到文件:du -sh /path 2>errors.log。
- 处理:这是正常现象,说明你对该路径下的某些文件或目录没有读取权限。使用
5.2 性能调优与替代方案
当du成为瓶颈时,可以考虑以下方案:
- 使用
ncdu(NCurses Disk Usage):这是一个基于文本界面的交互式du工具。它先扫描整个目录,将结果保存在内存中,然后允许你快速浏览、排序、删除。强烈推荐用于交互式分析。安装:apt install ncdu或yum install ncdu。使用:ncdu /path/to/scan。 - 使用
dust:一个用 Rust 编写的更直观的du替代品,默认输出树状图,视觉效果更佳。安装可通过 cargo:cargo install du-dust。 - 文件系统特性:一些现代文件系统(如 ZFS, Btrfs)支持原生的快照和空间使用报告,速度极快。例如 ZFS 的
zfs list -o space。 - 提前缓存:对于变化不频繁的目录,可以定期(如每天凌晨)运行
du并将结果保存到文件,需要时直接查看缓存。例如:du -sk /home > /var/cache/home_usage.dat。
5.3 脚本中的注意事项
在 Shell 脚本中使用du时,要特别注意:
- 数值比较:不要直接比较带
-h的输出。应该使用-k(KB)或-b(字节)获取纯数字进行比较。# 错误示例 usage=$(du -sh /dir | cut -f1) # 得到如 "1.2G" 的字符串,无法直接比较 # 正确示例 usage_kb=$(du -sk /dir | cut -f1) # 得到以KB为单位的整数 if [[ $usage_kb -gt $((1024 * 1024)) ]]; then # 大于1GB echo "Directory is larger than 1GB" fi - 跨平台兼容性:GNU
du(Linux 上常见)和 BSDdu(macOS 上)的选项略有不同。例如,BSDdu的-h和-d(对应--max-depth)行为一致,但-c是“显示总计”而 GNU 是-c。写可移植脚本时需注意,或者明确依赖 GNU coreutils。
经过对du命令从使用到原理,再到亲手实现的深度剖析,你会发现这个简单的命令背后连接着文件系统、操作系统和高效运维的诸多理念。下次当你再键入du时,希望你能更清晰地知道它在做什么,以及如何让它更好地为你服务。记住,工具的价值在于用它解决实际问题,而理解工具本身,能让你创造出新的解决方案。