春江暮客

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

Python 报 Too many open files:在 Linux 定位文件句柄泄漏

2026-10-15 技术
Python 报 Too many open files:在 Linux 定位文件句柄泄漏

批处理脚本跑了一阵,突然报 OSError: [Errno 24] Too many open files。磁盘还有空间,输入文件也不大,却再也打不开下一份文件。问题往往出在进程同时持有的文件描述符数量,而不是输入文件的总数。

本文面向初学者到中级读者,在 Linux 上复现一个文件句柄泄漏,再从进程外查看实际限制和打开的文件,最后用 with 修复。演示只读取一份测试文件;降低软上限的操作只作用于演示 Python 进程。

1. 方法:检查发生错误的进程

Python 的 resource 官方文档说明,RLIMIT_NOFILE 控制当前进程的文件描述符上限,getrlimit() 返回软上限和硬上限。软上限是当前执行的限制,不能超过硬上限。

文件描述符不只用于普通文件,也用于 socket 和管道。根据 Python errno 文档,EMFILE 表示打开文件过多;ENFILE 是系统文件表耗尽,排查范围不同。

先区分两种情况:

观察结果 下一步
同样的工作反复执行后,描述符数量持续增加 查找漏掉的关闭操作,以及仍保存着文件对象的列表或缓存。
数量随并发增加,任务结束后回落 限制并发,估算每个任务需要的描述符,再判断当前上限是否够用。

一次高计数不能证明泄漏。 在相同负载下重复采样,结合描述符指向的文件和代码生命周期判断。

2. 准备环境和测试文件

需要 Linux、可访问的 /proc 和已有的 Python 3.9 或更新版本。无需安装第三方依赖;本文在 Linux、Python 3.11.4 上测试。resource 模块不适用于 Windows。

  1. 在 bash 中创建独立演示目录。重复实验时换一个目录名:
python3 --version
python3 -c 'import resource; print("resource ready")'
mkdir fd-demo
cd fd-demo
printf 'alpha\nbeta\n' > sample.txt
  1. 把下一节代码保存为当前目录的 fd_demo.py,也可以下载完整演示脚本。
  2. 演示会把自身软上限降至 64;原有上限更小时沿用较小值,低于 16 时拒绝运行。硬上限不变,启动它的 shell 和其他进程也不变。这是复现故障的测试设置,不要复制到实际任务入口。

3. 完整实现:泄漏与修复共用一份脚本

"""Reproduce a file descriptor leak, then compare scoped file reads on Linux."""
import argparse
import os
from pathlib import Path
import resource
import sys
import time


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("file", type=Path, help="existing input file, read only")
    parser.add_argument("--mode", choices=("leak", "fixed"), default="fixed")
    parser.add_argument("--iterations", type=int, default=200)
    parser.add_argument("--hold", type=int, default=0, help="inspection pause, 0-60 seconds")
    args = parser.parse_args()
    if not 1 <= args.iterations <= 10000:
        parser.error("iterations must be from 1 to 10000")
    if not 0 <= args.hold <= 60:
        parser.error("hold must be from 0 to 60")

    soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
    finite = [64] + [n for n in (soft, hard) if n != resource.RLIM_INFINITY]
    limit = min(finite)
    if limit < 16:
        parser.error("demo needs an existing descriptor limit of at least 16")
    try:
        resource.setrlimit(resource.RLIMIT_NOFILE, (limit, hard))
    except (OSError, ValueError) as exc:
        print(f"ERROR: cannot set demo limit: {exc}", file=sys.stderr, flush=True)
        return 1
    print(f"pid={os.getpid()} mode={args.mode} soft={limit} hard={hard}", flush=True)

    retained = []
    processed = status = 0
    try:
        try:
            for _ in range(args.iterations):
                if args.mode == "leak":
                    file = args.file.open("rb")
                    retained.append(file)
                    file.read(1)
                else:
                    with args.file.open("rb") as file:
                        file.read(1)
                processed += 1
        except OSError as exc:
            print(f"ERROR: errno={exc.errno}: {exc}", file=sys.stderr, flush=True)
            status = 1
        print(f"processed={processed} retained={len(retained)} status={status}", flush=True)
        if args.hold:
            print(f"READY: holding for {args.hold}s", flush=True)
            time.sleep(args.hold)
    finally:
        for file in retained:
            file.close()
    return status


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

leak 分支持续把打开的文件对象保留在列表中,即使已经读取完毕,也没有关闭。fixed 分支每次都在 with 内读取,离开代码块时关闭文件。两者重复打开同一份文件,因此「目录里只有一个文件」也能触发上限。

--hold 让进程在处理完或报错后暂留,方便另一个进程检查。泄漏分支会在暂停结束后的 finally 中关闭文件,但工作循环已经失败,退出码仍是 1;这种收尾不能替代循环内及时释放资源。

