Python fdopen 的关闭责任:文件对象已经关闭,为什么原描述符还能继续写

前天 3阅读

一个辅助函数接收已经打开的文件描述符,为了方便写入,把它包装成文件对象。离开with以后,调用者有时还能写,有时却收到Bad file descriptor。关键不在with是否执行,而在包装对象是否同时拥有关闭底层描述符的责任。

本文在Linux、CPython 3.12.14实跑。保存为demo.py,运行python3 demo.py。程序只在自动清理的私有临时目录创建一个小文件,不接触现有业务文件。第一轮用closefd=False借用描述符;第二轮省略参数,观察默认所有权;最后检查文件名入口。

Python fdopen 的关闭责任:文件对象已经关闭,为什么原描述符还能继续写

AI模型生成概念插图:一侧外层套筒离开后通道仍开着,另一侧套筒与通道一起封闭,用来说明关闭责任;不是系统句柄截图。

完整程序与本地结果

完整可运行程序

import errno
import os
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory(prefix="fd-owner-") as directory:
    path = Path(directory) / "sample.bin"
    fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600)
    try:
        with os.fdopen(fd, "wb", closefd=False) as borrowed:
            borrowed.write(b"A")
        assert borrowed.closed
        assert os.write(fd, b"B") == 1
        assert path.read_bytes() == b"AB"
        print("borrowed wrapper closed:", borrowed.closed)
        print("original descriptor still writes:", path.read_bytes())
    finally:
        os.close(fd)

    owned_fd = os.open(path, os.O_RDONLY)
    with os.fdopen(owned_fd, "rb") as owned:
        assert owned.read() == b"AB"
    try:
        os.fstat(owned_fd)
    except OSError as exc:
        assert exc.errno == errno.EBADF
        print("default ownership:", "EBADF")
    else:
        os.close(owned_fd)
        raise AssertionError("owned descriptor should be closed")

    try:
        open(path, "rb", closefd=False)
    except ValueError:
        print("filename with closefd=False:", "ValueError")
    else:
        raise AssertionError("closefd=False requires a descriptor")

本次实际输出(以下为结果,不是程序)

borrowed wrapper closed: True
original descriptor still writes: b'AB'
default ownership: EBADF
filename with closefd=False: ValueError

关闭包装对象,不一定关闭底层通道

borrowed.closed显示True,但os.write仍能写入B,文件最后是b'AB'。closefd=False明确让底层描述符在文件对象关闭后继续有效。with已经正常结束,关闭动作也发生了,只是它没有接管调用方持有的描述符。

文件对象关闭时会处理自己的缓冲,因此A先被写出,随后底层写入B。这个顺序不能反推任意混用都安全:如果包装对象还开着且有未刷新的缓冲,直接对描述符读写可能让位置与内容变得难以预测。示例刻意先结束包装层,再使用原描述符。

默认参数会把关闭责任交给包装层

第二轮os.fdopen没有指定closefd,默认值为True。退出with后,os.fstat立即得到EBADF,说明owned_fd对应的描述符已经关闭。调用方此后不应再盲目调用os.close尝试重复清理;资源所有权应在包装前就约定清楚。

代码在检测关闭的期间没有另外打开文件,避免旧整数恰好被系统分配给新描述符而混淆结果。描述符只是进程里的整数编号,不是永远指向同一文件的身份标识。真实程序也不应保存已关闭编号,之后再根据数值相同推断资源仍然原样存在。

文件名入口不能借用一个尚不存在的描述符

最后open接收的是路径,却同时指定closefd=False,因此抛ValueError。这个选项的借用语义要求传入已有描述符;通过路径打开的新描述符需要由创建出来的文件对象负责关闭,不能用同一参数让它悄悄失去清理责任。

设计函数时可以明确区分“接收并借用现有描述符”和“接收路径并创建资源”两个入口。前者在文档中写清调用者负责最终关闭,后者用上下文管理器收尾。关闭状态、内容正确与数据持久化是不同问题;本例验证前两项,不把成功关闭当作断电后一定保存的证明。

参考资料

资料核验日期:2026年10月2日。以上输出对应文中固定输入和明确运行版本,本地执行退出码为0。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。