所有执行I/O操作的系统调用都以文件描述符,即一个非负整数来指代所打开的文件。文件描述符可以用来表示所有类型的已打开文件。同时,多个文件描述符可以指向同一个打开文件,因为有在不同进程中打开同一个文件的需求。
那么,系统是如何维护硬盘文件与文件描述符之间的联系呢?
要理解具体的情况如何,需要查看由内核维护的3个数据结构:
- 进程级的文件描述符
- 系统级的打开文件表
- 文件系统的i-node表
针对每个进程,内核为其维护打开文件的描述符(open file descriptor)表。该表的每一项都记录了单个文件描述符的相关信息,如下所示:
- 控制文件描述符操作的一组标志(目前仅定义了一个,即close-on-exec标志)
- 对打开文件句柄的引用
内核对所有打开的文件维护一个系统级的描述表格(open file description table)。也叫打开文件表(open file table),表中的每一项称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全信息,如下:
- 当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
- 文件的访问模式(如调用read()时设置的只读模式、只写模式等)
- 与信号驱动I/O相关的设置
- 对该文件i-node对象的引用
同时,文件系统又会对每个存储其上的文件建立一个i-node表。这里只给出i-node表的信息:
- 文件类型(如常规文件、套接字或FIFO)和访问权限
- 一个指针,指向该文件所持有的的锁的列表
- 文件的各种属性,包括文件大小以及不同类型操作相关的时间戳
下图展示了这3个数据结构之间的关系。在下图中,两个进程拥有诸多打开 的文件描述符:
在进程A中,文件描述符1和20都指向同一个打开的文件句柄23,这可能是通过调用dup、dup2或fcntl形成的。
进程A的文件描述符2和进程B的文件描述符2都指向同一个打开的文件句柄73,这可能是调用fork之后出现的。或者某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程。
此外,进程A的文件描述符0和进程B的文件描述符3分别指向不同的打开文件句柄,但这些打开文件句柄指向同一个i-node表中的相同条目。可能是两个进程各自对同一个文件调用open打开。同时,在一个进程中两次打开同一个文件,也会出现这种情况。
这里我们可以得到一些结论:
- 两个不同的文件描述符,若指向同一个文件打开句柄,将共享同一文件的偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量(调用open、write、lseek),那么从另一个文件描述符中也会得到相应的改变。不管这两个文件描述符是分别属于同一进程还是不同的进程;
- 要获取和修改打开的文件标志(如O_APPEND、O_NONBLOCK等),可以执行fcntl()的F_GETFL和F_SETFL操作,其对作用域的约束与上一条类似;
- 相比之下,文件描述符标志(close-on-exec标志)为进程和文件描述符所私有,对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符。
下面给出具体的实例验证上面的讨论。
fd1=open(file,O_CREAT | O_RDWR | O_TRUNC,S_IRUSR | S_IWUSR);
fd2=dup(fd1);
fd3=open(file,O_RDWR);
write(fd1,"hello",6);
write(fd2,"world",6);
lseek(fd2,0,SEEK_SET);
write(fd1,"HELLO",6);
write(fd3,"yellow",6);
上面的代码中有三个文件描述符,都是打开同一个文件,即共享同一个i-node项。不同的是fd2通过调用dup复制文件描述符fd1,因此fd1与fd2共享同一个文件打开句柄。fd3是同一个进程中另一个open操作,因此和fd1对比,两个文件描述符指向不同的文件打开句柄,但指向同一个i-node项目。下图展示了它们的关系:
因此,对fd1和fd2的open、write、lseek等操作会使文件偏移量发生改变。fd1写入“hello”之后,文件偏移量变为6,fd2写入时在6之后写入,此时文件中的内容是“helloworld”;
当fd2调用lseek改变文件偏移量为0,之后fd1写入文件,是在偏移量为0的基础上写入,因此覆盖最初的6字节,文件内容变为“HELLOworld”;对于fd3,因为与fd1和fd2指向不同的文件打开句柄,因此fd1和fd2的操作不影响fd3指向的文件打开句柄的文件偏移量,所以fd3写入时会覆盖最初的6字节,此时文件内容变为“yelloworld”。
程序运行结果如下:
结果证实了我们的讨论。