首先我们需要知道操作系统内维护了三个系统文件表:文件描述符表(file descriptor table),打开文件表(open file table),inode table。这三个表的结构见Table-1
我们知道在like-unix系统中所有的IO操作(包括socket等)都是基于文件描述符的。程序刚刚启动的时候已经默认帮我们分配了三个文件描述符,就是我们常用的0标准输入,1标准输出,2标准错误。如果此时进程再打开新的文件,它的文件描述符会加1也就是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号。
Table-1
每一个进程维护了一份自己的文件描述符表,该表中的每条记录维护了该条文件描述符的相关信息,包括:
内核内维护的一个系统范围的打开文件表。该表中包含每一个打开的文件条目,文件表项跟踪所有对文件的读或写操作当前偏移量和文件的打开模式(O_RDONLY, O_WRONLY, or O_RDWR)。
该表中的每条记录维护了与一个打开的文件相关的全部信息,包括:
在linux系统中使用inode号描述文件,就像进程使用pid描述进程一样。inode存储了文件的元信息。系统上的所有文件都分配了一个inode记录。
每个inode记录包含以下信息(可以使用stat命令查看):
$ stat a
File: a
Size: 4 Blocks: 8 IO Block: 4096 regular file
Device: 805h/2053d Inode: 1446030 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/zhangboqi) Gid: ( 1000/zhangboqi)
Access: 2021-09-29 14:35:32.344894535 +0800
Modify: 2021-09-29 14:35:32.344894535 +0800
Change: 2021-09-29 14:35:32.344894535 +0800
Birth: -
注:inode存储在磁盘设备上,但内核会在内存中维护一份。这里的inode table就是指内存中的副本。inode table中的记录同样维护了一个引用计数,该记录每被open file记录引用一次就加1。
进程可用的文件描述符的数量由sys/limits.h文件中的/OPEN_MAX控制。文件描述符的数量也可由ulimit -n控制。可以分配给进程的文件描述符的数量由资源限制控制。默认值是在/etc/security/limits文件中设置的,通常设置为2000。可以通过ulimit命令或setrlimit子例程更改该限制。最大大小由常量OPEN_MAX定义。
open, pipe, create和fcntl子程序都可以创建文件描述符。文件描述符在每个进程里都是唯一的,但是他们可以有fork子程序创建的子进程共享。也可以由fcntl,dup和dup2子程序复制。
文件描述符是一个存储在内核为每个进程维护的u_block区域中的文件描述符表的索引。进程获取文件描述符的最常见方法是通过open或create操作,或者从父进程继承。当一个fork操作发生时。系统为子进程复制父进程的文件描述符表,它允许子进程平等地访问父进程使用的文件。
因为文件是可以同时被多个用户共享的,所以有必要允许相关进程共享一个公共偏移指针,并且访问同一文件的独立进程拥有一个单独的当前偏移指针,open file table 条目维护了一个引用计数,用来跟踪分配给文件的文件描述符的数量。
对单个文件的多个引用可能是由下列任何一种情况引起的:
每个打开操作创建一个系统打开文件表项。单独的表项确保每个进程有单独的当前I/O偏移量。独立偏移量可以保护数据的完整性。当一个文件描述符被复制时,两个进程将共享相同的偏移量,并可能发生交错,在这种情况下,字节不是按顺序读或写的。
文件描述符可以通过以下方式在进程之间复制:dup或dup2子例程、fork子例程和fcntl(文件描述符控制)子例程。
dup子例程创建一个文件描述符的副本。副本是在包含原始描述符的用户文件描述符表中的空白位置创建的。dup进程将open file table项中的引用计数加1,并返回副本所在的文件描述符的索引号。
fork子例程创建一个子进程,子进程继承分配给父进程的文件描述符。然后子进程执行一个新进程。当fcntl子程序关闭时继承的文件描述符会设置close-on-exec标志
fcntl子例程操作文件结构并控制打开的文件描述符。它可以用来对描述符进行以下更改:
当shell运行一个程序时,它会打开三个文件,其中包含文件描述符0、1和2。这些描述符的默认赋值如下:
Descriptor |
Explanation |
---|---|
0 | Represents standard input. |
1 | Represents standard output. |
2 | Represents standard error. |
这些默认的文件描述符连接到终端,因此,如果程序读取文件描述符0并写入文件描述符1和2,则程序从终端收集输入并将输出发送到终端。当程序使用其他文件时,文件描述符按升序分配。
如果使用<(小于)或>(大于)符号重定向I/O,则shell的默认文件描述符分配将被更改。例如,以下命令将文件描述符0和1的默认赋值从终端更改为适当的文件:
prog < FileX > FileY
在这个例子中,文件描述符0现在指的是FileX,文件描述符1指的是FileY。文件描述符2没有被更改。程序不需要知道它的输入来自哪里,也不需要知道它被发送到哪里,只要文件描述符0表示输入文件,1和2表示输出文件。
下面的示例程序演示了标准输出的重定向:
#include
#include
void redirect_stdout(char *);
main()
{
printf("Hello world\n"); /*this printf goes to
* standard output*/
fflush(stdout);
redirect_stdout("foo"); /*redirect standard output*/
printf("Hello to you too, foo\n");
/*printf goes to file foo */
fflush(stdout);
}
void
redirect_stdout(char *filename)
{
int fd;
if ((fd = open(filename,O_CREAT|O_WRONLY,0666)) < 0)
/*open a new file */
{
perror(filename);
exit(1);
}
close(1); /*close old */
*standard output*/
if (dup(fd) !=1) /*dup new fd to
*standard input*/
{
fprintf(stderr,"Unexpected dup failure\n");
exit(1);
}
close(fd); /*close original, new fd,*/
* no longer needed*/
}
在文件描述符表中,文件描述符编号被分配到请求描述符时可用的最低描述符编号。但是,可以使用dup子例程在文件描述符表中分配任何值。