前言
今天在看UNP,看到socket的文件描述符,想起之前在看APUE时觉得比较绕的文件描述符、文件表项等知识,于是决定对相关知识整理一下。本篇博客的大部分内容都引用自:。原文写的很棒!
文件描述符、文件表、索引节点表
在进程打开一个文件时,会与三个表发生关联:文件描述符表、文件表、索引结点表。每个进程都有一张专属的文件描述符表,它负责保存当前进程已经打开的文件相关的文件描述符。文件表则是一张很大的表,它被存放在内核空间,由系统里的所有进程共享。索引结点表也存放在内核空间,由所有进程所共享。
三张表的作用
文件描述符表:该表记录进程打开的文件。它的表项里面有一个指针,指向存放在内核空间的文件表中的一个表项。它向用户提供一个简单的文件描述符,使得用户可以通过方便地访问一个文件。
当进程使用open打开一个文件时,内核就会在这个表中添加一个表项。如果对同一个文件打开多次,那么将有多个表项。使用dup时,也会增加一个表项。
文件表:文件表保存了进程对文件读写的偏移量。该表还保存了进程对文件的存取权限。比如,进程以O_RDONLY方式打开文件,这将记录到对应的文件表表项中。同时文件表项还有一个引用计数,记录有多少个文件描述符指向这个文件表项。只有指向这个表项的所有文件描述符都被close()之后,这个文件表项才会被回收。
索引结点表:在文件系统中,也是有一个索引结点表的。如下图:
文件被打开时三张表的多种状态
1. 不同的进程打开同一个文件
首先,每个进程有自己的文件描述表。不同的进程打开同一个文件,那么他们应该有各自对应的文件表表项,每个表项的引用计数都分别为1。因为文件表表项记录了进程读写文件时的偏移量和存取权限。多个进程不可能共享一个文件偏移量。另外他们各自打开文件的权限也可能是不同的,有的是为了读、有的为了写,有的为了读写。所以,他们应该有不同的文件表表项。
此外,因为是同一个文件,所以,多个进程会共享同一个索引结点表项。即他们的文件表表项指针会指向同一个索引结点。三张表的状态如下图所示:
2. 使用dup函数复制一个文件描述符
关于dup函数复制这个APUE上有详细说明,dup函数是用来复制一个文件描述符的。复制得到的文件描述符和原描述符共享文件偏移量和一些状态。所以dup的作用仅仅是复制一个文件描述符表项,而不会复制一个文件表表项。因此,文件表表项保存的引用计数为2,也就是如果这两个描述符中有其中一个被close,这个文件表项依然会被保留。只有两个文件描述符都被close了,这个文件表项才会被回收。三张表的状态如下图所示:
3. 同一个进程多次打开同一个文件
首先肯定是每次打开一个文件就会有一个不同的文件描述符。其次,每打开一次同一个文件,内核就会在文件表中增加一个表项。这是因为每次open文件时使用了不同的读写权限,而读写权限是保存在文件表表项里面的。三张表的状态如下图所示:
4. 父进程使用fork创建子进程
由于fork一个子进程,子进程将复制父进程的绝大部分东西(除了进程ID、进程的父进程ID、一些时间属性、文件锁)。所以子进程复制了父进程的整个文件描述符表。而父进程的文件描述符表中的每一个表项都是一个指向文件表项的指针,所以实际上子进程文件描述符表中的每一个表项也是指向父进程指向的文件表项。三张表的状态如下图:
5. 执行exec
因为执行exec实际上只是将进程的程序映像进行了替换,所以文件描述符表没有发生任何变化,与执行exec前相同。但有时,可能一个进程有很多个文件描述符,执行exec后,都用不着了。那么此时,应该关闭它。这涉及到一个close-on-exec概念,就是在执行exec时,close(关闭)文件描述符。在默认情况下,执行exec是不关闭的。这里有一个系统调用fcntl
可以关闭之。函数原型如下:
int fcntl(int fd,int cmd, … /* int arg */);
第一个参数是文件描述符,第二个参数用来指定是要进行的操作。第三个参数依赖于第二个参数。
与本文相关的是操作是 F_GETFD和F_SETFD。其分别用来获取close-on-exec,设置close-on-exec标识的值。
可以通过
fcntl(fd, F_SETFD, 1);
来设置,当执行exec时将fd文件描述符关闭。第三个参数为1时表示fd在exec时关闭,为0时表示fd在exec时不关闭。