
前几天同事抱着一台测试服务器来找我指着屏幕上两个目录问我这两个目录里的文件明明一模一样为什么ls显示的大小和du显示的大小完全不同左边那个目录ls -ld只有 4096 字节右边du -sh却显示好几个 G。这个问题太经典了当年我自己也懵过。说白了ls和du根本不是在测量同一个东西ls看的是“目录文件本身的元数据大小”du看的是“目录树下所有内容在磁盘上占用的块数”。弄清楚这层关系你就不会再被表面数字骗到。这篇内容主要写给被磁盘占用、目录大小困扰过的开发和运维也适合刚学 Linux 想搞懂文件系统底层逻辑的新人。我会把原理、常见原因、实操命令和排查套路一次讲透。1. 先说结论ls和du计量的根本不是同一件事1.1 目录在Linux里也是一种“文件”很多人刚接触 Linux 时会把“目录”当成一个装东西的容器就像 Windows 里的文件夹那样。但在 Linux 内核和文件系统的眼里目录就是一种特殊类型的文件只不过它内部存放的内容不是普通数据而是一张“文件名 - inode 号”的映射表。每个目录项就是一条记录写着这个文件名对应哪个 inode、文件类型是什么。所以目录本身一定占用磁盘空间哪怕它是一个空目录也至少要一个数据块来存放这张映射表。你可以用stat命令看到这个细节$ stat /tmp/test 文件/tmp/test 大小4096 块8 IO 块4096 目录 设备8,1 Inode525769 硬链接2这里的大小4096字节就是目录这个“文件”本身的逻辑大小。刚开始只有少量目录项时一个 4096 字节的块就够用了当目录项越来越多映射表放不下目录文件就会变大变成 8192、16384 甚至更大。关键点来了ls -ld看到的大小是目录文件自身这张映射表的大小而不是里面所有文件的总大小。Windows 的文件夹没有这种“自身大小”的直观概念所以从 Windows 迁移过来的朋友特别容易被绕晕这也是这个问题高发的背景之一。1.2 ls的“大小”是目录文件本身的逻辑大小ls -l显示的字段来自stat()系统调用返回的st_size字段。对于普通文件它等于文件包含的字节数对于目录文件它就是刚才说的目录映射表大小。$ ls -ld /tmp/test drwxr-xr-x 2 root root 4096 4月 1 10:00 /tmp/test很多人在看目录时习惯敲ls -l或ls -lh发现所有目录都显示 4.0K就以为目录只占 4K 空间。其实这 4.0K 只是目录表的起始大小。真正占空间的是目录下面层层叠叠的文件内容。如果目录里有几十万个文件你可以看到ls -ld的大小变成 8192、16384 等这就是目录表扩容的表现。但无论它怎么变它衡量的是“目录文件本身的逻辑大小”永远不是“目录里装了多少数据”。1.3 du的“大小”是整棵目录树的磁盘占用du的全称是 disk usage核心使命是统计磁盘块占用。默认情况下du会递归遍历目录把目录下每一个文件实际占用的磁盘块累加起来再加上各级目录自身的块开销。$ du -sh /tmp/test 24K /tmp/test注意这里有两个关键差异ls回答的是“目录文件本身的逻辑大小是多少字节”du回答的是“目录树整体占用了多少磁盘块”一个是测量对象不同一个是计量口径不同逻辑字节 vs 磁盘块我用一个表把这几个常用命令的差异列清楚命令统计对象计量口径典型结果ls -ld 目录目录文件本身逻辑字节st_size4096 字节stat -c %s 目录和 ls -ld 相同逻辑字节4096 字节du -sh 目录整棵目录树所有内容磁盘块默认按 1024 字节/块几十K ~ 几个Gdu -sh --apparent-size 目录整棵目录树所有内容逻辑字节总和接近文件字节数总和du的默认块大小在 GNU coreutils 中通常是 1024 字节也就是 1K加了-h会显示成人类可读的 K、M、G。需要注意一个隐藏因素如果环境变量POSIXLY_CORRECT存在du的部分行为会退回 POSIX 标准的 512 字节块模式导致同样的目录在不同机器上du数值有微小差别。这也是“同内容目录统计结果不一样”的隐性来源之一。2. 四个让数值“对不上”的底层原因2.1 块对齐机制小文件越多占用越大于文件大小文件系统以块为单位分配磁盘空间而不是按字节精确分配。绝大多数 Linux 文件系统的默认块大小是 4096 字节ext4、xfs 基本都是。这意味着一个文件即使只有 1 个字节也至少要占一个完整的 4096 字节块。假设目录下有 1000 个 1 字节的小文件$ mkdir -p /tmp/demo_small $ for i in $(seq 1 1000); do echo x /tmp/demo_small/f$i; done $ ls -l /tmp/demo_small/f1 | awk {print $5} 2 $ du -sh /tmp/demo_small 4.0M /tmp/demo_small1000 个文件每个实际内容只有 2 字节左右逻辑字节总和约 2K但磁盘上每个文件都独占一个 4K 块实际占用接近 4M。这就是du看到的“占用”远大于文件总大小的最常见原因。反过来一个大文件如果刚好填满多个完整块占用和文件大小就很接近。所以du不是ls -l文件大小的总和而是每个文件按块对齐后的总和。判断哪个数字“更真实”取决于你想知道什么想知道文件内容多大看ls -l想知道磁盘空间还剩多少必须看du。2.2 稀疏文件文件“看起来很大”实际占用很少稀疏文件指文件长度很大、但实际没有写满磁盘块的文件。典型的例子是数据库预分配一个 10G 的日志文件实际只写入了前面 100M后面全是“空洞”。文件系统很聪明不会为空洞分配真实块。创建稀疏文件非常简单$ truncate -s 10G /tmp/demo/sparse $ ls -lh /tmp/demo/sparse -rw-r--r-- 1 root root 10G ... /tmp/demo/sparse $ du -h /tmp/demo/sparse 0 /tmp/demo/sparsels显示 10Gdu显示 0。如果一个目录里躺着几个这种文件“目录内容一样、ls 和 du 统计结果却差好几个数量级”就非常正常了。判断方向很重要文件大小比占用大多半是稀疏文件或透明压缩文件系统占用比文件大小大多半是海量小文件 块对齐如果要把稀疏文件完整复制cp默认会尝试自动检测空洞用--sparsealways强制保持稀疏否则可能把 10G 的空洞全部填成真实数据白白爆炸一个分区。2.3 硬链接同一个inode只统计一次目录里有两个硬链接指向同一个 inode 时ls会把两个名字都列出来逻辑大小翻倍但du默认只按 inode 统计一次磁盘块占用。GNUdu内部会维护一张已统计过的 inode 列表第二次遇到同一个 inode 就不再累加。看个例子$ mkdir -p /tmp/demo_link $ echo hello /tmp/demo_link/orig $ ln /tmp/demo_link/orig /tmp/demo_link/link $ ls -l /tmp/demo_link -rw-r--r-- 2 root root 6 ... orig -rw-r--r-- 2 root root 6 ... link $ du -sh /tmp/demo_link 4.0K /tmp/demo_link $ du -l -sh /tmp/demo_link 8.0K /tmp/demo_linkls看到两个 6 字节的文件du默认只计一次所以是 4.0K加上-l参数强制重复统计才变成 8.0K。这个特性在做备份、迁移场景时特别重要你把一个目录打包传送到另一台机器如果原目录里存在大量硬链接du的统计结果会明显小于ls -l的累加结果。物理磁盘上同一份内容只存了一份这本身是正确的但统计口径如果没搞清很容易误判。2.4 权限、符号链接和挂载点造成的统计黑洞除了文件系统本身的机制还有几类“统计黑洞”会导致du的数比预期小权限不足。du遍历目录需要目录的读权限。如果某个子目录不可读它会提示cannot read directory然后继续统计其他能访问的部分最终结果会比实际占用小。用 root 或调整权限后再执行才能拿到完整数。符号链接。du默认不跟踪符号链接只统计符号链接自身占用的几个字节。如果目录里全是指向外部大文件的软链接du会显得目录非常小。想跟踪链接指向的目标要加-L$ du -sh /data 1.2K /data $ du -shL /data 28G /data挂载点。du默认只在当前文件系统内统计不会跨文件系统进入另一个挂载点。如果一个目录下面挂载了独立分区du不会把那个分区的内容算进来除非显式加-x或直接对挂载点目录执行。被删除但仍被打开的文件。进程持有文件句柄、文件已被rm时目录里看不到它ls和du都统计不到但磁盘块迟迟不释放。这种“空间不翼而飞”的情况du和ls都对不上只能靠df和lsof揪出来。这些黑洞叠加起来会让ls和du的差异变得特别难猜。排查时如果遇到结果离奇先按这四个方向逐一排除。3. 实操指南到底怎么看一个目录占了多少空间3.1 最常用的du命令组合日常排查目录空间我的常用命令基本就这几条需求命令只看目录总占用du -sh /path看下一层每个子项占用du -h --max-depth1 /path看逻辑大小总和du -sh --apparent-size /path排除某些目录再统计du -sh --exclude/proc --exclude/sys /path按大小排序展示du -h --max-depth1 /path 2/dev/null | sort -hr | head -20最开场白式的命令是这条$ du -h --max-depth1 /data 2/dev/null | sort -hr | head -20一屏就能看出哪一层目录最占空间然后顺着最大的目录往下钻。2/dev/null是为了屏蔽权限报错否则系统目录里那些不可读的子目录会刷屏。几个容易踩的小坑du -sh *不统计隐藏文件而--max-depth1会统计排查时优先用后者。du在非常大且文件很多的目录上跑得慢可以先排除明显不该统计的目录或者用--max-depth控制递归深度。macOS 的du不支持--max-depth要用-d 1跨平台写脚本时注意区分。3.2 用ls和stat交叉验证当du和ls对不上时先别急着怀疑系统坏了按下面顺序验证ls -ld 目录确认目录文件本身的逻辑大小。du -sh 目录确认整棵目录树的磁盘占用。du -sh --apparent-size 目录确认逻辑字节总和。对单个可疑文件执行stat看Size和Blocks字段。stat的输出里Blocks的单位是 512 字节不是 1024也不是文件系统块大小。计算实际占用时用Blocks * 512$ echo hi /tmp/onefile $ stat /tmp/onefile 文件/tmp/onefile 大小3 块8 IO 块4096 普通文件8 * 512 4096字节正好是一个 4K 块。文件内容只有 3 字节但占了一个块这就是块对齐的直观体现。还可以用ls -s查看文件占用它显示的是以 1K 块为单位的块数$ ls -s /tmp/onefile 4 /tmp/onefilels -s显示 4意思是 4 个 1K 块即 4K。整体对不上时可以用这个表做快速判断场景ls -l 大小du 占用结论普通小文件3 字节4K块对齐的正常现象稀疏大文件10G0文件有空洞硬链接文件两个名字各 6 字节4K同一 inode 只计一次目录本身4096整棵树占用统计对象不同3.3 实战案例50G磁盘空间“不翼而飞”的排查真实处理过一个案例一台服务器的/data分区df -h显示用了 92%但把/data下每个子目录的du -sh加起来怎么算都差了差不多 50G。排查步骤是这样的# 1. 先确认文件系统层面的占用 df -h /data # 2. 看第一层哪个目录最大 du -h --max-depth1 /data 2/dev/null | sort -hr | head -20 # 3. 顺着最大目录继续往下钻 du -h --max-depth2 /data/var 2/dev/null | sort -hr | head -20 # 4. 如果所有目录加起来都对不上 df查已删除但被进程持有的文件 lsof L1 | grep deleted最后一步是关键。L1表示列出 link 数小于 1 的文件也就是已经被删除但仍被进程握着的文件。一查发现某个 Java 进程长期持有一个被删掉的 50G 日志文件目录里早已看不见它du和ls都统计不到只有df知道空间没释放。遇到“du 对不上 df”的经典场景优先想这一条。进程不重启、文件句柄不释放空间永远不会回来。这也是我们在排查空间问题时ls、du、df三个命令缺一不可的原因。4. 常见问题速查与避坑手册4.1 两个“内容相同”的目录为什么du结果不一样这是标题里最想问的情况。答案要分几层第一所谓“内容相同”指的是文件名清单和文件字节内容一致但文件的块分配可能完全不同。A 目录里的文件是连续写入的大文件B 目录里是大量碎片化小文件小文件会因为最小块单位产生大量零头。第二稀疏文件的分布不同。A 目录里有个 10G 稀疏文件du根本不计算空洞B 目录里是实实在在写了 10G 数据。两者从ls -l看都是 10G但du一个显示 0一个显示 10G。第三硬链接数量不同。A 目录里有两个文件硬链接指向同一份内容du只计一次B 目录里文件名字和内容虽然一样却是两个独立 inode每个都要计一次。第四文件系统不同。同一个目录放在块大小 4096 的 ext4 上和放在块大小 1024 或启用了透明压缩的文件系统上du数值会不同。“内容相同”只是应用层看到的维度磁盘层的块分配图谱完全可能不同。所以比较两个目录到底是不是“一样大”不能只看文件名和文件字节还要用du -sh --apparent-size对比逻辑大小再分别用du -sh对比实际占用两个维度对上了才算真的一致。4.2 ls -ld显示4096为什么du显示几十G这其实是问得最多、也最好解释的一条ls -ld显示的是目录表自身大小du -sh显示的是目录树里所有文件的磁盘占用。一个是“装东西的盒子外壳重多少”一个是“盒子里所有东西加外壳一共多重”。拿系统里的/usr举例$ ls -ld /usr drwxr-xr-x 10 root root 4096 ... /usr $ du -sh /usr 9.8G /usr外壳 4K里面内容 9.8G。这个差异是正常的完全一致反而说明目录里没有文件。4.3 du显示0但目录里明明有文件碰到这种情况按顺序检查四个原因权限。当前用户读不了目录或里面的文件du无法统计严格来说它通常会输出警告但如果警告被重定向丢了看起来就像“没统计到”。符号链接。目录里放的都是指向其他位置的软链接du默认不跟踪加-L再看。挂载点。文件在另一个分区挂载在当前目录下du默认不跨文件系统加-x或用 root 到挂载点目录下执行。稀疏文件或透明压缩。文件逻辑上很大但实际分配的块就是 0尤其配合目录项自身占用不做精确展开时du显示 0 很正常。我遇到过一个特别迷惑的案例Btrfs 文件系统开启了透明压缩目录里一个 500M 的文本文件底层只占了几十Mdu显示结果远小于文件大小甚至小到离谱。加上 2.2 节里的稀疏文件场景du显示 0 不是系统坏了而是它确实在如实告诉你“没有分配实际磁盘块”。4.4 我一直用的排查小窍门养成的习惯是任何“目录大小不对”的疑问先问自己三个问题——我看的是逻辑字节还是磁盘块我统计的是目录表还是整棵树有没有符号链接、挂载点、硬链接和稀疏文件在中间捣乱把这三个问题在脑子里过一遍90% 的困惑当场就解开了。另外我还写了个小函数一键同时输出逻辑大小和磁盘占用space() { echo logical: $(du -sh --apparent-size $1 | cut -f1) echo disk: $(du -sh $1 | cut -f1) }执行space /data就能一眼看到两种口径的差异。差异越大越说明目录里存在稀疏文件、海量小文件或压缩文件系统。这个函数我放到所有服务器上的全局 profile 里排查空间问题时省事不少。最后再分享一个真实体会我刚踩这个坑时总觉得是文件系统出了问题后来才明白ls和du本来就不是为同一个问题设计的。ls回答“这个文件或目录在逻辑上有多大”du回答“这些东西实际占了多少磁盘块”。两个答案各有用途没有谁对谁错只有看的人是否清楚自己到底想了解哪一面。下次再有人拿这两个数字来问你你就可以直接用这篇文章里的例子让他现场跑一遍truncate和du他会比听任何解释都记得牢。