文件描述符
我们经常说到IO,什么是IO?
IO即是输入输出,它是在主存和外部设备(磁盘、终端、网络)之间复制数据的过程。输入Input,是对内存来说,即是从外部设备到主存;输出Output,则是从主存复制数据到IO设备上。
一个Linux文件就是一个m个字节的序列。 所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行的。
-
打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符。
-
Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)
-
改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为k。
-
读写文件。一个读操作就是从文件复制 n >0个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n。给定一个大小为 m 字节的文件,当 k >= m时执行读操作会触发一个称为end-of-file (EOF)的条件,应用程序能检测到这个条件.在文件结尾处并没有明确的"EOF符号"。 类似地,写操作就是从内存复制 n>0 个字节到一个文件,从当前文件位置k 开始,然后更新k。
-
关闭文件,当应用完成了对文件的访问之后,它就通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为什么原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
理解文件描述符的概念非常重要,有助于对很多系统软件运行的理解。
内核怎样表示一个打开的文件
内核用三个相关的数据结构来表示打开的文件:
- 描述符表(descriptor table)。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
- 文件表(file table)。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成(针对我们的目的)包括当前的文件位置、引用计数 (reference count)(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。直到引用计数为零时,内核才会删除这个文件表表项。
- v-node表(v-node table)。同文件表一样,所有的进程共享这张v-node表。每个表项包含 stat 结构中的大多数信息,包括 st_mode 和 st_size成员。
注:文件描述符表是每个进程级别拥有的,文件表和v-node表是所有进程共享的。
**每个文件表会记录当前描述符对应的文件位置**。
多个不同进程(非父子关系)打开同一个物理文件时,会有不同的文件表(不同进程打开该文件的位置等属性不同)引用同一个v-node文件。
父子进程因为是不同进程,会有不同的文件描述符表,但同一个文件会共享同一个文件表。
文件描述符是属于进程的,所以不同进程完全可能有相同的文件描述符数字。
关键思想:每个描述符有它自己的文件位置
这三个相关数据结构,有几种典型的打开文件情况。
- 同一个进程下,描述符1和4 通过打开不同的文件表,引用两个不同的物理文件。
- 同一个进程下,描述符1和4 通过打开不同的文件表,引用同一个物理文件。两个描述符有两个不同的文件位置。
- 父子两个进程,两个描述符表打开不同的文件表,引用各自物理文件。共享的文件表下引用计数大于1。
I/O重定向
shell中会经常用到重定向,比如把输出到标准输出的内容打到一个文件里。
$ ls > a.log
标准输出的描述符重定向到了a.log的文件描述符。 重定向描述符的函数dup2。
#inclued<unistd.h>
int dup2(int oldfd, int newfd);
新描述符(第二个参数newfd)变成旧描述符(第一个参数oldfd)的复制品,两个文件描述符都指向同一个文件,均指向 oldfd对应的文件表。
比如 dup2(4, 1);
重定向这里可以提及到web服务器中,cgi程序执行结果怎么返回给客户端的。cgi程序执行结果是打到标准输出上,比如利用dup2函数,把这个描述符重定向到tcp中的连接描述符上。
参考
- 《CSAPP》