ucore文件系統詳解


最近一直在mooc上學習清華大學的操作系統課程,也算是復習下基本概念和原理,為接下來的找工作做准備。
每次深入底層源碼都讓我深感操作系統實現的瑣碎,即使像ucore這樣簡單的kernel也讓我煩躁不已,文件系統相比於中斷子系統、調度子系統、進程管理子系統等等,要復雜很多,因此被稱為文件系統而不是文件子系統。參看網絡上的資料有時會增加我的困惑,很多人只是簡單轉載,很多細節描述的很模糊,實驗環境也各不相同,最終很難深入理解文件系統的本質,參考源碼我覺得有點像從三維世界進入到二維世界,一切變得清晰但是卻需要消耗更多精力,我覺得這樣做是值得的,因為如果不能深入而只是泛泛的理解,對於操作系統這樣偏向於工程學的東西來說意義不大,本文的研究內容主要是根據清華大學陳渝副教授、向勇副教授在mooc上所講,以及實驗參考書的內容和我自己在系統上打的log驗證過的,如果有讀者發現錯誤還請批評指正。
本篇博客希望盡可能照顧到初學者,但是有些簡單原理默認讀者已經掌握,很多細節不會展開敘述,讀者請自行Google或者參看Intel Development Manual,實驗用的源碼來自於清華大學教學操作系統,讀者可在github上搜索ucore_os_lab。
附上ucore的實驗參考書Ucore Lab Document.

綜述

最初實現文件系統是為了實現對磁盤數據的高效管理,使得用戶可以高效、快速的讀取磁盤上的數據內容。其實我個人覺得文件系統就是操作系統內核和外設之間的一套輸入輸出協議,我們所采用的算法和索引結點的建立方式都是為了根據實際應用情況所設計的一套高效的協議。在實現的過程中,我們還要將文件系統進行分層抽象,也就是實現我們的虛擬文件系統,虛擬文件系統有對上和對下兩個方面,對上是為了向上層應用提供一套通用的訪問接口,可以用相同的方式去訪問磁盤上的文件(包括目錄,后面會介紹)和外設;對下兼容不同的文件系統和外設。虛擬文件系統的層次和依賴關系如下圖所示:

對照上面的層次我們再大致介紹一下文件系統的訪問處理過程,加深對文件系統的總體理解。假如應用程序操作文件(打開/創建/刪除/讀寫),首先需要通過文件系統的通用文件系統訪問接口層給用戶空間提供的訪問接口進入文件系統內部,接着由文件系統抽象層把訪問請求轉發給某一具體文件系統(比如SFS文件系統),具體文件系統(Simple FS文件系統層)把應用程序的訪問請求轉化為對磁盤上的block的處理請求,並通過外設接口層交給磁盤驅動例程來完成具體的磁盤操作。結合用戶態寫文件函數write的整個執行過程,我們可以比較清楚地看出ucore文件系統架構的層次和依賴關系。

總體設計