4. 方法 1:复现错误并查看打开的文件

  1. 查看帮助,然后运行泄漏模式。以下命令在同一个 bash 终端中执行,pid=$! 记录刚启动的后台任务:
python3 fd_demo.py --help
python3 fd_demo.py sample.txt --mode leak --hold 30 &
pid=$!
sleep 1
cat "/proc/$pid/limits"
ls -l "/proc/$pid/fd"
python3 - "$pid" <<'PY_FD'
from pathlib import Path
import sys
folder = Path("/proc", sys.argv[1], "fd")
print("fd_count=", sum(1 for _ in folder.iterdir()), sep="")
PY_FD
if wait "$pid"; then
  printf 'exit=0\n'
else
  printf 'exit=%s\n' "$?"
fi
  1. 重点看 Max open files 行的软上限,以及 /proc/$pid/fd 中反复指向 sample.txt 的条目。只有标准输入、输出、错误三个初始描述符时,典型结果为:
ERROR: errno=24: [Errno 24] Too many open files: 'sample.txt'
processed=61 retained=61 status=1
READY: holding for 30s
fd_count=64
exit=1

实际 PID、硬上限和成功次数会随启动环境变化。启动器额外继承的描述符也会占用额度;不要把 61 写成业务断言。

Linux 内核的 /proc 文档说明,进程目录按 PID 命名,fd 包含文件描述符;/proc/PID/limits 手册说明该文件列出软、硬资源上限。ls -l 能查看这些描述符的链接目标。

这里从进程外检查,是因为已经耗尽描述符的进程可能连自己的 /proc/self/fd 目录都打不开。采样不是原子快照;在实际程序中,文件可能在查看期间关闭。

5. 方法 2:及时关闭,然后验证修复

  1. 在相同演示上限下运行修复模式:
python3 fd_demo.py sample.txt --mode fixed
printf 'exit=%s\n' "$?"
python3 fd_demo.py sample.txt --mode fixed --iterations 1000

第一次应输出 processed=200 retained=0 status=0 和 exit=0;第二次应输出 processed=1000 retained=0 status=0。成功次数已经超过软上限,因为处理完一轮就释放了描述符。

  1. 验证异常路径也会关闭文件:
python3 - <<'PY_CLOSE'
file = None
try:
    with open("sample.txt", "rb") as file:
        file.read(1)
        raise RuntimeError("demo processing error")
except RuntimeError:
    pass
assert file is not None and file.closed
print("OK: closed after exception")
PY_CLOSE

Python 文件读写教程说明,with 在代码块结束时关闭文件,即使处理过程中抛出异常。实际批处理也应把 with 放在每个文件的处理范围内,而不是先打开所有文件再统一处理。

读取 CSV 时可套用大型 CSV 流式处理中的单文件读取结构。运行外部命令时,还要管理管道、日志句柄和子进程结束,参考Python subprocess 超时与日志处理。

6. 排障:限制、权限和自动化环境

现象 具体操作
/proc/$pid 不存在 检查进程是否已经退出;重跑演示并使用 --hold 60,在暂停期间完成检查。
查看 fd 时提示权限不足 用任务所属用户检查,并确认容器 PID 命名空间和 /proc 访问策略。
演示显示 errno=2 确认 sample.txt 存在,或传入它的绝对路径;这是缺失文件,不是描述符耗尽。
修复文件读取后仍增长 按链接目标检查 socket、管道、日志处理器和重复创建的连接;为相应对象安排关闭流程。
终端成功,定时任务失败 查看定时任务实际 PID 的 limits;交互 shell 的限制不一定相同。
报的是 ENFILE 查主机层面的文件表和其他进程;仅检查一个 Python 进程不足以定位。

检查当前 bash 的软、硬上限,可以运行以下只读命令:

ulimit -Sn
ulimit -Hn

这两行显示的是当前 shell 的限制,不能代表另一份已经运行的服务。生产排障应以 /proc/实际PID/limits 为准;定时任务的运行路径和工作目录也可按Linux 定时任务环境排查逐项核对。

如果描述符数量在合理并发下稳定回落,但峰值确实高于现有限制,再按启动器的配置方式调整上限,并重新查看运行进程验证。持续增长的泄漏只会在更高上限下更晚失败。修复后重复运行同一批工作,确认句柄数量回到稳定范围;retained=0 只说明演示列表为空,不代表整个应用没有其他句柄。

7. 小结

先查看发生错误的进程上限和描述符目标,再判断是泄漏还是正常并发需求。把文件处理放进 with,同时验证正常和异常路径都能释放资源。最后在实际启动环境下重复采样,确认修复后数量稳定,再考虑是否需要调整容量。

标签

1024 12306 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 copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization debugging deployment desktop-app devtools disk-space disown docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo indexnow 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 mirror mmcif model-evaluation mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pdb pep-723 phaser pillow pip postfix preprocessing print 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 signals sklearn solana somaticsignatures spl sqlite ssh standardize static-site subprocess sysadmin system-one tensorflow tkinter toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它