在 Linux 中,最直觀、最可見的部分就是 文件系統(file system)。下面我們就來一起探讨一下關于 Linux 中國的文件系統,系統調用以及文件系統實現背後的原理和思想。這些思想中有一些來源于 MULTICS,現在已經被 Windows 等其他操作系統使用。Linux 的設計理念就是 小的就是好的(Small is Beautiful) 。雖然 Linux 隻是使用了最簡單的機制和少量的系統調用,但是 Linux 卻提供了強大而優雅的文件系統。
Linux 文件系統基本概念Linux 在最初的設計是 MINIX1 文件系統,它隻支持 14 字節的文件名,它的最大文件隻支持到 64 MB。在 MINIX 1 之後的文件系統是 ext 文件系統。ext 系統相較于 MINIX 1 來說,在支持字節大小和文件大小上均有很大提升,但是 ext 的速度仍沒有 MINIX 1 快,于是,ext 2 被開發出來,它能夠支持長文件名和大文件,而且具有比 MINIX 1 更好的性能。這使他成為 Linux 的主要文件系統。隻不過 Linux 會使用 VFS 曾支持多種文件系統。在 Linux 鍊接時,用戶可以動态的将不同的文件系統挂載倒 VFS 上。
Linux 中的文件是一個任意長度的字節序列,Linux 中的文件可以包含任意信息,比如 ASCII 碼、二進制文件和其他類型的文件是不加區分的。
為了方便起見,文件可以被組織在一個目錄中,目錄存儲成文件的形式在很大程度上可以作為文件處理。目錄可以有子目錄,這樣形成有層次的文件系統,Linux 系統下面的根目錄是 / ,它通常包含了多個子目錄。字符 / 還用于對目錄名進行區分,例如 /usr/cxuan 表示的就是根目錄下面的 usr 目錄,其中有一個叫做 cxuan 的子目錄。
下面我們介紹一下 Linux 系統根目錄下面的目錄名
- /bin,它是重要的二進制應用程序,包含二進制文件,系統的所有用戶使用的命令都在這裡
- /boot,啟動包含引導加載程序的相關文件
- /dev,包含設備文件,終端文件,USB 或者連接到系統的任何設備
- /etc,配置文件,啟動腳本等,包含所有程序所需要的配置文件,也包含了啟動/停止單個應用程序的啟動和關閉 shell 腳本
- /home,本地主要路徑,所有用戶用 home 目錄存儲個人信息
- /lib,系統庫文件,包含支持位于 /bin 和 /sbin 下的二進制庫文件
- /lost found,在根目錄下提供一個遺失 查找系統,必須在 root 用戶下才能查看當前目錄下的内容
- /media,挂載可移動介質
- /mnt,挂載文件系統
- /opt,提供一個可選的應用程序安裝目錄
- /proc,特殊的動态目錄,用于維護系統信息和狀态,包括當前運行中進程信息
- /root,root 用戶的主要目錄文件夾
- /sbin,重要的二進制系統文件
- /tmp, 系統和用戶創建的臨時文件,系統重啟時,這個目錄下的文件都會被删除
- /usr,包含絕大多數用戶都能訪問的應用程序和文件
- /var,經常變化的文件,諸如日志文件或數據庫等
在 Linux 中,有兩種路徑,一種是 絕對路徑(absolute path) ,絕對路徑告訴你從根目錄下查找文件,絕對路徑的缺點是太長而且不太方便。還有一種是 相對路徑(relative path) ,相對路徑所在的目錄也叫做工作目錄(working directory)。
如果 /usr/local/books 是工作目錄,那麼 shell 命令
brcp books books-replica
就表示的是相對路徑,而
brcp /usr/local/books/books /usr/local/books/books-replica
則表示的是絕對路徑。
在 Linux 中經常出現一個用戶使用另一個用戶的文件或者使用文件樹結構中的文件。兩個用戶共享同一個文件,這個文件位于某個用戶的目錄結構中,另一個用戶需要使用這個文件時,必須通過絕對路徑才能引用到他。如果絕對路徑很長,那麼每次輸入起來會變的非常麻煩,所以 Linux 提供了一種 鍊接(link) 機制。
舉個例子,下面是一個使用鍊接之前的圖
以上所示,比如有兩個工作賬戶 jianshe 和 cxuan,jianshe 想要使用 cxuan 賬戶下的 A 目錄,那麼它可能會輸入 /usr/cxuan/A ,這是一種未使用鍊接之後的圖。
使用鍊接後的示意如下
現在,jianshe 可以創建一個鍊接來使用 cxuan 下面的目錄了。‘
當一個目錄被創建出來後,有兩個目錄項也同時被創建出來,它們就是 . 和 .. ,前者代表工作目錄自身,後者代表該目錄的父目錄,也就是該目錄所在的目錄。這樣一來,在 /usr/jianshe 中訪問 cxuan 中的目錄就是 ../cxuan/xxx
Linux 文件系統不區分磁盤的,這是什麼意思呢?一般來說,一個磁盤中的文件系統相互之間保持獨立,如果一個文件系統目錄想要訪問另一個磁盤中的文件系統,在 Windows 中你可以像下面這樣。
兩個文件系統分别在不同的磁盤中,彼此保持獨立。
而在 Linux 中,是支持挂載的,它允許一個磁盤挂在到另外一個磁盤上,那麼上面的關系會變成下面這樣
挂在之後,兩個文件系統就不再需要關心文件系統在哪個磁盤上了,兩個文件系統彼此可見。
Linux 文件系統的另外一個特性是支持 加鎖(locking)。在一些應用中會出現兩個或者更多的進程同時使用同一個文件的情況,這樣很可能會導緻競争條件(race condition)。一種解決方法是對其進行加不同粒度的鎖,就是為了防止某一個進程隻修改某一行記錄從而導緻整個文件都不能使用的情況。
POSIX 提供了一種靈活的、不同粒度級别的鎖機制,允許一個進程使用一個不可分割的操作對一個字節或者整個文件進行加鎖。加鎖機制要求嘗試加鎖的進程指定其 要加鎖的文件,開始位置以及要加鎖的字節
Linux 系統提供了兩種鎖:共享鎖和互斥鎖。如果文件的一部分已經加上了共享鎖,那麼再加排他鎖是不會成功的;如果文件系統的一部分已經被加了互斥鎖,那麼在互斥鎖解除之前的任何加鎖都不會成功。為了成功加鎖、請求加鎖的部分的所有字節都必須是可用的。
在加鎖階段,進程需要設計好加鎖失敗後的情況,也就是判斷加鎖失敗後是否選擇阻塞,如果選擇阻塞式,那麼當已經加鎖的進程中的鎖被删除時,這個進程會解除阻塞并替換鎖。如果進程選擇非阻塞式的,那麼就不會替換這個鎖,會立刻從系統調用中返回,标記狀态碼表示是否加鎖成功,然後進程會選擇下一個時間再次嘗試。
加鎖區域是可以重疊的。下面我們演示了三種不同條件的加鎖區域。
如上圖所示,A 的共享鎖在第四字節到第八字節進行加鎖
如上圖所示,進程在 A 和 B 上同時加了共享鎖,其中 6 - 8 字節是重疊鎖
如上圖所示,進程 A 和 B 和 C 同時加了共享鎖,那麼第六字節和第七字節是共享鎖。
如果此時一個進程嘗試在第 6 個字節處加鎖,此時會設置失敗并阻塞,由于該區域被 A B C 同時加鎖,那麼隻有等到 A B C 都釋放鎖後,進程才能加鎖成功。
Linux 文件系統調用許多系統調用都會和文件與文件系統有關。我們首先先看一下對單個文件的系統調用,然後再來看一下對整個目錄和文件的系統調用。
為了創建一個新的文件,會使用到 creat 方法,注意沒有 e。
這裡說一個小插曲,曾經有人問 Unix 創始人 Ken Thompson,如果有機會重新寫 UNIX ,你會怎麼辦,他回答自己要把 creat 改成 create ,哈哈哈哈。
這個系統調用的兩個參數是文件名和保護模式
brfd = creat("aaa",mode);
這段命令會創建一個名為 aaa 的文件,并根據 mode 設置文件的保護位。這些位決定了哪個用戶可能訪問文件、如何訪問。
creat 系統調用不僅僅創建了一個名為 aaa 的文件,還會打開這個文件。為了允許後續的系統調用訪問這個文件,這個 creat 系統調用會返回一個 非負整數, 這個就叫做 文件描述符(file descriptor),也就是上面的 fd。
如果在已經存在的文件上調用了 creat 系統調用,那麼該文件中的内容會被清除,從 0 開始。通過設置合适的參數,open 系統調用也能夠創建文件。
下面讓我們看一看主要的系統調用,如下表所示
系統調用
描述
fd = creat(name,mode)
一種創建一個新文件的方式
fd = open(file, ...)
打開文件讀、寫或者讀寫
s = close(fd)
關閉一個打開的文件
n = read(fd, buffer, nbytes)
從文件中向緩存中讀入數據
n = write(fd, buffer, nbytes)
從緩存中向文件中寫入數據
position = lseek(fd, offset, whence)
移動文件指針
s = stat(name, &buf)
獲取文件信息
s = fstat(fd, &buf)
獲取文件信息
s = pipe(&fd[0])
創建一個管道
s = fcntl(fd,...)
文件加鎖等其他操作
為了對一個文件進行讀寫的前提是先需要打開文件,必須使用 creat 或者 open 打開,參數是打開文件的方式,是隻讀、可讀寫還是隻寫。open 系統調用也會返回文件描述符。打開文件後,需要使用 close 系統調用進行關閉。close 和 open 返回的 fd 總是未被使用的最小數量。
什麼是文件描述符?文件描述符就是一個數字,這個數字标示了計算機操作系統中打開的文件。它描述了數據資源,以及訪問資源的方式。
當程序要求打開一個文件時,内核會進行如下操作
- 授予訪問權限
- 在全局文件表(global file table)中創建一個條目(entry)
- 向軟件提供條目的位置
文件描述符由唯一的非負整數組成,系統上每個打開的文件至少存在一個文件描述符。文件描述符最初在 Unix 中使用,并且被包括 Linux,macOS 和 BSD 在内的現代操作系統所使用。
當一個進程成功訪問一個打開的文件時,内核會返回一個文件描述符,這個文件描述符指向全局文件表的 entry 項。這個文件表項包含文件的 inode 信息,字節位移,訪問限制等。例如下圖所示
默認情況下,前三個文件描述符為 STDIN(标準輸入)、STDOUT(标準輸出)、STDERR(标準錯誤)。
标準輸入的文件描述符是 0 ,在終端中,默認為用戶的鍵盤輸入
标準輸出的文件描述符是 1 ,在終端中,默認為用戶的屏幕
與錯誤有關的默認數據流是 2,在終端中,默認為用戶的屏幕。
在簡單聊了一下文件描述符後,我們繼續回到文件系統調用的探讨。
在文件系統調用中,開銷最大的就是 read 和 write 了。read 和 write 都有三個參數
- 文件描述符:告訴需要對哪一個打開文件進行讀取和寫入
- 緩沖區地址:告訴數據需要從哪裡讀取和寫入哪裡
- 統計:告訴需要傳輸多少字節
這就是所有的參數了,這個設計非常簡單輕巧。
雖然幾乎所有程序都按順序讀取和寫入文件,但是某些程序需要能夠随機訪問文件的任何部分。與每個文件相關聯的是一個指針,該指針指示文件中的當前位置。順序讀取(或寫入)時,它通常指向要讀取(寫入)的下一個字節。如果指針在讀取 1024 個字節之前位于 4096 的位置,則它将在成功讀取系統調用後自動移至 5120 的位置。
Lseek 系統調用會更改指針位置的值,以便後續對 read 或 write 的調用可以在文件中的任何位置開始,甚至可以超出文件末尾。
lseek = Lseek ,段首大寫。
lseek 避免叫做 seek 的原因就是 seek 已經在之前 16 位的計算機上用于搜素功能了。
Lseek 有三個參數:第一個是文件的文件描述符,第二個是文件的位置;第三個告訴文件位置是相對于文件的開頭,當前位置還是文件的結尾
brlseek(int fildes, off_t offset, int whence);
lseek 的返回值是更改文件指針後文件中的絕對位置。lseek 是唯一從來不會造成真正磁盤查找的系統調用,它隻是更新當前的文件位置,這個文件位置就是内存中的數字。
對于每個文件,Linux 都會跟蹤文件模式(常規,目錄,特殊文件),大小,最後修改時間以及其他信息。程序能夠通過 stat 系統調用看到這些信息。第一個參數就是文件名,第二個是指向要放置請求信息結構的指針。這些結構的屬性如下圖所示。
存儲文件的設備
存儲文件的設備
i-node 編号
文件模式(包括保護位信息)
文件鍊接的數量
文件所有者标識
文件所屬的組
文件大小(字節)
創建時間
最後一個修改/訪問時間
fstat 調用和 stat 相同,隻有一點區别,fstat 可以對打開文件進行操作,而 stat 隻能對路徑進行操作。
pipe 文件系統調用被用來創建 shell 管道。它會創建一系列的僞文件,來緩沖和管道組件之間的數據,并且返回讀取或者寫入緩沖區的文件描述符。在管道中,像是如下操作
brsort <in | head –40
sort 進程将會輸出到文件描述符1,也就是标準輸出,寫入管道中,而 head 進程将從管道中讀入。在這種方式中,sort 隻是從文件描述符 0 中讀取并寫入到文件描述符 1 (管道)中,甚至不知道它們已經被重定向了。如果沒有重定向的話,sort 會自動的從鍵盤讀入并輸出到屏幕中。
最後一個系統調用是 fcntl,它用來鎖定和解鎖文件,應用共享鎖和互斥鎖,或者是執行一些文件相關的其他操作。
現在我們來關心一下和整體目錄和文件系統相關的系統調用,而不是把精力放在單個的文件上,下面列出了這些系統調用,我們一起來看一下。
系統調用
描述
s = mkdir(path,mode)
創建一個新的目錄
s = rmdir(path)
移除一個目錄
s = link(oldpath,newpath)
創建指向已有文件的鍊接
s = unlink(path)
取消文件的鍊接
s = chdir(path)
改變工作目錄
dir = opendir(path)
打開一個目錄讀取
s = closedir(dir)
關閉一個目錄
dirent = readdir(dir)
讀取一個目錄項
rewinddir(dir)
回轉目錄使其在此使用
可以使用 mkdir 和 rmdir 創建和删除目錄。但是需要注意,隻有目錄為空時才可以删除。
創建一個指向已有文件的鍊接時會創建一個目錄項(directory entry)。系統調用 link 來創建鍊接,oldpath 代表已有的路徑,newpath 代表需要鍊接的路徑,使用 unlink 可以删除目錄項。當文件的最後一個鍊接被删除時,這個文件會被自動删除。
使用 chdir 系統調用可以改變工作目錄。
最後四個系統調用是用于讀取目錄的。和普通文件類似,他們可以被打開、關閉和讀取。每次調用 readdir 都會以固定的格式返回一個目錄項。用戶不能對目錄執行寫操作,但是可以使用 creat 或者 link 在文件夾中創建一個目錄,或使用 unlink 删除一個目錄。用戶不能在目錄中查找某個特定文件,但是可以使用 rewindir 作用于一個打開的目錄,使他能在此從頭開始讀取。
Linux 文件系統的實現下面我們主要讨論一下 虛拟文件系統(Virtual File System)。 VFS 對高層進程和應用程序隐藏了 Linux 支持的所有文件系統的區别,以及文件系統是存儲在本地設備,還是需要通過網絡訪問遠程設備。設備和其他特殊文件和 VFS 層相關聯。接下來,我們就會探讨一下第一個 Linux 廣泛傳播的文件系統: ext2。随後,我們就會探讨 ext4 文件系統所做的改進。各種各樣的其他文件系統也正在使用中。 所有 Linux 系統都可以處理多個磁盤分區,每個磁盤分區上都有不同的文件系統。
Linux 虛拟文件系統為了能夠使應用程序能夠在不同類型的本地或者遠程設備上的文件系統進行交互,因為在 Linux 當中文件系統千奇百種,比較常見的有 EXT3、EXT4,還有基于内存的 ramfs、tmpfs 和基于網絡的 nfs,和基于用戶态的 fuse,當然 fuse 應該不能完全的文件系統,隻能算是一個能把文件系統實現放到用戶态的模塊,滿足了内核文件系統的接口,他們都是文件系統的一種實現。對于這些文件系統,Linux 做了一層抽象就是 VFS虛拟文件系統,
下表總結了 VFS 支持的四個主要的文件系統結構。
對象
描述
超級塊
特定的文件系統
Dentry
目錄項,路徑的一個組成部分
I-node
特定的文件
File
跟一個進程相關聯的打開文件
超級塊(superblock) 包含了有關文件系統布局的重要信息,超級塊如果遭到破壞那麼就會導緻整個文件系統不可讀。
i-node 索引節點,包含了每一個文件的描述符。
在 Linux 中,目錄和設備也表示為文件,因為它們具有對應的 i-node
超級塊和索引塊所在的文件系統都在磁盤上有對應的結構。
為了便于某些目錄操作和路徑遍曆,比如 /usr/local/cxuan,VFS 支持一個 dentry 數據結構,該數據結構代表着目錄項。這個 dentry 數據結構有很多東西(http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec7.html)這個數據結構由文件系統動态創建。
目錄項被緩存在 dentry_cache 緩存中。例如,緩存條目會緩存 /usr 、 /usr/local 等條目。如果多個進程通過硬連接訪問相同的文件,他們的文件對象将指向此緩存中的相同條目。
最後,文件數據結構是代表着打開的文件,也代表着内存表示,它根據 open 系統調用創建。它支持 read、write、sendfile、lock 和其他在我們之前描述的系統調用中。
在 VFS 下實現的實際文件系統不需要在内部使用完全相同的抽象和操作。 但是,它們必須在語義上實現與 VFS 對象指定的文件系統操作相同的文件系統操作。 四個 VFS 對象中每個對象的操作數據結構的元素都是指向基礎文件系統中功能的指針。
Linux Ext2 文件系統現在我們一起看一下 Linux 中最流行的一個磁盤文件系統,那就是 ext2 。Linux 的第一個版本用于 MINIX1 文件系統,它的文件名大小被限制為最大 64 MB。MINIX 1 文件系統被永遠的被它的擴展系統 ext 取代,因為 ext 允許更長的文件名和文件大小。由于 ext 的性能低下,ext 被其替代者 ext2 取代,ext2 目前仍在廣泛使用。
一個 ext2 Linux 磁盤分區包含了一個文件系統,這個文件系統的布局如下所示
Boot 塊也就是第 0 塊不是讓 Linux 使用的,而是用來加載和引導計算機啟動代碼的。在塊 0 之後,磁盤分區被分成多個組,這些組與磁盤柱面邊界所處的位置無關。
第一個塊是 超級塊(superblock)。它包含有關文件系統布局的信息,包括 i-node、磁盤塊數量和以及空閑磁盤塊列表的開始。下一個是 組描述符(group descriptor),其中包含有關位圖的位置,組中空閑塊和 i-node 的數量以及組中的目錄數量的信息。這些信息很重要,因為 ext2 會在磁盤上均勻分布目錄。
圖中的兩個位圖用來記錄空閑塊和空閑 i-node,這是從 MINIX 1文件系統繼承的選擇,大多數 UNIX 文件系統使用位圖而不是空閑列表。每個位圖的大小是一個塊。如果一個塊的大小是 1 KB,那麼就限制了塊組的數量是 8192 個塊和 8192 個 i-node。塊的大小是一個嚴格的限制,塊組的數量不固定,在 4KB 的塊中,塊組的數量增大四倍。
在超級塊之後分布的是 i-node 它們自己,i-node 取值範圍是 1 - 某些最大值。每個 i-node 是 128 字節的 long ,這些字節恰好能夠描述一個文件。i-node 包含了統計信息(包含了 stat 系統調用能獲得的所有者信息,實際上 stat 就是從 i-node 中讀取信息的),以及足夠的信息來查找保存文件數據的所有磁盤塊。
在 i-node 之後的是 數據塊(data blocks)。所有的文件和目錄都保存在這。如果一個文件或者目錄包含多個塊,那麼這些塊在磁盤中的分布不一定是連續的,也有可能不連續。事實上,大文件塊可能會被拆分成很多小塊散布在整個磁盤上。
對應于目錄的 i-node 分散在整個磁盤組上。如果有足夠的空間,ext2 會把普通文件組織到與父目錄相同的塊組中,而把同一塊上的數據文件組織成初始 i-node 節點。位圖用來快速确定新文件系統數據的分配位置。在分配新的文件塊時,ext2 也會給該文件預分配許多額外的數據塊,這樣可以減少将來向文件寫入數據時産生的文件碎片。這種策略在整個磁盤上實現了文件系統的 負載,後續還有對文件碎片的排列和整理,而且性能也比較好。
為了達到訪問的目的,需要首先使用 Linux 系統調用,例如 open,這個系統調用會确定打開文件的路徑。路徑分為兩種,相對路徑 和 絕對路徑。如果使用相對路徑,那麼就會從當前目錄開始查找,否則就會從根目錄進行查找。
目錄文件的文件名最高不能超過 255 個字符,它的分配如下圖所示
每一個目錄都由整數個磁盤塊組成,這樣目錄就可以整體的寫入磁盤。在一個目錄中,文件和子目錄的目錄項都是未經排序的,并且一個挨着一個。目錄項不能跨越磁盤塊,所以通常在每個磁盤塊的尾部會有部分未使用的字節。
上圖中每個目錄項都由四個固定長度的屬性和一個長度可變的屬性組成。第一個屬性是 i-node 節點數量,文件 first 的 i-node 編号是 19 ,文件 second 的編号是 42,目錄 third 的 i-node 編号是 88。緊随其後的是 rec_len 域,表明目錄項大小是多少字節,名稱後面會有一些擴展,當名字以未知長度填充時,這個域被用來尋找下一個目錄項,直至最後的未使用。這也是圖中箭頭的含義。緊随其後的是 類型域:F 表示的是文件,D 表示的是目錄,最後是固定長度的文件名,上面的文件名的長度依次是 5、6、5,最後以文件名結束。
rec_len 域是如何擴展的呢?如下圖所示
我們可以看到,中間的 second 被移除了,所以将其所在的域變為第一個目錄項的填充。當然,這個填充可以作為後續的目錄項。
由于目錄是按照線性的順序進行查找的,因此可能需要很長時間才能在大文件末尾找到目錄項。因此,系統會為近期的訪問目錄維護一個緩存。這個緩存用文件名來查找,如果緩存命中,那麼就會避免線程搜索這樣昂貴的開銷。組成路徑的每個部分都在目錄緩存中保存一個 dentry 對象,并且通過 i-node 找到後續的路徑元素的目錄項,直到找到真正的文件 i - node。
比如說要使用絕對路徑來尋找一個文件,我們暫定這個路徑是 /usr/local/file,那麼需要經過如下幾個步驟:
- 首先,系統會确定根目錄,它通常使用 2 号 i -node ,也就是索引 2 節點,因為索引節點 1 是 ext2 /3/4 文件系統上的壞塊索引節點。系統會将一項放在 dentry 緩存中,以應對将來對根目錄的查找。
- 然後,在根目錄中查找字符串 usr,得到 /usr 目錄的 i - node 節點号。/usr 的 i - node 同樣也進入 dentry 緩存。然後節點被取出,并從中解析出磁盤塊,這樣就可以讀取 /usr 目錄并查找字符串 local 了。一旦找到這個目錄項,目錄 /usr/local 的 i - node 節點就可以從中獲得。有了 /usr/local 的 i - node 節點号,就可以讀取 i - node 并确定目錄所在的磁盤塊。最後,從 /usr/local 目錄查找 file 并确定其 i - node 節點呢号。
如果文件存在,那麼系統會提取 i - node 節點号并把它作為索引在 i - node 節點表中定位相應的 i - node 節點并裝入内存。i - node 被存放在 i - node 節點表(i-node table) 中,節點表是一個内核數據結構,它會持有當前打開文件和目錄的 i - node 節點号。下面是一些 Linux 文件系統支持的 i - node 數據結構。
屬性
字節
描述
Mode
2
文件屬性、保護位、setuid 和 setgid 位
Nlinks
2
指向 i - node 節點目錄項的數目
Uid
2
文件所有者的 UID
Gid
2
文件所有者的 GID
Size
4
文件字節大小
Addr
60
12 個磁盤塊以及後面 3 個間接塊的地址
Gen
1
每次重複使用 i - node 時增加的代号
Atime
4
最近訪問文件的時間
Mtime
4
最近修改文件的時間
Ctime
4
最近更改 i - node 的時間
現在我們來一起探讨一下文件讀取過程,還記得 read 函數是如何調用的嗎?
brn = read(fd,buffer,nbytes);
當内核接管後,它會從這三個參數以及内部表與用戶有關的信息開始。内部表的其中一項是文件描述符數組。文件描述符數組用文件描述符 作為索引并為每一個打開文件保存一個表項。
文件是和 i - node 節點号相關的。那麼如何通過一個文件描述符找到文件對應的 i - node 節點呢?
這裡使用的一種設計思想是在文件描述符表和 i - node 節點表之間插入一個新的表,叫做 打開文件描述符(open-file-description table)。文件的讀寫位置會在打開文件描述符表中存在,如下圖所示
我們使用 shell 、P1 和 P2 來描述一下父進程、子進程、子進程的關系。Shell 首先生成 P1,P1 的數據結構就是 Shell 的一個副本,因此兩者都指向相同的打開文件描述符的表項。當 P1 運行完成後,Shell 的文件描述符仍會指向 P1 文件位置的打開文件描述。然後 Shell 生成了 P2,新的子進程自動繼承文件的讀寫位置,甚至 P2 和 Shell 都不知道文件具體的讀寫位置。
上面描述的是父進程和子進程這兩個 相關 進程,如果是一個不相關進程打開文件時,它将得到自己的打開文件描述符表項,以及自己的文件讀寫位置,這是我們需要的。
因此,打開文件描述符相當于是給相關進程提供同一個讀寫位置,而給不相關進程提供各自私有的位置。
i - node 包含三個間接塊的磁盤地址,它們每個指向磁盤塊的地址所能夠存儲的大小不一樣。
Linux Ext4 文件系統為了防止由于系統崩潰和電源故障造成的數據丢失,ext2 系統必須在每個數據塊創建之後立即将其寫入到磁盤上,磁盤磁頭尋道操作導緻的延遲是無法讓人忍受的。為了增強文件系統的健壯性,Linux 依靠日志文件系統,ext3 是一個日志文件系統,它在 ext2 文件系統的基礎之上做了改進,ext4 也是 ext3 的改進,ext4 也是一個日志文件系統。ext4 改變了 ext3 的塊尋址方案,從而支持更大的文件和更大的文件系統大小。下面我們就來描述一下 ext4 文件系統的特性。
具有記錄的文件系統最基本的功能就是記錄日志,這個日志記錄了按照順序描述所有文件系統的操作。通過順序寫出文件系統數據或元數據的更改,操作不受磁盤訪問期間磁盤頭移動的開銷。最終,這個變更會寫入并提交到合适的磁盤位置上。如果這個變更在提交到磁盤前文件系統宕機了,那麼在重啟期間,系統會檢測到文件系統未正确卸載,那麼就會遍曆日志并應用日志的記錄來對文件系統進行更改。
Ext4 文件系統被設計用來高度匹配 ext2 和 ext3 文件系統的,盡管 ext4 文件系統在内核數據結構和磁盤布局上都做了變更。盡管如此,一個文件系統能夠從 ext2 文件系統上卸載後成功的挂載到 ext4 文件系統上,并提供合适的日志記錄。
日志是作為循環緩沖區管理的文件。日志可以存儲在與主文件系統相同或者不同的設備上。日志記錄的讀寫操作會由單獨的 JBD(Journaling Block Device) 來扮演。
JBD 中有三個主要的數據結構,分别是 log record(日志記錄)、原子操作和事務。一個日志記錄描述了一個低級别的文件系統操作,這個操作通常導緻塊内的變化。因為像是 write 這種系統調用會包含多個地方的改動 --- i - node 節點,現有的文件塊,新的文件塊和空閑列表等。相關的日志記錄會以原子性的方式分組。ext4 會通知系統調用進程的開始和結束,以此使 JBD 能夠确保原子操作的記錄都能被應用,或者一個也不被應用。最後,主要從效率方面考慮,JBD 會視原子操作的集合為事務。一個事務中的日志記錄是連續存儲的。隻有在所有的變更一起應用到磁盤後,日志記錄才能夠被丢棄。
由于為每個磁盤寫出日志的開銷會很大,所以 ext4 可以配置為保留所有磁盤更改的日志,或者僅僅保留與文件系統元數據相關的日志更改。僅僅記錄元數據可以減少系統開銷,提升性能,但不能保證不會損壞文件數據。其他的幾個日志系統維護着一系列元數據操作的日志,例如 SGI 的 XFS。
/proc 文件系統另外一個 Linux 文件系統是 /proc (process) 文件系統
它的主要思想來源于貝爾實驗室開發的第 8 版的 UNIX,後來被 BSD 和 System V 采用。
然而,Linux 在一些方面上對這個想法進行了擴充。它的基本概念是為系統中的每個進程在 /proc 中創建一個目錄。目錄的名字就是進程 PID,以十進制數進行表示。例如,/proc/1024 就是一個進程号為 1024 的目錄。在該目錄下是進程信息相關的文件,比如進程的命令行、環境變量和信号掩碼等。事實上,這些文件在磁盤上并不存在磁盤中。當需要這些信息的時候,系統會按需從進程中讀取,并以标準格式返回給用戶。
許多 Linux 擴展與 /proc 中的其他文件和目錄有關。它們包含各種各樣的關于 CPU、磁盤分區、設備、中斷向量、内核計數器、文件系統、已加載模塊等信息。非特權用戶可以讀取很多這樣的信息,于是就可以通過一種安全的方式了解系統情況。
NFS 網絡文件系統從一開始,網絡就在 Linux 中扮演了很重要的作用。下面我們會探讨一下 NFS(Network File System) 網絡文件系統,它在現代 Linux 操作系統的作用是将不同計算機上的不同文件系統鍊接成一個邏輯整體。
NFS 架構NFS 最基本的思想是允許任意選定的一些客戶端和服務器共享一個公共文件系統。在許多情況下,所有的客戶端和服務器都會在同一個 LAN(Local Area Network) 局域網内共享,但是這并不是必須的。也可能是下面這樣的情況:如果客戶端和服務器距離較遠,那麼它們也可以在廣域網上運行。客戶端可以是服務器,服務器可以是客戶端,但是為了簡單起見,我們說的客戶端就是消費服務,而服務器就是提供服務的角度來聊。
每一個 NFS 服務都會導出一個或者多個目錄供遠程客戶端訪問。當一個目錄可用時,它的所有子目錄也可用。因此,通常整個目錄樹都會作為一個整體導出。服務器導出的目錄列表會用一個文件來維護,這個文件是 /etc/exports,當服務器啟動後,這些目錄可以自動的被導出。客戶端通過挂載這些導出的目錄來訪問它們。當一個客戶端挂載了一個遠程目錄,這個目錄就成為客戶端目錄層次的一部分,如下圖所示。
在這個示例中,一号客戶機挂載到服務器的 bin 目錄下,因此它現在可以使用 shell 訪問 /bin/cat 或者其他任何一個目錄。同樣,客戶機 1 也可以挂載到 二号服務器上從而訪問 /usr/local/projects/proj1 或者其他目錄。二号客戶機同樣可以挂載到二号服務器上,訪問路徑是 /mnt/projects/proj2。
從上面可以看到,由于不同的客戶端将文件挂載到各自目錄樹的不同位置,同一個文件在不同的客戶端有不同的訪問路徑和不同的名字。挂載點一般通常在客戶端本地,服務器不知道任何一個挂載點的存在。
NFS 協議由于 NFS 的協議之一是支持 異構 系統,客戶端和服務器可能在不同的硬件上運行不同的操作系統,因此有必要在服務器和客戶端之間進行接口定義。這樣才能讓任何寫一個新客戶端能夠和現有的服務器一起正常工作,反之亦然。
NFS 就通過定義兩個客戶端 - 服務器協議從而實現了這個目标。協議就是客戶端發送給服務器的一連串的請求,以及服務器發送回客戶端的相應答複。
第一個 NFS 協議是處理挂載。客戶端可以向服務器發送路徑名并且請求服務器是否能夠将服務器的目錄挂載到自己目錄層次上。因為服務器不關心挂載到哪裡,因此請求不會包含挂載地址。如果路徑名是合法的并且指定的目錄已經被導出,那麼服務器會将文件 句柄 返回給客戶端。
文件句柄包含唯一标識文件系統類型,磁盤,目錄的i節點号和安全性信息的字段。
随後調用讀取和寫入已安裝目錄或其任何子目錄中的文件,都将使用文件句柄。
當 Linux 啟動時會在多用戶之前運行 shell 腳本 /etc/rc 。可以将挂載遠程文件系統的命令寫入該腳本中,這樣就可以在允許用戶登陸之前自動挂載必要的遠程文件系統。大部分 Linux 版本是支持自動挂載的。這個特性會支持将遠程目錄和本地目錄進行關聯。
相對于手動挂載到 /etc/rc 目錄下,自動挂載具有以下優勢
- 如果列出的 /etc/rc 目錄下出現了某種故障,那麼客戶端将無法啟動,或者啟動會很困難、延遲或者伴随一些出錯信息,如果客戶根本不需要這個服務器,那麼手動做了這些工作就白費了。
- 允許客戶端并行的嘗試一組服務器,可以實現一定程度的容錯率,并且性能也可以得到提高。
另一方面,我們默認在自動挂載時所有可選的文件系統都是相同的。由于 NFS 不提供對文件或目錄複制的支持,用戶需要自己确保這些所有的文件系統都是相同的。因此,大部分的自動挂載都隻應用于二進制文件和很少改動的隻讀的文件系統。
第二個 NFS 協議是為文件和目錄的訪問而設計的。客戶端能夠通過向服務器發送消息來操作目錄和讀寫文件。客戶端也可以訪問文件屬性,比如文件模式、大小、上次修改時間。NFS 支持大多數的 Linux 系統調用,但是 open 和 close 系統調用卻不支持。
不支持 open 和 close 并不是一種疏忽,而是一種刻意的設計,完全沒有必要在讀一個文件之前對其進行打開,也沒有必要在讀完時對其進行關閉。
NFS 使用了标準的 UNIX 保護機制,使用 rwx 位來标示所有者(owner)、組(groups)、其他用戶 。最初,每個請求消息都會攜帶調用者的 groupId 和 userId,NFS 會對其進行驗證。事實上,它會信任客戶端不會發生欺騙行為。可以使用公鑰密碼來創建一個安全密鑰,在每次請求和應答中使用它驗證客戶端和服務器。
NFS 實現即使客戶端和服務器的代碼實現是獨立于 NFS 協議的,大部分的 Linux 系統會使用一個下圖的三層實現,頂層是系統調用層,系統調用層能夠處理 open 、 read 、 close 這類的系統調用。在解析和參數檢查結束後調用第二層,虛拟文件系統 (VFS) 層。
VFS 層的任務是維護一個表,每個已經打開的文件都在表中有一個表項。VFS 層為每一個打開的文件維護着一個虛拟i節點,簡稱為 v - node。v 節點用來說明文件是本地文件還是遠程文件。如果是遠程文件的話,那麼 v - node 會提供足夠的信息使客戶端能夠訪問它們。對于本地文件,會記錄其所在的文件系統和文件的 i-node ,因為現代操作系統能夠支持多文件系統。雖然 VFS 是為了支持 NFS 而設計的,但是現代操作系統都會使用 VFS,而不管有沒有 NFS。
如果文章對你有幫助,希望各位小夥伴們交出你的三連呀!
,