ucore的文件系統模型源於Havard的OS161文件系統和Linux文件系統。但其實這兩者都源於UNIX文件系統設計。UNIX提出了四個文件系統抽象概念:文件(file)、目錄項(descriptor entry,簡寫為dentry)、索引節點(inode)和安裝點(mount point)。

  • 文件:UNIX文件中的內容可理解為是一有序字節buffer,文件都有一個方便應用程序識別的文件名稱(也稱文件路徑名)。典型的文件操作有讀、寫、創建和刪除等。
  • 目錄項:目錄項不是目錄,而是目錄的組成部分。在UNIX中目錄被看作一種特定的文件,而目錄項是文件路徑中的一部分。如一個文件路徑名是“/test/testfile”,則包含的目錄項為:根目錄“/”,目錄“test”和文件“testfile”,這三個都是目錄項。一般而言,目錄項包含目錄項的名字(文件名或目錄名)和目錄項的索引節點(見下面的描述)位置。
  • 索引節點:UNIX將文件的相關元數據信息(如訪問控制權限、大小、擁有者、創建時間、數據內容等等信息)存儲在一個單獨的數據結構中,該結構被稱為索引節點。
  • 安裝點:在UNIX中,文件系統被安裝在一個特定的文件路徑位置,這個位置就是安裝點。所有的已安裝文件系統都作為根文件系統樹中的葉子出現在系統中。
    上述抽象概念形成了UNIX文件系統的邏輯數據結構,並需要通過一個具體文件系統的架構設計與實現把上述信息映射並儲存到磁盤介質上。一個具體的文件系統需要在磁盤布局也實現上述抽象概念。比如文件元數據信息存儲在磁盤塊中的索引節點上。當文件被載如內存時,內核需要使用磁盤塊中的索引點來構造內存中的索引節點。
    ucore模仿了UNIX的文件系統設計,ucore的文件系統架構主要由四部分組成:
  • 通用文件系統訪問接口層:該層提供了一個從用戶空間到文件系統的標准訪問接口。這一層訪問接口讓應用程序能夠通過一個簡單的接口獲得ucore內核的文件系統服務。
  • 文件系統抽象層:向上提供一個一致的接口給內核其他部分(文件系統相關的系統調用實現模塊和其他內核功能模塊)訪問。向下提供一個同樣的抽象函數指針列表和數據結構屏蔽不同文件系統的實現細節。
  • Simple FS文件系統層:一個基於索引方式的簡單文件系統實例。向上通過各種具體函數實現以對應文件系統抽象層提出的抽象函數。向下訪問外設接口
  • 外設接口層:向上提供device訪問接口屏蔽不同硬件細節。向下實現訪問各種具體設備驅動的接口,比如disk設備接口/串口設備接口/鍵盤設備接口等。

相關數據結構

在文件系統初始化時,主要完成三個函數:

    void fs_init(void) {
    vfs_init();
    dev_init();
    sfs_init();
}

它們的具體實現如下:

    void vfs_init(void) {
    sem_init(&bootfs_sem, 1);
    vfs_devlist_init();
}
    void dev_init(void) {
    init_device(stdin);
    init_device(stdout);
    init_device(disk0);
}
    void sfs_init(void) {
    int ret;
    if ((ret = sfs_mount("disk0")) != 0) {
        panic("failed: sfs: sfs_mount: %e.\n", ret);
    }
}

對於vfs_init,它只是完成了對vfs訪問的信號量和devlist的初始化。dev_init則完成了對設備的初始化,這里的stdin代表輸入設備,即鍵盤,stdout代表輸出設備,包括UART串口和顯示器,disk0代表磁盤,我們以外設stdin作為例子進行講述,stdout和stdin類似,disk0和stdin有一些不同,會在下文進行對比。首先,這里調用了init_device(stdin),它是一個宏,如下所示:

#define init_device(x)                                  \
    do {                                                \
        extern void dev_init_##x(void);                 \
        dev_init_##x();                                 \
    } while (0)

最后會調用到dev_init_stdin()

void dev_init_stdin(void) {
    struct inode *node;
    if ((node = dev_create_inode()) == NULL) {
        panic("stdin: dev_create_node.\n");
    }
    cprintf("dev_init_stdin is called\n");
    stdin_device_init(vop_info(node, device));

    int ret;
    if ((ret = vfs_add_dev("stdin", node, 0)) != 0) {
        panic("stdin: vfs_add_dev: %e.\n", ret);
    }
}

在dev_init_stdin中主要涉及兩個數據結構,分別是struct inode和device,它們的數據結構如下所示:

struct inode {
    union {
        struct device __device_info;
        struct sfs_inode __sfs_inode_info;
    } in_info;
    enum {
        inode_type_device_info = 0x1234,
        inode_type_sfs_inode_info,
    } in_type;
    int ref_count;
    int open_count;
    struct fs *in_fs;
    const struct inode_ops *in_ops;
};
struct device {
    size_t d_blocks;
    size_t d_blocksize;
    int (*d_open)(struct device *dev, uint32_t open_flags);
    int (*d_close)(struct device *dev);
    int (*d_io)(struct device *dev, struct iobuf *iob, bool write);
    int (*d_ioctl)(struct device *dev, int op, void *data);
};

這里的關鍵函數是stdin_device_init(vop_info(node, device)),它完成設置inode為設備文件,初始化設備文件。其中vop_info是一個宏,實現如下:

