文件描述符与文件句柄

1. 示例现象

下面的 Python 程序通过 os.open() 打开 test.txt,写入内容后保持进程运行 60 秒,方便从 /proc 观察这个进程持有哪些文件描述符。

python#!/usr/bin/env python3
import os
import time

# 打开一个文件,获取文件描述符
fd = os.open('test.txt', os.O_RDWR | os.O_CREAT, 0o644)
print(f"文件描述符: {fd}")

# 写入一些内容
os.write(fd, b'Hello Rocky Linux!\n')

# 获取当前进程ID
pid = os.getpid()
print(f"当前进程PID: {pid}")
print(f"查看 /proc/{pid}/fd/ 目录来确认文件描述符")

# 保持进程运行以便观察
print("进程将在60秒后退出,现在可以查看/proc目录...")
time.sleep(60)

os.close(fd)

运行后输出:

text文件描述符: 3
当前进程PID: 517211
查看 /proc/517211/fd/ 目录来确认文件描述符
进程将在60秒后退出,现在可以查看/proc目录...

在另一个终端查看 /proc/517211/fd/

text0 -> /dev/pts/0
1 -> /dev/pts/0
2 -> /dev/pts/0
3 -> /root/python_pj/test.txt

这说明当前进程已经打开了 4 个文件描述符:012 是进程启动时默认打开的标准输入、标准输出、标准错误,3 是程序通过 os.open() 打开的 test.txt

2. 文件描述符是什么

文件描述符(File Descriptor,FD)是 Linux 进程内部用来引用“已打开文件”的整数编号。它不是文件路径,也不是文件本身,而是当前进程文件描述符表里的一个索引。

可以把当前进程的文件描述符表理解成:

text进程 517211 的文件描述符表

0 -> /dev/pts/0                # stdin
1 -> /dev/pts/0                # stdout
2 -> /dev/pts/0                # stderr
3 -> /root/python_pj/test.txt  # os.open() 打开的文件

当程序执行:

pythonos.write(3, b'Hello Rocky Linux!\n')

内核会先在当前进程的文件描述符表里查找 3,然后找到它对应的已打开文件对象,最后把数据写入这个对象背后的文件。

3. 为什么第一个自定义文件描述符通常是 3

Linux 进程启动时通常默认占用 3 个文件描述符:

FD名称含义示例指向
0stdin标准输入/dev/pts/0
1stdout标准输出/dev/pts/0
2stderr标准错误/dev/pts/0

所以程序自己打开的第一个文件,通常会拿到最小可用编号 3。如果关闭 3 后再打开新文件,内核也可能复用这个编号。

4. /proc/PID/fd 看到的是什么

/proc/<PID>/fd/ 是 Linux 暴露给用户态的进程文件描述符视图。目录里的每个数字都是该进程的一个文件描述符,并以符号链接形式指向它当前关联的对象。

bashls -l /proc/517211/fd/

看到:

text3 -> /root/python_pj/test.txt

表示的是:

textPID 517211 这个进程的 3 号文件描述符,当前关联到 /root/python_pj/test.txt。

💡 提示:文件描述符不只会指向普通文件,也可以指向目录、终端、管道、socket、设备文件、eventfd、timerfd、inotify fd、epoll fd 等对象。比如 Web 服务中经常能在 /proc/<PID>/fd/ 里看到 socket:[inode]

5. 文件句柄是什么

“文件句柄”是一个更通用的说法,在不同系统和语言里含义不完全一样。在 Linux 语境下,可以把它理解为内核中表示“已打开文件”的对象,也就是内核的 open file object。

文件描述符和文件句柄的关系可以简化为:

用户进程 fd = 3 | v 进程文件描述符表 3 -> 内核打开文件对象(文件句柄 / open file object) | v inode / 文件系统对象

文件描述符只是进程看到的整数编号;内核打开文件对象才保存真正的打开状态,例如:

  • 当前读写偏移量
  • 打开模式:只读、只写、读写
  • 文件状态标志:O_APPENDO_NONBLOCK
  • 引用计数
  • 指向 inode 或其他内核对象的指针

6. 路径、文件描述符、文件句柄、inode 的区别

概念视角表现形式作用
文件路径文件系统视角/root/python_pj/test.txt用来定位并打开文件
文件描述符进程视角整数,例如 3进程用它告诉内核要操作哪个已打开对象
文件句柄 / 打开文件对象内核视角内核数据结构保存打开模式、偏移量、引用计数等状态
inode文件系统底层inode 编号表示磁盘上的真实文件对象

一个常见误区是把路径等同于文件本身。实际上路径只是找到文件的名字;文件打开以后,进程主要通过文件描述符操作内核中的打开文件对象。

7. 文件描述符是进程级别的

文件描述符编号不是系统全局唯一的,而是每个进程各自维护一套。

text进程 A:
3 -> /tmp/a.log

进程 B:
3 -> /var/log/messages

同样是 3,在不同进程里可能指向完全不同的对象。因此描述一个文件描述符时,必须同时包含 PID 和 FD,例如 /proc/517211/fd/3

8. 文件删除后 FD 为什么还可能有效

如果进程已经打开了文件:

text3 -> /root/python_pj/test.txt

另一个终端执行:

bashrm /root/python_pj/test.txt

只要原进程还没有关闭 fd,文件内容仍然可能存在,进程也仍然可以继续读写。此时 /proc/<PID>/fd/ 里可能看到:

text3 -> /root/python_pj/test.txt (deleted)

原因是路径被删除了,但 inode 仍然被内核打开文件对象引用。只有当最后一个引用它的文件描述符关闭后,内核才会真正释放相关文件数据。磁盘空间“删除后不释放”的问题,经常就是进程仍持有 deleted 文件的 fd。

排查命令:

bashlsof | grep deleted

9. os.open() 与 Python open() 的区别

os.open() 更接近 Linux 系统调用,返回的是底层文件描述符整数:

pythonfd = os.open('test.txt', os.O_RDWR | os.O_CREAT, 0o644)
print(fd)  # 例如 3

普通 open() 返回的是 Python 文件对象:

pythonf = open('test.txt', 'w')
print(f.fileno())

Python 文件对象内部仍然包装了一个底层 fd,可以通过 fileno() 取出。

关系如下:

Python file object | v 文件描述符 fd | v 内核打开文件对象 | v inode / 文件

10. 总结

文件路径用于打开文件,文件描述符用于在打开后操作文件,文件句柄则是内核中保存打开状态的对象。

Python 程序调用 os.open() ↓ 内核打开 /root/python_pj/test.txt ↓ 给当前进程分配 fd 3 ↓ /proc/517211/fd/3 显示这个 fd 指向 test.txt ↓ os.write(3, ...) 通过 fd 3 写入文件 ↓ os.close(3) 关闭这个文件描述符

核心理解:文件路径是用来打开文件的;文件描述符是打开之后用来操作文件的;文件句柄是内核中保存打开状态的对象。

目录