3.10 文件共享
UNIX系统支持在不同进程间共享打开文件。在介绍dup函数之前,先要说明这种共享。为此先介绍内核用于所有I/O的数据结构。
内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
a、文件描述符标志(close_on_exec);
b、指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表。每个文件表项包含:
a、文件状态标志(读、写、添写、同步和非阻塞等,关于这些标志的更多信息参见3.14节);
b、当前文件偏移量;
c、指向该文件v节点表项的指针。
(3)每个打开文件(或设备)都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
我们忽略了那些不影响讨论的实现细节。例如,打开文件描述符可存放在用户空间(作为一个独立的对应于每个进程的结构,可以换出),而非进程表中。这些表也可以用多种方式实现,不必一定是数据,例如,可将它们实现为结构的链表。如果不考虑实现细节的话,通用概念是相同的。
图3-7显示了一个进程对应的3张表之间的关系。该进程有两个不同的打开文件:一个文件从标准输入打开(文件描述符为0),另一个从标准输出打开(文件描述符为1)。
从UNIX系统的早期版本以来,这3张表之间的关系一直保持至今。这种关系对于在不同进程之间共享文件的方式非常重要。在以后的章节中涉及其他文件共享方式时还会回到这张图上来。
如果两个独立进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。
给出了这些数据结构后,现在对前面所述的操作进一步说明。
- 在完成每个write后,在文件表项中的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量(也就是该文件加长了)。
- 如果用O_APPEND标志打开了一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处。
- 若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置i节点表项中的当前文件长度(注意,这与用O_APPEND标志打开文件时不同的)。
- lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。
可能有多个文件描述符项都指向同一个文件表项。在3.12节中讨论dup函数时,我们就能看到这一点。在fork后也发生同样的情况,此时父进程、子进程各自的每一个打开文件描述符共享同一个文件表项。
注意,文件描述符标志和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,而后者则应用于指向该给定文件表项的任何进程中的所有描述符。在3.14节说明fcntl函数时,我们将会了解如何获取和修改文件描述符标志和文件状态标志。
本节前面所述的一切对于多个进程读取同一个文件都能正确工作。每个进程都有它自己的文件表项,其中也有它自己的当前文件偏移量。但是,当多个进程写同一个文件时,则可能产生预想不到的结果。为了说明如何避免这种情况,需要理解原子操作的概念。