春江暮客

春江暮客的个人学习分享网站

Linux 文件很大却不占空间?用 Python 检查稀疏文件与磁盘分配量

2026-11-04 技术
Linux 文件很大却不占空间?用 Python 检查稀疏文件与磁盘分配量

ls -l 显示文件有 8 MiB,du 却只算出几 KiB:文件损坏了,还是磁盘统计不准?对包含“空洞”的稀疏文件,两者可能都正确,只是计算口径不同。

本文面向初级到中级读者,先创建一个小型稀疏文件,再用 Python 输出逻辑字节数、已分配字节数和硬链接身份。检查器只读元数据,不打开文件内容、不删除文件、不修改系统设置。

1. 方法:先统一比较口径

  1. ls -l 展示普通文件的逻辑长度;GNU du -B1 用字节单位展示分配量,du --apparent-size -B1 展示逻辑量。特别注意:du -b 等价于后者,不是“实际占用的字节数”。选项定义见 GNU du 手册。
  2. Python 的 st_size 是逻辑字节数;Linux 上 st_blocks * 512 是文件报告的已分配字节数。st_blksize 是建议 I/O 大小,不能拿来乘 st_blocks。字段定义见 Python stat 结果文档。
  3. 两个路径若具有相同的 (st_dev, st_ino),可能只是同一个文件的硬链接;报告保留身份,不按路径相加。Linux 的 stat 手册说明了链接与元数据行为。

分配量小于逻辑量只是线索,不是可回收空间估算。 压缩、共享数据块、预分配和文件系统实现都会影响解释;本工具不定位空洞,也不判断文件能否删除。df 与目录总量不一致时,应继续看磁盘空间与已删除文件排查。

2. 准备:使用独立练习目录

需要 Linux、已有的 Python 3.10+ 和 GNU coreutils;不需要 pip 包。本例在 Python 3.11.4、GNU coreutils 9.7 上测试,shell 命令适用于 bash 或 zsh。

  1. 先确认工具,再新建目录。重复实验时换一个目录名,不覆盖旧文件:
python3 --version
du --version
mkdir sparse-usage-demo
cd sparse-usage-demo
  1. 在这个目录执行下面的命令。它只在首尾写入 4 个字节,中间的逻辑区间没有逐字节写入;xb 会拒绝已有文件:
python3 - <<'PYCODE'
import os

size = 8 * 1024 * 1024
with open("sparse.bin", "xb") as stream:
    stream.write(b"HEAD")
    stream.seek(size - 4)
    stream.write(b"TAIL")
    stream.flush()
    os.fsync(stream.fileno())
with open("empty.bin", "xb"):
    pass
print("created sparse.bin and empty.bin")
PYCODE

在支持稀疏文件的文件系统中,这种跨越写入可以形成空洞;空洞读取为零,行为说明见 Linux lseek 手册。实际分配量由当前文件系统决定,不保证所有挂载类型都得到相同节省。

不要对现有数据库、虚拟磁盘或日志运行 truncate 来“修复大小”。 缩短文件会丢失数据。本文没有这类操作。

3. 实现:只读文件用量检查器

把下面代码保存为 file_usage.py:

"""Read Linux regular-file size metadata. Python 3.10+."""
import argparse
import json
import os
import stat
import sys


