1. 程式人生 > >淺談檔案描述符及檔案系統

淺談檔案描述符及檔案系統

之前在講IO操作的時候,其中系統級IO中的open,write,read,close都用到了檔案描述符(file descriptor),其中open的返回值為檔案描述符,write、read和close都是在傳參的時候需要傳檔案的檔案描述符。

那麼,檔案描述符到底是個什麼樣的概念呢?
簡單地來說,檔案描述符就是一個小整數,它是非負數,最小值為0,作業系統核心利用檔案描述符來訪問檔案。而實際上,它是一個索引,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。當我們開啟一個檔案時,核心要在記憶體中建立資料結構來描述目標檔案,於是便有了我們的file結構體,它表示一個已經開啟的檔案物件。而當程序執行open系統呼叫介面的時候,我們需要讓程序和檔案關聯起來,每個程序都有一個檔案指標*files,它指向一張檔案結構表。這張表裡最重要的東西就是一個指標陣列,裡面存放的都是指向各種檔案的指標。而檔案描述符,就是這個指標陣列的下標。
這裡寫圖片描述

從圖中可以看到,系統在開啟一個檔案的時候,會先預設開啟三個檔案標準輸入,標準輸出,標準錯誤,它們三個分別佔據了這個檔案描述符陣列的前三個,也就是下標0,1,2。這樣我們新開啟一個檔案,這個檔案的檔案描述符只有被存放到3中了,那麼一定是每次開啟第一個檔案,它的檔案描述符都是3嗎?我們通過一段程式碼來看一下。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main() { //close(0); close(2); int fd=open("close1",O_RDONLY); if(fd<0) { perror("open!\n"); return 1; } printf("%d\n",fd); close(fd); return 0; }

這裡我們在程式一開始就關閉了檔案描述符為0或2的檔案(注意不能關1,當然其實也可以,只不過這樣我們的結果就出不到終端視窗了,因為1是標準輸出),看一下我們的結果
這裡寫圖片描述

這裡寫圖片描述
我們可以看到,我們關了0,那麼開啟的檔案的檔案描述符即為0,;我們關了2,那麼開啟的檔案的檔案描述符即為2。由此可見,檔案描述符的分配規則是,找到當前未被使用的最小的一個下標,作為新開啟檔案的檔案描述符。

關於輸出重定向的問題
之前我們說不能關閉1,因為會看不到結果,那麼我們就要關閉1,會有什麼結果呢?看程式碼

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    //close(0);
    //close(2);
    umask(0);
    close(1);
    int fd=open("close1",O_WRONLY|O_CREAT,0666);
    if(fd<0)
    {
        perror("open!\n");
        return 1;
    }
    printf("%d\n",fd);
    fflush(stdout);

    close(fd);
    return 0;
}

這裡寫圖片描述
我們可以看到,本來是要輸出到顯示器上的內容,輸出到了我們的檔案close1當中,並且,該檔案的fd為1,也就是我們關閉的標準輸出的原始檔案描述符。這叫做輸出重定向,下面我們來看一下它的本質。
這裡寫圖片描述

printf函式的輸出結果一般是往標準輸出輸出的。但stdout在底層尋找檔案的時候,還是找的fd=1的檔案。這裡原來fd=1的檔案是stdout,但此時我們改成了close1,所以輸出的任何資訊都會往該檔案中寫入,實現了輸出重定向。

在這裡順便提下能夠輸出到顯示器的函式printf,fwrite,write進行重定向時的區別
printf和fwrite是C庫函式,它們兩個自帶使用者級別的緩衝區。當寫入普通檔案時,緩衝方式為全緩衝;當寫入顯示器時,緩衝方式為行緩衝。當進行了輸出重定向後,在緩衝區中的資料不會被立即重新整理,當程序退出的時候,會統一重新整理。而write屬於系統呼叫介面,它沒有緩衝區。下面給一份程式碼來感受一下它們的區別:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
    char* msg1="pringf!\n";
    char* msg2="fwrite!\n";
    char* msg3="write!\n";

    printf("%s",msg1);
    fwrite(msg2,1,strlen(msg2),stdout);
    write(1,msg3,strlen(msg3));

    fork();
    return 0;
}

