March 15th, 2017

文件描述符是unix系统中最重要的概念之一,绝大部分的系统调用都涉及到文件描述符。文本阐述文件描述符的概念。

文件描述符

简称fd,unix内核对所有打开的文件,使用fd进行引用,fd表现为一个递增的非负整数。

所谓“打开的文件”并不一定是指我们通常理解的文本文件,可能是个设备文件,或是socket。在unix中一切皆文件,文件是一个泛称。

文件描述符具有以下性质:

  • 每个进程具有自己的fd递增空间。已关闭的fd所占用的正整数是可能被重复利用的。单个进程能同时打开的fd数量是受到系统limit设置限制的。
  • 按照约定,所有的shell在启动新应用程序的时候,总是将012这三个数字的描述符打开为标准输入标准输出标准错误

文件表项

文件表项是内核中的概念,但是却是理解fd的必需概念,下面通过APUE的几张经典的图来说明。

下图描述了在应用程序通过shell启动后,进程的内核数据结构的大致安排:

内核为每一个进程维护一个fd表,其中每一条记录包含fd的值,标志(close_on_exec)和指针。

每个fd在内核中会指向一个文件表项,文件表项保存了文件状态标志,文件偏移,进一步的,文件表项又指向v节点,v节点与实际的物理上的文件是一一对应的。也就是说,对于同一个文件,无论有多少个进程打开它,v节点都指向同一个。下图展示了当两个不同的进程同时打开一个文件时的内核数据结构:

补充一点,由于不同的进程对同一个文件的读写行为往往是不同的,所以文件表项可以不共享,但指向同一个v节点。

下图展示了一种更复杂的场景,不同进程可以指向相同的文件表项,这种场景往往是因为fork产生的,理解这种场景对于理解很多基于多进程的技巧有很大帮助:

在这种场景下,即使进程A和进程B对同一个文件的引用fd值不同,但只要他们同时指向同一个文件表项。那么互相影响是存在的。

dup

除了通过一些系统调用来打开文件以获得文件描述符外,还可以通过dupdup2,复制当前进程内已有的文件描述符,甚至可以在复制的时候覆盖已有的fd。

例如下图,dup(1)将得到fd3,而且fd3fd1同时指向同一个文件表项,换句话说fd3fd1实际是完全一样,如果fd1是约定的标准输出的话,向fd3写入数据,也可以输出到标准输出

记得shell有重定向输出的功能吧?比如2>&1表示将标准错误重定向到标准输出。内部,shell通过在启动子进程时,将子进程的fd2通过dup2(2,1)函数调用重新指向fd1来实现这个功能!

int dup2(int fd, int fd2) 表示复制一个fd出来,新的fd使用fd2作为值,如果fd2已经打开,那么先关闭fd2

fork

fork是unix的系统调用,指示内核创建一个与当前调用进程“完全一样的”的进程内存镜像。换句话说fork出来的子进程跟父进程在调用时的状态是几乎完全一致的,除了个别东西不被继承(比如不继承文件锁),但是很重要的一点是继承了fd。也就是说截止fork的调用点,子进程和父进程的fd表是完全一致的,而且每个fd指向的文件表项并没有复制!上面的例子已经说明了这个问题。

用unix传统的IPC管道来解释forkfd的问题会比较容易理解。管道(pipe)是进程间通信的一种古老方式,shell中的管道就是用pipe来实现的。

首先,进程通过调用pipe(int fd[2]);系统调用创建出两个fd,这两个fd在当前进程中的关系如下:

如上图,在fd[1]的上写入数据,将从fd[0]上读出。然而,到目前为止,这个管道只是可以通过同一个进程的两个fd之间通信,并没有意义。

接下来,调用fork后:

可以看到,这个时候,child和parent之间可以通过各自的fd[0]和fd[1]进行通信了,这就是管道。如果只希望一个方向的数据通信,可以各自关闭不需要的fd,保留一个方向即可。

exec

通常,我们希望利用管道建立两个不同功能的进程之间的通信机制,而不是仅仅使用fork,创建一个完全一样功能的进程。

exec系列函数的功能是将当前进程的内存镜像,用另外一个程序覆盖,并且从新程序的main开始执行。因此,fork后,在子进程中调用exec,就是用来启动另一个程序的方法。

尽管,exec覆盖了当前进程的内存镜像,但是还是有东西被保留下来了,其中就有fd。在考虑fd是否在exec是被保留下来时,我们不得不回顾,每个fd有一个close_on_exec的标志,这个标志就是设置这个fd,是否在进程exec时,需要被内核关闭。

如果我们需要用pipe+fork+exec的组合来启动一个新的进程,并希望两个进程之间能通过管道交互的话,显然,我们不希望exec时管道相关的fd被关闭。因为,如果fd在子进程中在exec时被关闭了,那么通道其实就断开了。

总结

本文阐述了unix系统中,文件描述符的含义,并且展示了fd和内核数据结构的关系,这对于理解fd至关重要。介绍了dup的功能,另外,以pipe为例,阐述了fork和exec时,fd的关系和变化。


1块2块也是钱,小额赞助