File System, Kernel Data Structures, and Open Files(文件系统,内核数据结构,与打开文件)

写在前面

  1. 本文来自 USNA(美国海军学院)系统编程课的讲义,现将其翻译在此,由于没有版权所以 谢绝任何转载,如果你能拿到版权,当我没说
  2. 本人英文水平较弱,有错误请大家帮忙指出
  3. 关于内核结构,我没有看过最近的 Linux 系统内核,所以是否真如文章说的那样,有待验证, 不过测试程序是可以用文中内核结构解释
  4. 译者: fighter(tt)

回顾: 什么是文件系统

回忆一下,文件系统是一种放在目录中的文件和文件夹的组织方式,有许多不同种类的文件系统和不同的实现方式,就是和包含数据的存储设备密切相联系的,e.g. 硬盘,指存(thumb drive), CDrom,和操作系统,e.g. MAC,Linux,Windows

文件系统的目的是维护和组织一个二级存储,和RAM相反,RAM是易失的并且不能在计算机重启的时候一直存在,二级存储是设计来永久存储数据的。文件系统提供了由操作系统维护的数据布局的便捷表示。存在各种各样的文件系统实现,其描述了组织维护文件系统所必需的元数据的不同方式。 e.g.,Windows上的标准文件系统类型称为NTFS,它代表Windows NT文件系统,Linux / Unix系统的标准文件系统称为ext3或ext4,具体取决于版本。

O.S. 有一个根文件系统,用于存储主数据和系统文件。 在Unix系统上,这是基本文件系统,由单个/表示根目录。 在Windows上,这通常称为C:\驱动器。 O.S. 也可以挂载(mount)其他文件系统,如插入USB驱动器或CD-ROM,用户可以访问这些其他文件系统,这可以使用不同的布局和数据组织,例如FAT32 CD驱动器上的USB驱动器或ISO 9660。

然而,从系统程序员的视角,我们写的程序对于底层文件系统的实现来说是透明的;我们用 open打开一个文件, 用 read读一个文件, write写一个文件并不关心底层的文件系统的实现方式。底层文件系统的实现细节是完全透明的。那么 OS 是如何维护这种幻觉的呢? 这部分主题在下一节——我们会探索文件系统的实现细节来支持我们此前已经写过的程序。

内核数据结构

为了理解文件系统,你首先得知道内核是如何组织和维护信息的, 内核做了大量的记录(bookkeeping) 它(OS kernel)需要知道哪个进程正在运行, 他的内存布局是什么,进程持有哪些打开的文件,etc. 为了支持这些工作,内核维护了3个重要的 表/结构 来管理进程打开的文件: 进程表,文件表, 和 v-node/i-node info

Figure 1: Kernel File System Data Structures

我们接下来会深入的了解每一个部分.

进程表(Process Table)

第一个数据结构是进程表(Process Table) ,存储所有当前正在运行的进程信息(译者注: 一个CPU不是只有一个运行进程吗?),这些信息包括进程的内存布局,当前执行点, etc. 通常也包括打开的文件描述符。

在这里插入图片描述

众所周知,所有的进程开始的时候都有3个标准的文件描述符, 0,1,2 ,这些数字和其他的文件描述符是索引,所有进程也在进程表中存储了在打开文件表中的进程项(Process’s entry),(译者注: 上面那个进程表中的小框,标志了fd与filepointer的东西,称为打开文件表,每个进程都有,而后面要说的文件表是全局的,整个操作系统仅有一个 )每次新文件打开,文件表中会添加新的一行(译者注 fd的最大值是有限的,OS会选取一个最小的未使用的fd), 索引文件描述符的值, e.g. 3,4,5,etc

在打开文件表中的每一行都有两个值。 一个是 fd flags, 描述了文件的处理方式,例如, 被打开,或是被关闭,或者是否可以对某些被关闭的文件采取某些行动。第二个值是指向文件表中的打开文件的指针,文件表(不是指进程表中的打开文件表)是一个当前打开文件的全局列表,穿过所有进程(across all process)

一个有趣的笔记是,进程表无论何时都会被fork给创建的孩子,整个进程表项都会被复制,包括打开的文件项和他们的文件指针。这是两个进程,父进程和子进程共享打开文件的方法。 之前我们看见过这个例子,我们会在这门课中再次看见他。

文件表

每当在系统范围内打开一个新的文件时,就会在全局文件表中创建一个新的表项。例如,当一个文件被两个不同的进程打开时,他们或许会有同样的文件描述符,e.g. 3 , 但是每一个文件描述符都会引用在文件表中的不同表项。

在这里插入图片描述

每一个文件表项都包含了当前文件的信息,最重要的是, 文件状态,例如文件的读写状态和其他的状态信息。 此外, 文件表包含了一个 offset ,描述文件读和写了多少个字节,表明文件下一次读(和写) 的位置。 例如, 文件最初打开用来读的时候,偏移是0,因为没有文件用来读,在读了10个字节之后 offset 被迁移10个,应为被读了10个字节。这是一种允许程序在序列中读文件的机制。后续,我们会看见我们如何操纵这些偏移以及从文件的不同部分读取文件

这个表中的最后一部分是 一个 v-node 指针, 这是一个指向 v-node(virtual node) 和 i-node(index node) 的指针。这两个节点包含了如何读取文件的信息。

V-node 和 I-node 表