輸入結果如下
這裡寫圖片描述

當我們把輸出結果重定向到一個file檔案中之後,再來看看結果
這裡寫圖片描述

正如之前所說,在發生輸出重定向之時,printf以及fwrite中的緩衝區並沒有立即重新整理,即便是呼叫了fork。fork之後,子程序會寫時拷貝一份父程序的資料,所以當父程序準備重新整理的時候,子程序也就有了同樣的一份資料,便有了如上的兩份資料。write並沒有緩衝區,它是直接輸出的,而且是第一個輸出,因為別的都還在緩衝。

關於檔案系統

Linux中有一個很重要的理念,即“一切皆檔案”,任何目錄,程序,命令,裝置等,歸根結底在Linux看來都是檔案。它們被分成若干個基本儲存單元,存放在磁碟的不同實體地址上,並具有特定的許可權。

那麼既然任何東西都可以被看成是檔案,作業系統就需要來管理檔案,而檔案系統,就是作業系統來管理檔案的方式。簡單的說,作業系統通過檔案系統來管理分佈在磁碟上的各個檔案。

通過stat指令可以檢視檔案的狀態資訊
這裡寫圖片描述
上圖中的Links表示該目錄下有的連結數,如果該檔案是目錄,Links表示該目錄還有多少目錄(包括隱藏目錄.和..);如果該檔案是普通檔案,Links表示指向該檔案的硬連結數加上它本身(即如果有一個硬連結指向該檔案,那麼該檔案的Links就是2)。該數也可以通過stat結構體的st_nlink欄位獲得

這裡我們需要解釋幾個概念
磁碟 :存放檔案的裝置,如下圖的/dev/sda
這裡寫圖片描述
分割槽:磁碟上劃分出來的空間,如下圖的/dev/sda1
這裡寫圖片描述

Block :塊,是系統檔案讀寫和存放資料的最小單位。每個Block裡只能存放一個檔案的資料。如果檔案大於Block大小,則該檔案會佔用多個Block;如果檔案小於Block大小,他也會完全佔用該Block,剩餘的空間也不會再被使用(磁碟空間會浪費)。因此,在對Block大小進行設定的時候要考慮當前系統存放的資料的特點,如果有很多小於Block大小的檔案,格式化的時候卻把Block設定成較大,這樣會造成很多的磁碟空間浪費;反之,如果系統裡以大檔案居多,這時卻把Block設定成較小,固然不會浪費磁碟空間,但也導致Block較多,影響讀寫效能。

Sector :扇區,是磁碟控制器每次對磁碟進行讀寫的最小單位。扇區是最小的物理儲存的單位,一般為512位元組,由磁碟生產商確定,使用者改不了。磁碟讀到的Sectoer資料會先放在磁碟的快取裡,直到整個Block的所有Sector都快取到了才會傳輸給記憶體,交由檔案系統處理。

超級塊(super Block):也是一個block,用來記錄檔案系統的整體資訊,包括 inode/block總量,使用量,剩餘量,以及檔案系統的格式與相關資訊等

indoe :這是一個非常重要的概念,是Linux非常厲害的一個設計。它將檔案的屬性,許可權等和檔案的資料分開存放。
這裡寫圖片描述

下面我們來看一下作業系統是如何將檔案的屬性和資料分開來存放的。
我們先建立一個新的檔案,如下圖所示(ls -i可以顯示檔案對應的i節點號)
這裡寫圖片描述

目錄:也是檔案的一種,自然也有自己的inode和block,目錄的inode主要記錄目錄的屬性,許可權等,目錄的block主要記錄目錄下的檔名和對應的inode
這裡寫圖片描述

目錄有以下屬性:

1.建立的時候會分配一個inode給目錄,如果目錄是空的,則不佔用block,如果目錄下檔案過多,可能會佔用多個block