def inspect_file(path):
    info = os.lstat(path)
    if not stat.S_ISREG(info.st_mode):
        raise ValueError(f"{path!r}: not a regular file; no final symlink following")
    blocks = getattr(info, "st_blocks", None)
    if blocks is None:
        raise ValueError("st_blocks is unavailable on this platform")
    allocated = blocks * 512
    return {
        "path": path,
        "logical_bytes": info.st_size,
        "allocated_bytes": allocated,
        "allocated_less_than_logical": allocated < info.st_size,
        "hard_links": info.st_nlink,
        "device": info.st_dev,
        "inode": info.st_ino,
    }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("paths", nargs="+", help="regular-file paths; no recursion")
    args = parser.parse_args()
    try:
        rows = [inspect_file(path) for path in args.paths]
    except (OSError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2
    print(json.dumps(rows, indent=2, ensure_ascii=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

os.lstat()读取最后一个路径项本身;stat.S_ISREG()限制输入为普通文件,所以目录、FIFO 和最后一级符号链接会被拒绝。父目录中的链接仍会被正常解析;这不是对不可信目录的路径隔离工具。

成功返回 0,stdout 是一个 JSON 数组,包含 logical_bytes、allocated_bytes、allocated_less_than_logical、hard_links、device 与 inode。任一路径失败返回 2,说明写到 stderr,不输出部分 JSON;参数错误同样返回 2。脚本不递归,且不会把多个文件当作同一个时刻的快照。

4. 运行:对照 JSON 与 du

依次执行:

python3 file_usage.py --help
python3 file_usage.py sparse.bin empty.bin
ls -l -- sparse.bin
du -B1 -- sparse.bin
du --apparent-size -B1 -- sparse.bin
du -b -- sparse.bin

本次测试中,sparse.bin 的逻辑长度为 8388608 字节,分配量为 8192 字节,布尔字段为 true。du -B1 显示 8192,另外两条 du 命令显示 8388608。这些是测试环境观察值,不能把分配量写死为通用断言;空文件的布尔字段为 false,没有除以零的问题。

想把同样的检查用于实际文件时,传入确定属于你的普通文件路径即可;路径以 - 开头时在参数前加 --。要先寻找候选大文件,可用按逻辑大小排名的工具,再逐个检查分配量,而不是直接相加排名结果。

5. 验证:覆盖硬链接和失败输入

把下面保存为 check_usage.py,与 file_usage.py 放在一起。测试只在当前目录下创建自己的临时子目录,结束时自动清理;不会改动前面的演示文件。

"""Run next to file_usage.py; all fixtures live in a temporary directory."""
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile

script = Path("file_usage.py").resolve()


def run(paths, expected):
    result = subprocess.run(
        [sys.executable, str(script), *map(str, paths)],
        text=True, capture_output=True, check=False,
    )
    assert result.returncode == expected, result.stderr
    if expected == 0:
        assert result.stderr == "", result.stderr
        return json.loads(result.stdout)
    assert result.stdout == "", result.stdout
    assert result.stderr.startswith("ERROR:"), result.stderr


with tempfile.TemporaryDirectory(prefix="usage-check-", dir=".") as tmp:
    directory = Path(tmp)
    sparse = directory / "sparse.bin"
    size = 8 * 1024 * 1024
    with sparse.open("xb") as stream:
        stream.write(b"HEAD")
        stream.seek(size - 4)
        stream.write(b"TAIL")
        stream.flush()
        os.fsync(stream.fileno())
    empty = directory / "empty.bin"
    empty.touch(exist_ok=False)
    tiny = directory / "tiny.bin"
    tiny.write_bytes(b"x")
    before = sparse.stat()
    rows = run([sparse, empty, tiny], 0)
    assert [row["logical_bytes"] for row in rows] == [size, 0, 1]
    for path, row in zip([sparse, empty, tiny], rows):
        info = path.lstat()
        assert row["allocated_bytes"] == info.st_blocks * 512
        assert row["allocated_less_than_logical"] == (
            row["allocated_bytes"] < row["logical_bytes"]
        )
    assert sparse.stat().st_mtime_ns == before.st_mtime_ns
    with sparse.open("rb") as stream:
        assert stream.read(4) == b"HEAD"
        stream.seek(size // 2)
        assert stream.read(4) == b"\0" * 4
        stream.seek(size - 4)
        assert stream.read() == b"TAIL"
    alias = directory / "alias.bin"
    os.link(sparse, alias)
    linked = run([sparse, alias], 0)
    assert linked[0]["hard_links"] == linked[1]["hard_links"] == 2
    assert (linked[0]["device"], linked[0]["inode"]) == (
        linked[1]["device"], linked[1]["inode"]
    )
    symlink = directory / "link.bin"
    symlink.symlink_to("sparse.bin")
    run([symlink], 2)
    run([directory], 2)
    run([directory / "missing.bin"], 2)
    run([sparse, directory / "missing.bin"], 2)
print("PASS: sizes, blocks, content, hard links, and failure exits")

运行:

python3 check_usage.py

成功时输出:

PASS: sizes, blocks, content, hard links, and failure exits

这里断言的是计量公式、内容与退出行为,不是某种文件系统必须节省多少空间。测试还核对最后一级符号链接、目录、缺失文件和混合输入失败时没有部分 stdout。硬链接的相同身份只说明别名关系;不要把它当成两份独立备份。需要查找独立内容副本时,参考分块 SHA-256 重复文件检查。

6. 常见问题与边界

现象 原因与具体处理
allocated_bytes 大于 logical_bytes 小文件也可能按整块分配;先比较字节单位,不要据此判定损坏。
演示文件没有明显节省 检查当前挂载类型和文件系统能力,在支持稀疏文件的练习目录重试;不要调整脚本让它虚报节省。
not a regular file 输入是目录、FIFO 或最后一级符号链接;明确选择真正的普通文件路径。
No such file / Permission denied 核对路径和父目录的搜索权限;不要把失败算成零占用,也不要默认以 root 重跑。
多个硬链接都报告同一分配量 用设备号与 inode 识别同一个文件;如需汇总,先按身份去重,且仍不代表删除后释放量。
复制后目标占用变大 复制工具、文件系统与空洞保留策略可能不同;先在小样本上复制并复查两种大小,再做容量预算。

元数据查询成功不代表内容可读、备份完整或文件此后不再变化。不要在活动数据库上用一次 stat 结果估算精确备份空间;本工具也不统计目录自身、快照或文件系统全局元数据。

7. 小结

先区分逻辑长度与文件报告的分配量,再解释 ls 和 du 的差异。用小型稀疏文件与硬链接测试验证公式和退出码,把只读 JSON 报告接入巡检。把结果当成进一步排查的线索,而不是删除建议或可回收空间承诺。

标签

1024 12306 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv venv vhhbert vite web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它