文件描述符是一个进程内部用于引用已打开资源的小整数。进程的描述符表把这个数字映射到内核维护的打开文件描述;对于普通文件,打开描述包含当前偏移和文件状态标志。另一个进程可以把相同整数用于完全不同的资源,因此数字本身不是全局文件身份。理解描述符表、打开描述与底层文件之间的关系,才能正确判断复制、关闭和继承的效果。
坏掉的样子 dup 分配另一个描述符,让它引用同一份打开文件描述;它不是简单复制整数,也不是重新按路径打开文件。因此,两份描述符读取普通文件时共享偏移。相反,再次打开相同路径,通常会得到具有独立偏移的新打开描述。设备与 inode 身份相同,不代表打开状态也相同,排查读取位置异常时必须区分这两个层面。
状态标志和描述符标志也属于不同层次。通过 F_SETFL 改变共享打开描述的状态,复制出来的引用都会看到;close-on-exec 标志则分别属于每个描述符。把这些层次混在一起,可能改变另一个消费者的行为,也可能让描述符意外进入后续执行的程序。选择复制引用之前,需要知道自己想独立控制的是寿命、偏移,还是继承行为。
关闭描述符会释放表项,整数可能立即被再次使用。只保留旧数字的代码,之后可能操作新资源,而不是稳定得到“无效描述符”错误。当描述符在回调、线程或库之间传递时,需要明确所有权和寿命,不能仅检查一个整数非负,就认定它仍然代表原来那个对象。数字看起来没有变化,引用关系却可能已经完全不同。
快速验证 在一次性 Linux 环境中运行下面的 Python 脚本。它创建两份无害临时文件,并关闭自己打开的所有描述符。实测环境为 Linux 6.12.76、Python 3.11.2。实验没有后台线程,因此可以观察分配与复用,不会有另一个线程同时抢占刚释放的编号:
from pathlib import Path from tempfile import TemporaryDirectory import fcntl,os with TemporaryDirectory(prefix='xbcatm-fds-') as directory: root=Path(directory);original=root/'original';other=root/'other' original.write_bytes(b'abcdef');other.write_bytes(b'replacement') handles=set() try: first=os.open(original,os.O_RDONLY);handles.add(first) duplicate=os.dup(first);handles.add(duplicate) independent=os.open(original,os.O_RDONLY);handles.add(independent) assert first!=duplicate assert os.read(first,2)==b'ab' and os.read(duplicate,2)==b'cd' assert os.read(independent,2)==b'ab' assert os.lseek(first,0,os.SEEK_CUR)==4 print('dup uses a different descriptor but shares offset; a separate open has its own offset') flags=fcntl.fcntl(duplicate,fcntl.F_GETFL) fcntl.fcntl(duplicate,fcntl.F_SETFL,flags|os.O_NONBLOCK) assert fcntl.fcntl(first,fcntl.F_GETFL)&os.O_NONBLOCK assert not fcntl.fcntl(independent,fcntl.F_GETFL)&os.O_NONBLOCK os.set_inheritable(first,False);os.set_inheritable(duplicate,True) assert not os.get_inheritable(first) and os.get_inheritable(duplicate) print('file status flags shared by dup; close-on-exec flags remain descriptor-specific') os.close(first);handles.remove(first) reused=os.open(other,os.O_RDONLY);handles.add(reused) assert reused==first and os.read(first,32)==b'replacement' assert os.read(duplicate,2)==b'ef' print('closed descriptor number is reused for another file; surviving duplicate still reads original') original.unlink();os.lseek(duplicate,0,os.SEEK_SET) assert os.fstat(duplicate).st_nlink==0 and os.read(duplicate,32)==b'abcdef' print('unlink removes the pathname while an open descriptor retains access') finally: for handle in handles:os.close(handle) 第一次打开读取 ab,复制描述符接着读取 cd,两者此时都看到偏移四。对同一个文件独立执行 open,则仍从自己的起始位置读到 ab。这些精确字节证明哪份状态被共享;仅仅看到两个入口都能读取相同内容,并不足以区分复制引用和独立打开,因为它们本来就可以指向同一个底层文件。
...