2.訪問檔案時,先訪問檔案所在目錄的inode,驗證是否有許可權,如果有則訪問對應父目錄的block,獲得該檔案對應inode,驗證是否有該檔案的許可權,若有則訪問該檔案的block

3.父目錄的inode從父目錄的父目錄獲得!這樣層層遞推,Linux對所有檔案的訪問都是從最上層的根目錄開始的

4.根目錄的inode是固定的,一般是2號inode,根目錄的上層目錄就是他自己

儲存一個檔案的過程
1. 核心先找到一個空閒的i節點,這裡是137,把檔案屬性資訊記錄到其中。
2. 核心在資料區找到幾個空閒的磁碟塊(假設需要三個磁碟塊分別是300,500,800),將核心緩衝區的第一塊資料複製到第一塊磁碟塊(300),往後以此類推,直到存完資料
3. 檔案內容按順序300,500,800存放。核心在inode上的磁碟分佈區記錄了上述記錄。
4. 新的檔名為pigff,核心將入口(137,pigff)新增到目錄檔案。檔名和inode之間的對應關係將檔名和檔案的內容及其屬性連線起來(如上面目錄的圖)。

從磁碟上讀取一個檔案的過程

  1. 程式呼叫read函式請求讀檔案
  2. read函式根據給的檔案描述符引數,拿到對應檔案的檔案表項,在檔案表項中拿到目錄項模組,找到對應檔案的inode
  3. 在inode中,根據檔案表項中的偏移量計算出要讀取的頁
  4. 通過inode找到檔案對應的address_space(該結構體是用來管理檔案(struct inode)對映到記憶體的頁面(struct page)的
  5. 在address_space中訪問該檔案的頁快取樹,查詢對應的頁快取節點:如果頁快取命中,那麼直接返回檔案內容;如果頁快取缺失,那麼發生缺頁異常,將會建立一個快取頁,通過inode找到檔案該頁的磁碟地址,讀取相應的頁填充該快取頁;然後重新查詢快取頁
  6. 檔案內容讀取成功

往檔案裡寫檔案的過程

  1. 程式呼叫write函式請求寫檔案
  2. write函式根據給的檔案描述符引數,拿到對應檔案的檔案表項,在檔案表項中拿到目錄項模組,找到對應檔案的inode
  3. 在inode中,根據檔案表項中的偏移量計算出要往檔案中寫的頁
  4. 通過inode找到檔案對應的address_space
  5. 在address_space中訪問該檔案的頁快取樹,查詢對應的頁快取節點:
    a). 如果頁快取命中,那麼直接把檔案內容修改更新在頁快取的頁中,寫檔案就結束了;這時候檔案修改位於頁快取,並沒有寫回到磁碟檔案中去
    b). 如果頁快取缺失,那麼發生缺頁異常,將會建立一個快取頁,通過inode找到檔案該頁的磁碟地址,讀取相應的頁填充該快取頁;然後重新查詢快取頁

  6. 一個頁快取中的頁如果被修改,那麼會被標記成髒頁。髒頁需要寫回到磁碟中的檔案塊。有兩種方式可以把髒頁寫回磁碟:
    a). 手動呼叫sync()或者fsync()系統呼叫把髒頁寫回;
    b). pdflush程序會定時把髒頁寫回到磁碟;
    注意: 髒頁不能被置換出記憶體,如果髒頁正在被寫回,那麼會被設定寫回標記,這時候該頁就被上鎖,其他寫請求被阻塞直到鎖釋放。

實際上讀寫檔案發生了兩次拷貝,從磁碟拷貝到頁快取,再從頁快取拷貝到使用者空間。這期間都發生了系統呼叫,要從使用者態切換到核心態。

理解硬連結
事實上,真正找到磁碟上的檔案的並不是檔名,而是inode。我們通過硬連結可以讓多個檔案對應於同一個inode。目錄沒有硬連結。
這裡寫圖片描述
這裡寫圖片描述
可以看到第一列,兩個檔案對應的inode都是137,它們被稱為指向檔案的硬連結。核心記錄了這個連結數,inode137對應的硬連結數為2。每次新建立一個檔案的時候在刪除檔案的時候,我們幹了兩件事:1.將目錄中對應的記錄刪除。2.將檔案的硬連結數-1,如果為0,則將對應的磁碟釋放。