v-node 和 i-node 表明了文件的文件系统的处理方式和底层的存储机制。它连接了软件和硬件。 例如,在某些时刻,文件被打开需要接触磁盘,但是,我们知道在磁盘上,不同的文件系统使用了不同的编码方式。 V-node 是一种抽象的机制,使得有一种统一的方式访问这些信息,独立于底层的文件系统实现,然而i-node 存储了特殊的访问信息。

在这里插入图片描述

另一种区别v-node 和 i-node 的方式是,v-node 想一个存储在文件系统中的文件,抽象的说,它可以是任何东西,并且存储在任何设备上——它甚至可以不是文件,像/dev/urandom 或者 /dev/zero. 一个 i-node, 贩子,描述了文件应该怎样被访问,包括他存储在哪一个设备上,以及设备独有的读/写 程序

在Linux和许多Unix系统中,没有显式地使用v-node,而是只有i-node, 然而,i-node服务于双重目的。一个 i-node 可以是文件的通用抽象表示,像 v-node 一样;也可以是存储设备的特殊结构。我们将坚持讨论v-node / i-node的区别,因为它简化了许多概念。

回顾打开文件

现在我们对内核数据结构有了更好的了解,让我们回顾一下文件描述符的一些常见用法,以及它如何与我们对内核数据结构的理解相匹配。

标准文件描述符

标准文件描述符用 getty创建,并且和一个终端设备绑定, 但是我们使用它们和其他的打开文件一样,用readwrite操作,它们必须在进程表,文件表和v-node中有一个表项

在这里插入图片描述

可是,标准文件描述符并不是磁盘上的文件,而是与终端设备相关联的,这意味着v-node项指向的是终端设备,而i-node 存储的是底层访问函数使用户可以从终端设备进行读写。

打开新文件

当用open函数打开一个新文件时,一个新的文件描述符就产生了,通常是最后一个文件描述符号加1(最小可用文件描述符号) 文件描述符表中会提供一个新的表项,index就是文件描述符号,打开文件表中也会产生一个新的表项。

在这里插入图片描述

如果此文件存在于光盘上,则文件表条目将引用引用可从磁盘读取/写入数据的i-node信息的v节点或存储该文件的特定设备。

父进程与子进程共享文件

当进程fork的时候,整个进程表都会被复制,包括所有打开的文件描述符

在这里插入图片描述

但是文件表并没有被复制。父进程和子进程的文件描述符都引用的是同一个文件表项。记住文件offset 是存在文件表中的,所以当一个进程从文件中读数据的时候,它会移动这个偏移,而当其他进程从这个文件读数据的时候,它会从第一个进程离开的地方开始读。这就解释了我们在之前的课程中所看到的下面的这个程序

int main(int argc, char * argv[]){

  int status;
  pid_t cpid, pid;

  int i=0;

  while(1){ //loop and fork children

    cpid = fork();

    if( cpid == 0 ){
      /* CHILD */

      pid = getpid();

      printf("Child: %d: i:%d\n", pid, i);

      //set i in child to something differnt
      i *= 3;
      printf("Child: %d: i:%d\n", pid, i);

      _exit(0); //NO FORK BOMB!!!
    }else if ( cpid > 0){
      /* PARENT */

      //wait for child
      wait(&status);

      //print i after waiting
      printf("Parent: i:%d\n", i);

      i++;
      if (i > 5){       
        break; //break loop after 5 iterations
      }

    }else{
      /* ERROR */
      perror("fork");
      return 1;
    }

    //pretty print
    printf("--------------------\n");
  }

  return 0;
}

父进程和子进程有不同的进程表,共享一个文件表项。子进程的每一次成功read都会使offset前进,而这个同样的光标会出现在父进程中。 结果是父进程和子进程交替的读取文件中的每一个字符。

(译者注: 1.上面的程序中并没有出现交替阅读的现象,2. 编译需加<stdio.h>和<unistd.h>头文件,并且gcc加上-lptread选项)

复制文件

我们也看一下用dup2进行文件复制及其内核数据结构的表示。回忆一下,dup2会将一个文件描述符指向另外一个

在这里插入图片描述

在内核数据结构中,这意味着两个文件描述符表引用同一个文件表项。结果是读写任何一个文件描述符都是一样的,因为他们引用了同一个文件。

管道

管道更像是标准文件描述符,因为他们并不指向文件系统中的具体文件,而是内核数据结构本身,用于管道读写端之间的管道数据。

在这里插入图片描述

调用pipe会创建两个文件描述符,一个用于读,一个用于写,在文件的两端。每一个文件描述符都会在文件表中有表项,但是v-node项是通过内核缓存链接的.

在虚拟文件系统中挂载文件系统

从上面的示例中可以看出,内核数据结构为在不同情况下处理各种文件提供了很大的灵活性。 v-node和i-node的抽象是关键:无论底层实现和数据布局如何,从v-node往上,我们可以使用单个接口,而i-node提供设备特定信息。 这允许文件系统在单个接口下混合不同的v-node,即使v-node表示的每个文件可能存在于不同的设备上。 这个过程称为挂载(mount); 在统一的抽象下将多个设备及其文件系统合并到一个统一的文件系统中。

(译者注后面还有一节内容不想翻译了,真是痛苦,第一次翻译:(, )

猜你喜欢

转载自blog.csdn.net/Dylan_Frank/article/details/83216050
今日推荐