1. 程式人生 > >File System, Kernel Data Structures, and Open Files(檔案系統,核心資料結構,與開啟檔案)

File System, Kernel Data Structures, and Open Files(檔案系統,核心資料結構,與開啟檔案)

寫在前面

  1. 本文來自 USNA(美國海軍學院)系統程式設計課的講義,現將其翻譯在此,由於沒有版權所以 謝絕任何轉載,如果你能拿到版權,當我沒說
  2. 本人英文水平較弱,有錯誤請大家幫忙指出
  3. 關於核心結構,我沒有看過最近的 Linux 系統核心,所以是否真如文章說的那樣,有待驗證, 不過測試程式是可以用文中核心結構解釋

回顧: 什麼是檔案系統

回憶一下,檔案系統是一種放在目錄中的檔案和資料夾的組織方式,有許多不同種類的檔案系統和不同的實現方式,就是和包含資料的儲存裝置密切相聯絡的,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); 在統一的抽象下將多個裝置及其檔案系統合併到一個統一的檔案系統中。

(譯者注後面還有一節內容不想翻譯了,真是痛苦,第一次翻譯:(, )