星闌

返回

程序读取文件,通常先调用open,再进行read或write,最后调用close。这个过程看起来只是打开、读写、关闭,但系统还要记录这次打开允许什么操作、已经读到了哪里,以及哪些访问正在共用同一份状态。

以下以Linux中的普通文件为例,拆开看这几层关系。

文件描述符与读写位置

1. open返回的编号代表什么

假设程序要读取notes.txt。调用open时,系统根据路径找到目标文件,检查相关权限,并建立这次打开所需的状态。成功后,程序拿到一个非负整数,叫文件描述符,通常简称fd。

fd是当前进程文件描述符表中的索引。后续调用read时,程序把编号交给系统,系统就能找到已经建立的打开状态,不需要再次传入完整路径。

fd不是文件在整台计算机上的固定编号。两个进程都拿到数字3,并不代表它们打开了同一个文件;同一个进程多次打开同一文件,也可以拿到不同的fd。

fd后面还有系统维护的“打开文件描述”(open file description),其中保存文件偏移量、文件状态标志等信息。可以把它理解为一次打开操作对应的状态记录,fd则是程序引用这份记录的入口。

2. 连续读取时,为什么能接着上次的位置

假设一个普通文件的内容是ABCDEFGH,每个字母占1字节,没有换行符。

刚打开时,偏移量为0。第一次读取3字节,如果实际读到了ABC,偏移量就前进到3。接着再读取2字节,如果实际读到了DE,偏移量就变成5。下一次读取会从F开始。

偏移量表示文件内部的字节位置,不是磁盘上的物理块地址。程序关心从文件的第几个字节读起,至于这个位置对应哪个数据块,由文件系统完成映射。

位置前进多少,要看实际读到了多少字节。请求读取100字节,不代表每次都能得到100字节。比如文件只剩20字节,本次读取就可能只返回20,程序需要根据返回值处理。

如果想重新从开头读取,对支持定位的普通文件,可以用lseek把偏移量调回0,再继续读取。lseek改变的是读写位置,本身不读取数据,也不修改已有文件内容。管道和套接字不能像普通文件一样随意定位。

3. 同一个文件,读写位置是否一定相同

不一定,关键要看这些fd引用的是不是同一份打开文件描述。

分别调用两次open,打开同一个文件时,两次打开各自建立状态,因此各有自己的偏移量。

仍以ABCDEFGH为例。通过第一个fd读走ABC后,第二个fd的偏移量仍然是0。如果此时通过第二个fd读取3字节,它仍然可以读到ABC。底层是同一个文件,但两次打开的读取进度互不影响。

如果先打开一次,再用dup复制已有的文件描述符,情况就不同了。dup返回另一个fd,但两个编号引用同一份打开文件描述,因此共享偏移量。通过第一个fd读走ABC后,如果没有其他操作改变位置,再通过第二个fd读取2字节,读到的就是DE。

所以,单看有几个文件描述符还不够。两个不同编号可能各有自己的读取进度,也可能共同使用同一个进度。判断时要看它们是分别打开得到的,还是从已有描述符复制出来的。

4. close结束的是哪一层关系

调用close,关闭的是指定的文件描述符。这个编号随后可以被系统重新分配,所以关闭后不能继续把它当成原来的文件入口。

如果两个fd通过dup共享打开状态,关闭其中一个,另一个仍然可以继续使用。只有相应引用和使用都结束后,底层打开状态占用的资源才能释放。课本里“关闭文件、释放打开文件表项”的过程,在存在共享时还需要考虑引用关系。

对于写入操作,还要区分“关闭了描述符”和“数据已经持久保存”。Linux会利用缓存延后写入,close成功并不保证所有修改都已经落到存储设备上。

需要明确同步文件数据时,可以使用fsync,并检查它的返回结果。如果保存过程还涉及创建文件或重命名,目录项的持久化也需要另外考虑。比如编辑器保存文章时,写完正文、更新文件名和关闭文件,虽然在界面上可能只是一次“保存”,底层却是几件需要分别处理的事。

参考:open ↗、read ↗、dup ↗、lseek ↗、close ↗、fsync ↗。