#define vop_info(node, type)                                        __vop_info(node, type)
#define __vop_info(node, type)                                      \
    ({                                                              \
        struct inode *__node = (node);                              \
        assert(__node != NULL && check_inode_type(__node, type));   \
        &(__node->in_info.__##type##_info);                         \
     })

它完成返回in_info這個聯合體里device的地址,然后交個stdin_device_init處理,實現如下:

static void
stdin_device_init(struct device *dev) {
    dev->d_blocks = 0;
    dev->d_blocksize = 1;
    dev->d_open = stdin_open;
    dev->d_close = stdin_close;
    dev->d_io = stdin_io;
    dev->d_ioctl = stdin_ioctl;

    p_rpos = p_wpos = 0;
    wait_queue_init(wait_queue);
}

inode代表的是一個抽象意義的文件,根據in_info和in_type的值的不同,它既可以表示文件也可以表示外設,open_count表示一個文件被進程打開的次數,當open_count=0時我們可以在kernel移除這個inode結點。這個inode是系統管理文件用的,因此用戶層的程序不需要關心這個數據結構。device這個數據結構只有當inode表示設備時才會有用,其中d_blocks表示設備占據的數據塊個數,d_blocksize表示數據占據的數據塊大小,相應的四個操作也很簡單,直接翻譯過來就能理解,這里不再詳述。需要注意的是,stdin相對於stdout多了一個輸入緩沖區,需要額外的兩個指針p_rpos,p_wpos分別記錄當前讀的位置和寫的位置,當p_rpos < p_wpos時,說明當前有從鍵盤輸入到緩沖區的數據但是還沒有讀到進程里,需要喚醒進程從緩沖區進行讀操作,當p_rpos=p_wpos而進程發起讀的系統調用時(如調用c庫的scanf),這時需要阻塞進程,等待鍵盤輸入時產生中斷喚醒對應進程。
disk0的初始化過程其實和stdin和stdout幾乎一樣,只是在第三步sfs_init的過程中需要執行mount操作。sys_mount的調用過程如下:

int sfs_mount(const char *devname) {
    return vfs_mount(devname, sfs_do_mount);
}
int
vfs_mount(const char *devname, int (*mountfunc)(struct device *dev, struct fs **fs_store)) {
    int ret;
    lock_vdev_list();
    vfs_dev_t *vdev;
    if ((ret = find_mount(devname, &vdev)) != 0) {
        goto out;
    }
    if (vdev->fs != NULL) {
        ret = -E_BUSY;
        goto out;
    }
    assert(vdev->devname != NULL && vdev->mountable);

    struct device *dev = vop_info(vdev->devnode, device);
    if ((ret = mountfunc(dev, &(vdev->fs))) == 0) {
        assert(vdev->fs != NULL);
        cprintf("vfs: mount %s.\n", vdev->devname);
    }
out:
    unlock_vdev_list();
    return ret;
}

其中這里面最重要的就是對回調函數sfs_do_mount的調用,sfs_do_mount主要完成對struct sfs數據結構的初始化,這里的sfs是simple file system的縮寫,本文討論的ucore目前只支持這一種文件系統,該數據結構的實現如下:

struct sfs_fs {
    struct sfs_super super;                         /* on-disk superblock */
    struct device *dev;                             /* device mounted on */
    struct bitmap *freemap;                         /* blocks in use are mared 0 */
    bool super_dirty;                               /* true if super/freemap modified */
    void *sfs_buffer;                               /* buffer for non-block aligned io */
    semaphore_t fs_sem;                             /* semaphore for fs */
    semaphore_t io_sem;                             /* semaphore for io */
    semaphore_t mutex_sem;                          /* semaphore for link/unlink and rename */
    list_entry_t inode_list;                        /* inode linked-list */
    list_entry_t *hash_list;                        /* inode hash linked-list */
};

其中最主要的是從磁盤中讀取該文件系統的superblock(上文提到過,每個文件系統一個,記錄該文件系統的信息),至此整個初始化過程討論完畢。

Simple FS文件系統

ucore內核把所有文件都看作是字節流,任何內部邏輯結構都是專用的,由應用程序負責解釋。ucore區分文件的物理結構,目前ucore支持如下幾種類型的文件:

  • 常規文件:文件中包括的內容信息是由應用程序輸入。SFS文件系統在普通文件上不強加任何內部結構,把其文件內容信息看作為字節。
  • 目錄:包含一系列的entry,每個entry包含文件名和指向與之相關聯的索引節點(index node)的指針。目錄是按層次結構組織的。
  • 鏈接文件:實際上一個鏈接文件是一個已經存在的文件的另一個可選擇的文件名。
  • 設備文件:不包含數據,但是提供了一個映射物理設備(如串口、鍵盤等)到一個文件名的機制。可通過設備文件訪問外圍設備。
  • 管道:管道是進程間通訊的一個基礎設施。管道緩存了其輸入端所接受的數據,以便在管道輸出端讀的進程能一個先進先出的方式來接受數據。
    在github上的ucore教學操作系統中,主要關注的是常規文件、目錄和鏈接中的hardlink的設計實現。SFS文件系統中目錄和常規文件具有共同的屬性,而這些屬性保存在索引節點中。SFS通過索引節點來管理目錄和常規文件,索引節點包含操作系統所需要的關於某個文件的關鍵信息,比如文件的屬性、訪問許可權以及其他控制信息都保存在索引節點中。可以有多個文件名指向一個索引節點。

文件系統的布局

文件系統通常保存在磁盤上,disk0代表磁盤,用來存放一個SFS文件系統。磁盤的使用是以扇區為單位的,但是在文件系統中,一般按數據塊來使用磁盤,在sfs中,我們以4k(8個sector,和page大小相等)為一個數據塊。sfs文件系統的布局如下圖所示。

第0個塊(4K)是超級塊(superblock),它包含了關於文件系統的所有關鍵參數,當計算機被啟動或文件系統被首次接觸時,超級塊的內容就會被裝入內存。其定義如下:

struct sfs_super {
    uint32_t magic;                                  /* magic number, should be SFS_MAGIC */
    uint32_t blocks;                                 /* # of blocks in fs */
    uint32_t unused_blocks;                         /* # of unused blocks in fs */
    char info[SFS_MAX_INFO_LEN + 1];                /* infomation for sfs  */
};

其中成員變量magic代表一個魔數,其值為0x2f8dbe2a,內核用它來檢查磁盤鏡像是否合法,blocks記錄了sfs中block的數量,unused_block記錄了sfs中還沒有被使用的block數量,其中關於物理磁盤的管理與虛擬內存的管理十分類似,每次使用物理磁盤也會有一個類似於物理內存管理的分配算法。最后info是記錄一個字符串"simple file system"。
第1個塊放了一個root-dir的inode,用來記錄根目錄的相關信息。有關inode還將在后續部分介紹。這里只要理解root-dir是SFS文件系統的根結點,通過這個root-dir的inode信息就可以定位並查找到根目錄下的所有文件信息。
從第2個塊開始,根據SFS中所有塊的數量,用1個bit來表示一個塊的占用和未被占用的情況。這個區域稱為SFS的freemap區域,這將占用若干個塊空間。為了更好地記錄和管理freemap區域,專門提供了兩個文件kern/fs/sfs/bitmap.[ch]來完成根據一個塊號查找或設置對應的bit位的值。
最后在剩余的磁盤空間中,存放了所有其他目錄和文件的inode信息和內容數據信息。需要注意的是雖然inode的大小小於一個塊的大小(4096B),但為了實現簡單,每個 inode 都占用一個完整的 block。
在sfs_fs.c文件中的sfs_do_mount函數中,完成了加載位於硬盤上的SFS文件系統的超級塊superblock和freemap的工作。這樣,在內存中就有了SFS文件系統的全局信息。

索引節點

磁盤索引節點

之前在初始化過程中討論過vfs對應的索引節點,其實索引節點主要是指存在磁盤中的索引節點,當把磁盤中的索引節點load到內存中之后,在內存中也會存在一個索引節點,下面先討論磁盤中的索引節點,數據結構如下所示。

struct sfs_disk_inode {
    uint32_t size;                              如果inode表示常規文件,則size是文件大小
    uint16_t type;                                  inode的文件類型
    uint16_t nlinks;                               此inode的硬鏈接數
    uint32_t blocks;                              此inode的數據塊數的個數
    uint32_t direct[SFS_NDIRECT];                此inode的直接數據塊索引值(有SFS_NDIRECT個)
    uint32_t indirect;                            此inode的一級間接數據塊索引值
};

對於磁盤索引節點,這里只解釋最后的兩個成員變量,direct指的是這個inode的直接索引塊的索引值,它的大小是12,所以最多能夠通過direct的方式支持最大12*4096B的文件大小。之所以這樣設計是因為我們實際的文件系統中,絕大多數文件都是小文件,因此直接索引的方式能夠提高小文件的存取速度,而且通過間接索引的方式還能支持大文件。當使用一級間接數據塊索引時,ucore 支持最大的文件大小為 12 * 4k + 1024 * 4k = 48k + 4m。
對於普通文件,索引值指向的 block 中保存的是文件中的數據。而對於目錄,索引值指向的數據保存的是目錄下所有的文件名以及對應的索引節點所在的索引塊(磁盤塊)所形成的數組。數據結構如下:

/* file entry (on disk) */
struct sfs_disk_entry {
    uint32_t ino;                                   索引節點所占數據塊索引值
    char name[SFS_MAX_FNAME_LEN + 1];               文件名
};

操作系統中,每個文件系統下的 inode 都應該分配唯一的 inode 編號。SFS 下,為了實現的簡便(偷懶),每個 inode 直接用他所在的磁盤 block 的編號作為 inode 編號。比如,root block 的 inode 編號為 1;每個 sfs_disk_entry 數據結構中,name 表示目錄下文件或文件夾的名稱,ino 表示磁盤 block 編號,通過讀取該 block 的數據,能夠得到相應的文件或文件夾的 inode。ino 為0時,表示一個無效的 entry。(因為 block 0 用來保存 super block,它不可能被其他任何文件或目錄使用,所以這么設計也是合理的)。
此外,和 inode 相似,每個 sfs_dirent_entry 也占用一個 block。

內存索引節點

內存索引節點的數據結構如下圖所示。

/* inode for sfs */
struct sfs_inode {
    struct sfs_disk_inode *din;                     /* on-disk inode */
    uint32_t ino;                                   /* inode number */
    uint32_t flags;                                 /* inode flags */
    bool dirty;                                     /* true if inode modified */
    int reclaim_count;                              /* kill inode if it hits zero */
    semaphore_t sem;                                /* semaphore for din */
    list_entry_t inode_link;                        /* entry for linked-list in sfs_fs */
    list_entry_t hash_link;                         /* entry for hash linked-list in sfs_fs */
};

內存inode只有在打開一個文件后才會創建,如果關機則相關信息都會消失。可以看到,內存inode包含了硬盤inode的信息,而且還增加了其他一些信息,這是為了實現判斷是否改寫(dirty),互斥操作(sem),回收(reclaim——count)和快速定位(hash_link)等作用。為了便於實現上面提到的多級數據訪問以及目錄中entry的操作,對inode SFS實現了一些輔助函數:

  1. sfs_bmap_load_nolock:將對應 sfs_inode 的第 index 個索引指向的 block 的索引值取出存到相應的指針指向的單元(ino_store)。該函數只接受 index <= inode-<blocks 的參數。當 index == inode-&ltblocks 時,該函數理解為需要為 inode 增長一個 block。並標記 inode 為 dirty(所有對 inode 數據的修改都要做這樣的操作,這樣,當 inode 不再使用的時候,sfs 能夠保證 inode 數據能夠被寫回到磁盤)。sfs_bmap_load_nolock 調用的 sfs_bmap_get_nolock 來完成相應的操作。(sfs_bmap_get_nolock 只由 sfs_bmap_load_nolock 調用)
  2. sfs_bmap_truncate_nolock:將多級數據索引表的最后一個 entry 釋放掉。他可以認為是 sfs_bmap_load_nolock 中,index == inode->blocks 的逆操作。當一個文件或目錄被刪除時,sfs 會循環調用該函數直到 inode->blocks 減為 0,釋放所有的數據頁。函數通過 sfs_bmap_free_nolock 來實現,他應該是 sfs_bmap_get_nolock 的逆操作。和 sfs_bmap_get_nolock 一樣,調用 sfs_bmap_free_nolock 也要格外小心。
  3. sfs_dirent_read_nolock:將目錄的第 slot 個 entry 讀取到指定的內存空間。他通過上面提到的函數來完成。
  4. sfs_dirent_write_nolock:用指定的 entry 來替換某個目錄下的第 slot 個entry。他通過調用 sfs_bmap_load_nolock保證,當第 slot 個entry 不存在時(slot == inode->blocks),SFS 會分配一個新的entry,即在目錄尾添加了一個 entry。
  5. sfs_dirent_search_nolock:是常用的查找函數。他在目錄下查找 name,並且返回相應的搜索結果(文件或文件夾)的 inode 的編號(也是磁盤編號),和相應的 entry 在該目錄的 index 編號以及目錄下的數據頁是否有空閑的 entry。(SFS 實現里文件的數據頁是連續的,不存在任何空洞;而對於目錄,數據頁不是連續的,當某個 entry 刪除的時候,SFS 通過設置 entry->ino 為0將該 entry 所在的 block 標記為 free,在需要添加新 entry 的時候,SFS 優先使用這些 free 的 entry,其次才會去在數據頁尾追加新的 entry。
    這部分比較復雜,關於這部分以后會單獨開一篇博客來敘述。

文件系統抽象層-VFS

文件系統抽象層是把不同文件系統的對外共性借口提取出來,形成一個數據結構(包含多個函數指針),這樣,通用文件系統訪問接口層只需要訪問文件系統抽象層,而不需要關心具體文件系統的實現細節和接口。

file&dir接口

file&dir接口層定義了進程在內核中直接訪問的文件相關信息,這定義在file數據結構中,具體描述如下:

struct file {
    enum {
        FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED,
    } status;                          //訪問文件的執行狀態
    bool readable;                     //文件是否可讀
    bool writable;                     //文件是否可寫
    int fd;                           //文件在filemap中的索引值
    off_t pos;                        //訪問文件的當前位置
    struct inode *node;               //該文件對應的內存inode指針
    atomic_t open_count;              //打開此文件的次數
};

而在kern/process/proc.h中的proc_struct結構中描述了進程訪問文件的數據接口fs_struct,其數據結構定義如下:

struct fs_struct {
    struct inode *pwd;                //進程當前執行目錄的內存inode指針
    struct file *filemap;             //進程打開文件的數組
    atomic_t fs_count;                //訪問此文件的線程個數??
    semaphore_t fs_sem;                //確保對進程控制塊中fs_struct的互斥訪問
};

當創建一個進程后,該進程的fs_struct將會被初始化或復制父進程的fs_struct。當用戶進程打開一個文件時,將從filemap數組中取得一個空閑file項,然后會把此file的成員變量node指針指向一個代表此文件的inode的起始地址。

inode接口

index node是位於內存的索引節點,它是VFS結構中的重要數據結構,因為它實際負責把不同文件系統的特定索引節點信息(甚至不能算是一個索引節點)統一封裝起來,避免了進程直接訪問具體文件系統。它的數據結構在上文已經介紹過,在inode中,有一個成員變量in_ops,這是對此inode的操作函數指針列表,其數據結構定義如下:

struct inode_ops {
    unsigned long vop_magic;
    int (*vop_open)(struct inode *node, uint32_t open_flags);
    int (*vop_close)(struct inode *node);
    int (*vop_read)(struct inode *node, struct iobuf *iob);
    int (*vop_write)(struct inode *node, struct iobuf *iob);
    int (*vop_getdirentry)(struct inode *node, struct iobuf *iob);
    int (*vop_create)(struct inode *node, const char *name, bool excl, struct inode **node_store);
int (*vop_lookup)(struct inode *node, char *path, struct inode **node_store);
……
 };

參照上面對SFS中的索引節點操作函數的說明,可以看出inode_ops是對常規文件、目錄、設備文件所有操作的一個抽象函數表示。對於某一具體的文件系統中的文件或目錄,只需實現相關的函數,就可以被用戶進程訪問具體的文件了,且用戶進程無需了解具體文件系統的實現細節。

一個例子(打開文件,讀文件)

有了上述實現后,ucore就可以支持文件系統了,那么我們來看一看當用戶進程打開文件時會做哪些事情?
首先假定用戶進程需要打開的文件已經存在在硬盤上。以user/sfs_filetest1.c為例,首先用戶進程會調用在main函數中的如下語句:

int fd1 = safe_open("/test/testfile", O_RDWR | O_TRUNC);

從字面上可以看出,如果ucore能夠正常查找到這個文件,就會返回一個代表文件的文件描述符fd1,這樣在接下來的讀寫文件過程中,就直接用這樣fd1來代表就可以了。那這個打開文件的過程是如何一步一步實現的呢?

通用文件訪問接口層的處理流程

首先進入通用文件訪問接口層的處理流程,即進一步調用如下用戶態函數: open->sys_open->syscall,從而引起系統調用進入到內核態。到了內核態后,通過中斷處理例程,會調用到sys_open內核函數,並進一步調用sysfile_open內核函數。到了這里,需要把位於用戶空間的字符串"/test/testfile"拷貝到內核空間中的字符串path中,並進入到文件系統抽象層的處理流程完成進一步的打開文件操作中。

文件接口抽象層的處理流程

分配一個空閑的file數據結構變量file在文件系統抽象層的處理中,首先調用的是file_open函數,它要給這個即將打開的文件分配一個file數據結構的變量,這個變量其實是當前進程的打開文件數組current->fs_struct->filemap[]中的一個空閑元素(即還沒用於一個打開的文件),而這個元素的索引值就是最終要返回到用戶進程並賦值給變量fd1。到了這一步還僅僅是給當前用戶進程分配了一個file數據結構的變量,還沒有找到對應的文件索引節點。為此需要進一步調用vfs_open函數來找到path指出的文件所對應的基於inode數據結構的VFS索引節點node。vfs_open函數需要完成兩件事情:通過vfs_lookup找到path對應文件的inode;調用vop_open函數打開文件。

  1. 找到文件設備的根目錄“/”的索引節點需要注意,這里的vfs_lookup函數是一個針對目錄的操作函數,它會調用vop_lookup函數來找到SFS文件系統中的“/test”目錄下的“testfile”文件。為此,vfs_lookup函數首先調用get_device函數,並進一步調用vfs_get_bootfs函數(其實調用了)來找到根目錄“/”對應的inode。這個inode就是位於vfs.c中的inode變量bootfs_node。這個變量在init_main函數(位於kern/process/proc.c)執行時獲得了賦值。

  2. 找到根目錄“/”下的“test”子目錄對應的索引節點,在找到根目錄對應的inode后,通過調用vop_lookup函數來查找“/”和“test”這兩層目錄下的文件“testfile”所對應的索引節點,如果找到就返回此索引節點。

  3. 把file和node建立聯系。完成第3步后,將返回到file_open函數中,通過執行語句“file->node=node;”,就把當前進程的current->fs_struct->filemap[fd](即file所指變量)的成員變量node指針指向了代表“/test/testfile”文件的索引節點node。這時返回fd。經過重重回退,通過系統調用返回,用戶態的syscall->sys_open->open->safe_open等用戶函數的層層函數返回,最終把把fd賦值給fd1。自此完成了打開文件操作。但這里我們還沒有分析第2和第3步是如何進一步調用SFS文件系統提供的函數找位於SFS文件系統上的“/test/testfile”所對應的sfs磁盤inode的過程。下面需要進一步對此進行分析。

SFS文件系統層的處理流程

這里需要分析文件系統抽象層中沒有徹底分析的vop_lookup函數到底做了啥。下面我們來看看。在sfs_inode.c中的sfs_node_dirops變量定義了“.vop_lookup = sfs_lookup”,所以我們重點分析sfs_lookup的實現。
sfs_lookup有三個參數:node,path,node_store。其中node是根目錄“/”所對應的inode節點;path是文件“testfile”的絕對路徑“/test/testfile”,而node_store是經過查找獲得的“testfile”所對應的inode節點。
Sfs_lookup函數以“/”為分割符,從左至右逐一分解path獲得各個子目錄和最終文件對應的inode節點。在本例中是分解出“test”子目錄,並調用sfs_lookup_once函數獲得“test”子目錄對應的inode節點subnode,然后循環進一步調用sfs_lookup_once查找以“test”子目錄下的文件“testfile1”所對應的inode節點。當無法分解path后,就意味着找到了testfile1對應的inode節點,就可順利返回了。
當然這里講得還比較簡單,sfs_lookup_once將調用sfs_dirent_search_nolock函數來查找與路徑名匹配的目錄項,如果找到目錄項,則根據目錄項中記錄的inode所處的數據塊索引值找到路徑名對應的SFS磁盤inode,並讀入SFS磁盤inode對的內容,創建SFS內存inode。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM