如何判断一个文件描述符是否已关闭(不抛异常)

最轻量无副作用的文件描述符有效性检查是调用fcntl(fd, F_GETFD):成功返回标志值(通常0),失败返回-1且errno为EBADF;其他方法如read/write、dup、poll/select均有副作用或不可靠性。

fcntl(fd, F_GETFD) 检查文件描述符有效性

Linux/macOS 下最轻量、不触发副作用的判断方式是调用 fcntl(fd, F_GETFD)。它只读取文件描述符标志,不会修改状态,也不引发 I/O 或信号。如果 fd 无效(已关闭或根本不存在),系统调用返回 -1 并置 errnoEBADF;否则返回当前文件描述符标志值

(通常为 0)。

注意:不能用 read(fd, &buf, 1)write(fd, &buf, 1) 判断——即使 fd 已关,某些场景下(如管道写端已关但读端仍开)可能返回 0 或阻塞,且会改变文件偏移、触发 SIGPIPE 等副作用。

  • fcntl 是原子操作,线程安全,适合多线程环境快速探活
  • Windows 不支持该用法,需改用 GetFileType() + GetLastError() == ERROR_BAD_UNIT
  • 不要依赖 dup(fd) 是否成功:它在 fd 有效时返回新 fd,但失败时也可能因资源耗尽而返回 -1,无法区分原因

为什么 poll() / select() 不可靠

poll() 对已关闭的 fd 可能返回 POLLNVAL,但行为不一致:对 socket fd,若对端已关闭连接但本端未 close(),它仍可能返回 POLLIN;对普通文件 fd,poll() 总是立即返回 POLLNVAL,但 POSIX 并未保证这点,glibc 实现也依赖内核版本。

  • poll() 需要构造 struct pollfd,开销比 fcntl 大,且引入超时逻辑干扰判断意图
  • select() 更糟:它会修改传入的 fd_set,且对非法 fd 的行为未明确定义,部分实现直接 abort
  • 二者都要求 fd 在进程 fd 表中“存在”,但已关闭的 fd 条目会被内核立即回收,所以实际测试中常返回 EBADF ——这又绕回了需要先做一次系统调用验证,不如直接用 fcntl

避免 race condition:检查后立即使用的陷阱

即使 fcntl(fd, F_GETFD) 返回成功,也不能保证下一刻 fd 仍有效——其他线程或信号处理函数可能在检查后、使用前调用 close(fd)。这不是检测方法的问题,而是 Unix fd 模型本身的限制。

  • 真正安全的做法是:在关键路径上直接使用 fd,并妥善处理 EBADF(以及 EIOEAGAIN 等常见错误),而不是预先检查
  • 若必须预检(如日志记录、资源清理决策),应配合同步机制(如互斥锁)保护 fd 生命周期
  • 某些库(如 libuv)内部就放弃“预检”,改为统一错误处理 + fallback 逻辑

Python 中的等效做法:别用 os.fstat()os.dup()

Python 用户常误用 os.fstat(fd) 判断——它底层调用 fstat(),对已关闭 fd 抛 OSError: [Errno 9] Bad file descriptor,无法“不抛异常”。同理,os.dup(fd) 也会抛异常。

正确方式是用 ctypes 调用 fcntl,或更简单:捕获异常并忽略:

import errno
try:
    os.fstat(fd)
    is_valid = True
except OSError as e:
    is_valid = (e.errno == errno.EBADF)

但要注意:这仍是“事后捕获”,不是无异常检测;若想严格符合“不抛异常”要求,只能走 ctypes + fcntl 路径。

真正难的不是怎么查,而是查完之后信不信这个结果——fd 的有效性本质上是瞬态的,靠单次检查无法建立可靠契约。