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 个文件描述符:0、1、2 是进程启动时默认打开的标准输入、标准输出、标准错误,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 | 名称 | 含义 | 示例指向 |
|---|---|---|---|
| 0 | stdin | 标准输入 | /dev/pts/0 |
| 1 | stdout | 标准输出 | /dev/pts/0 |
| 2 | stderr | 标准错误 | /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。
文件描述符和文件句柄的关系可以简化为:
文件描述符只是进程看到的整数编号;内核打开文件对象才保存真正的打开状态,例如:
- 当前读写偏移量
- 打开模式:只读、只写、读写
- 文件状态标志:
O_APPEND、O_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() 取出。
关系如下:
10. 总结
文件路径用于打开文件,文件描述符用于在打开后操作文件,文件句柄则是内核中保存打开状态的对象。
核心理解:文件路径是用来打开文件的;文件描述符是打开之后用来操作文件的;文件句柄是内核中保存打开状态的对象。