在用mv命令為一個檔案更名的時候,該檔案的實際內容並未移動,只需要構造一個指向現有i節點的新目錄項,並解除與舊目錄項的連結。

以便於簡單理解,我們可以把硬連結理解為C++當中的引用。即硬連結的兩個檔案,實際上都是一個檔案,tmp可以當作是pigff的別名。當我們修改其中一個檔案的內容時,另一個檔案也會隨之修改。上圖可以看到兩者出了名字其餘資訊全部相同。
這裡寫圖片描述
如上圖,我們把ls的輸出結果重定向到了tmp檔案中,tmp檔案的大小從之間的0變到了82,而同時pigff檔案的大小也變為了82。

理解軟連結
硬連結是通過inode引用另外一個檔案,軟連結是通過名字引用另外一個檔案。
這裡寫圖片描述
如圖我們在ln命令加一個-s選項,便是軟連結,他有一個指向關係,從顯示結果的第一列我們可以看到 lyb檔案的型別是一個連結檔案“l”。

簡單地理解,我們可以把軟連結理解為Windows下常用的快捷方式,或者是複製了一份檔案從而變成一份新的檔案。當我們修改新的檔案的內容的時候,原檔案的內容不會隨之修改。

動態庫和靜態庫

  • 靜態庫(.a):程式在編譯連結的時候把庫的程式碼連結到可執行檔案中。程式執行的時候將不再需要靜態庫。我們用的最多的函式printf便是在連結的時候連結到可執行檔案當中的。
  • 動態庫(.so):程式在執行的時候才去連結動態庫的程式碼,多個程式共享使用庫的程式碼。
  • 一個與動態庫連結的可執行檔案僅僅包含它用到的函式入口地址的一個表,而不是外部函式所在目標檔案的整個機器碼。
  • 在可執行檔案開始執行之前,外部函式的機器碼由作業系統從磁碟上的該動態庫複製到記憶體中,這個過程稱為動態連結。
  • 動態庫可以在多個程式間共享,所以動態連結使得可執行檔案更小,節省了磁碟空間。作業系統採用虛擬記憶體機制允許實體記憶體中的一份動態庫被要用到該庫的所有程序公用,節省了記憶體和磁碟空間。

下面我們通過一段程式來生成靜態庫和動態庫
add.h
這裡寫圖片描述
add.c
這裡寫圖片描述
sub.h
這裡寫圖片描述
sub.c
這裡寫圖片描述
main.c
這裡寫圖片描述
這裡寫圖片描述

生成靜態庫
這裡寫圖片描述
ar是gnu歸檔工具,rc表示替換或建立
t:列出靜態庫中檔案
v:詳細資訊

這裡寫圖片描述
-L:指定庫路徑
-l: 指定庫名

如上圖,我們可以看到程式執行處了正確的結果,此時我們刪除靜態庫,程式依然可以執行成功。
這裡寫圖片描述

生成動態庫

  • shared:表示生成共享庫格式
  • fPIC:產生位置無關碼
  • 庫名規則:libxxx.so

    這裡寫圖片描述

使用動態庫
編譯選項

  • L:連結庫所在路徑
  • l:連結動態庫,只要庫名即可(去掉lib以及版本號)
gcc main.c -o main -L . -lmymath

執行動態庫

  • 拷貝.so檔案到系統共享庫路徑下,一般是/usr/lib
  • 更改LD_LIBRARY_PATH
    這裡寫圖片描述

  • ldconfig配置/etc/ld.so.conf.d/ , ldconfig更新
    在/etc/ld.so.conf.d目錄中建立一個my.conf,裡面只有一句話,就是剛才建立的動態庫的路徑/usr/lib/libmymath.so,儲存退出後執行ldconfig。