歡迎您光臨本站 登入註冊首頁

Linux 2.4.30 內核文件關鍵數據結構

admin @ 2014-03-25 , reply:0

概述

1.  概述根據以前學習內核源碼的經驗,在學習文件系統實現之前,我大概定了個目標:建立一個清晰的全局概念。為將來需要研究代碼細節打下堅實基礎。只研究虛擬文件系統VFS的實現,不研究……

1.   概述
根據以前學習內核源碼的經驗,在學習文件系統實現之前,我大概定了個目標:

  1. 建立一個清晰的全局概念。為將來需要研究代碼細節打下堅實基礎。
  2. 只研究虛擬文件系統 VFS 的實現,不研究具體文件系統。

為什麼選擇 Linux 2.4.30?因為可以參考《Linux 源碼情景分析》一書,減少學習難度。

1.1. 基本概念
1、  一塊磁碟(塊設備),首先要按照某種文件系統(如 NTFS)格式進行格式化,然後才能在其上進行創建目錄、保存文件等操作。
    在Linux 中,有“安裝”文件系統和“卸載”文件系統的概念。
    一塊經過格式化的“塊設備”(不管是剛剛格式化完的,沒有創建任何名錄和文件;還是已經創建了目錄和文件),只有先被“安裝”,才能融入 Linux 的文件系統中,用戶才可以在它上面進行正常的文件操作。

2、  Linux 把目錄或普通文件,統一看成“目錄節點”。通常一個“目錄節點”具有兩個重要屬性:名稱以及磁碟上實際對應的數據。本文中,“目錄節點”有時簡稱為“節點”
    “符號鏈接”是一種特殊的目錄節點,它只有一個名稱,沒有實際數據。這個名稱指向一個實際的目錄節點。

3、  “介面結構”:在 內核代碼中,經常可以看到一種結構,其成員全部是函數指針,例如:
struct file_operations {
  struct module *owner;
  loff_t (*llseek) (struct file *, loff_t, int);
  ssize_t (*read) (struct file *, char *, size_t, loff_t *);
  ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
  int (*readdir) (struct file *, void *, filldir_t);
  unsigned int (*poll) (struct file *, struct poll_table_struct *);
  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
  int (*mmap) (struct file *, struct vm_area_struct *);
  int (*open) (struct inode *, struct file *);
  int (*flush) (struct file *);
  int (*release) (struct inode *, struct file *);
  int (*fsync) (struct file *, struct dentry *, int datasync);
  int (*fasync) (int, struct file *, int);
  int (*lock) (struct file *, int, struct file_lock *);
  ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
  ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
  ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
  unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
};

    這種結構的作用類似與 C++ 中的“介面類”,它是用 C 語言進行軟體抽象設計時最重要的工具。通過它,將一組通用的操作抽象出來,核心的代碼只針對這種“介面結構”進行操作,而這些函數的具體實現由不同的“子類”去完成。
    以這個 file_operations“介面”為例,它是“目錄節點”提供的操作介面。不同的文件系統需要提供這些函數的具體實現。
    本文中,“介面結構”有時簡稱“介面”。

1.2.  虛擬文件系統
   
Linux 通過虛擬文件系統 (VFS) 來支持不同的具體的文件系統,那麼 VFS 到底是什麼?
    從程序員的角度看,我認為 VFS 就是一套代碼框架(framework),它將用戶與具體的文件系統隔離開來,使得用戶能夠通過這套框架,以統一的介面在不同的具體的文件系統上進行操作。

    這套框架包括:

  1. 為用戶提供統一的文件和目錄的操作介面,如  open, read, write
  2. 抽象出文件系統共有的一些結構,包括“目錄節點”inode、“超級塊”super_block 等。
  3. 面向具體的文件系統,定義一系列統一的操作“介面”, 如 file_operations, inode_operations, dentry_operation,具體的文件系統必須提供它們的實現。
  4. 提供一套機制,讓具體的文件系統融入 VFS 框架中,包括文件系統的“註冊”和“安裝”
  5. 實現這套框架邏輯的核心代碼

    我對文件系統的學習,實際上就是學習虛擬文件系統這套框架是如何實現的。

2.   核心數據結構
   數據結構是代碼的靈魂,要分析一個複雜的系統,關鍵是掌握那些核心的數據結構,這包括:

  1. 弄清數據結構的核心功能。一個數據結構通常具有比較複雜的成員,此外,還有一些成員用於建立數據結構之間的關係。如果要一個個去理解,就會陷入細節。
  2. 弄清數據結構之間的靜態關係
  3. 弄清數據結構之間是如何建立起動態的關係的

本文重點分析文件系統中的關鍵數據結構以及它們之間的關係。

2.1.  inode 和 file_operations

  1. inode 用以描述“目錄節點” ,它描述了一個目錄節點物理上的屬性,例如大小,創建時間,修改時間、uid、gid 等
  2. file_operations 是“目錄節點”提供的操作“介面”。它包括 open, read, wirte, ioctl, llseek, mmap 等操作。
  3. 一個  inode 通過成員 i_fop 對應一個 file_operations
  4. 打開文件的過程就是尋找 “目錄節點”對應的 inode 的過程
  5. 文件被打開后,inode 和 file_operation 都已經在內存中建立,file_operations 的指針也已經指向了具體文件系統提供的函數,此後都文件的操作,都由這些函數來完成。

例如打開了一個普通文件 /root/file,其所在文件系統格式是 ext2,那麼,內存中結構如下:
  
2.2. 目錄節點入口dentry
    本來,inode 中應該包括“目錄節點”的名稱,但由於符號鏈接的存在,導致一個物理文件可能有多個文件名,因此把和“目錄節點”名稱相關的部分從 inode 中分開,放在一個專門的 dentry 結構中。這樣:

  1. 一個dentry 通過成員 d_inode 對應到一個 inode上,尋找 inode 的過程變成了尋找 dentry 的過程。因此,dentry 變得更加關鍵,inode 常常被 dentry 所遮掩。可以說, dentry 是文件系統中最核心的數據結構,它的身影無處不在
  2. 由於符號鏈接的存在,導致多個 dentry 可能對應到同一個 inode 上

例如,有一個符號鏈接 /tmp/abc 指向一個普通文件 /root/file,那麼 dentry 與 inode 之間的關係大致如下:
 
2.3.  super_block 和 super_operations 
    一個存放在磁碟上的文件系統如 EXT2 等,在它的格式中通常包括一個“超級塊”或者“控制塊”的部分,用於從整體上描述文件系統,例如文件系統的大小、是否可讀可寫等等。
    虛擬文件系統中也通過“超級塊”這種概念來描述文件系統整體的信息,對應的結構是 struct super_block。
    super_block 除了要記錄文件大小、訪問許可權等信息外,更重要的是提供一個操作“介面”super_operations。

struct super_operations {
            struct inode *(*alloc_inode)(struct super_block *sb);
            void (*destroy_inode)(struct inode *);
            void (*read_inode) (struct inode *);
            void (*read_inode2) (struct inode *, void *) ;
            void (*dirty_inode) (struct inode *);
            void (*write_inode) (struct inode *, int);
            void (*put_inode) (struct inode *);
            void (*delete_inode) (struct inode *);
            void (*put_super) (struct super_block *);
            void (*write_super) (struct super_block *);
            int (*sync_fs) (struct super_block *);
            void (*write_super_lockfs) (struct super_block *);
            void (*unlockfs) (struct super_block *);
            int (*statfs) (struct super_block *, struct statfs *);
            int (*remount_fs) (struct super_block *, int *, char *);
            void (*clear_inode) (struct inode *);
            void (*umount_begin) (struct super_block *);
            struct dentry * (*fh_to_dentry)(struct super_block *sb, __u32 *fh, int len, int fhtype, int parent);
            int (*dentry_to_fh)(struct dentry *, __u32 *fh, int *lenp, int need_parent);
            int (*show_options)(struct seq_file *, struct vfsmount *);
};

    我們通過分析“獲取一個 inode ”的過程來只理解這個“介面”中兩個成員  alloc_inode  和 read_inode 的作用。
    在文件系統的操作中,經常需要獲得一個“目錄節點”對應的 inode,這個 inode 有可能已經存在於內存中了,也可能還沒有,需要創建一個新的 inode,並從磁碟上讀取相應的信息來填充。

對應的代碼是 iget()   (inlcude/linux/fs.h)過程如下:

  1. 通過 iget4_locked() 獲取 inode。如果 inode 在內存中已經存在,則直接返回;否則創建一個新的 inode
  2. 如果是新創建的 inode,通過 super_block->s_op->read_inode() 來填充它。也就是說,如何填充一個新創建的 inode, 是由具體文件系統提供的函數實現的。

    iget4_locked()  首先在全局的 inode hash table 中尋找,如果找不到,則調用 get_new_inode() ,進而調用 alloc_inode() 來創建一個新的 inode。
    在 alloc_inode() 中可以看到,如果具體文件系統提供了創建 inode 的方法,則由具體文件系統來負責創建,否則採用系統默認的的創建方法。

static struct inode *alloc_inode(struct super_block *sb)
{

            static struct address_space_operations empty_aops;
            static struct inode_operations empty_iops;
            static struct file_operations empty_fops;
            struct inode *inode;

             if (sb->s_op->alloc_inode)
                        inode = sb->s_op->alloc_inode(sb);
            else {
                        inode = (struct inode *) kmem_cache_alloc(inode_cachep, SLAB_KERNEL);
                         if (inode)
                                    memset(&inode->u, 0, sizeof(inode->u));
            }

             if (inode) {
                        struct address_space * const mapping = &inode->i_data;
                        inode->i_sb = sb;
                        inode->i_dev = sb->s_dev;
                        inode->i_blkbits = sb->s_blocksize_bits;
                        inode->i_flags = 0;
                        atomic_set(&inode->i_count, 1);
                        inode->i_sock = 0;
                        inode->i_op = &empty_iops;
                        inode->i_fop = &empty_fops;
                        inode->i_nlink = 1;
                        atomic_set(&inode->i_writecount, 0);
                        inode->i_size = 0;
                        inode->i_blocks = 0;
                        inode->i_bytes = 0;
                        inode->i_generation = 0;
                        memset(&inode->i_dquot, 0, sizeof(inode->i_dquot));
                        inode->i_pipe = NULL;
                        inode->i_bdev = NULL;
                        inode->i_cdev = NULL;

                        mapping->a_ops = &empty_aops;
                        mapping->host = inode;
                        mapping->gfp_mask = GFP_HIGHUSER;
                        inode->i_mapping = mapping;
            }
            return inode;
}

    super_block 是在安裝文件系統的時候創建的,後面會看到它和其它結構之間的關係。

3.   安裝文件系統

  1. 一個經過格式化的塊設備,只有安裝后,才能融入 Linux 的 VFS 之中。
  2. 安裝一個文件系統,必須指定一個目錄作為安裝點。
  3. 一個設備可以同時被安裝到多個目錄上。
  4. 如果某個目錄下原來有一些文件和子目錄,一旦將一個設備安裝到目錄下后,則原有的文件和子目錄消失。因為這個目錄已經變成了一個安裝點。
  5. 一個目錄節點下可以同時安裝多個設備。

3.1.  “根安裝點”、“根設備”和“根文件系統”
   
安裝一個文件系統,除了需要“被安裝設備”外,還要指定一個“安裝點”。“安裝點”是已經存在的一個目錄節點。例如把 /dev/sda1 安裝到 /mnt/win 下,那麼 /mnt/win 就是“安裝點”。
    可是文件系統要先安裝后使用。因此,要使用 /mnt/win 這個“安裝點”,必然要求它所在文件系統已也經被安裝。
    也就是說,安裝一個文件系統,需要另外一個文件系統已經被安裝。
    這是一個雞生蛋,蛋生雞的問題:最頂層的文件系統是如何被安裝的?
    答案是,最頂層文件系統的時候是被安裝在“根安裝點”上的,而根安裝點不屬於任何文件系統,它對應的 dentry 、inode 是由內核在初始化階段憑空構造出來的。
    最頂層的文件系統叫做“根文件系統”。Linux 在啟動的時候,要求用戶必須指定一個“根設備”,內核在初始化階段,將“根設備”安裝到“根安裝點”上,從而有了根文件系統。這樣,文件系統才算準備就緒。此後,用戶就可以通過 mount 命令來安裝新的設備。

3.2.   安裝連接件 vfsmount
   
“安裝”一個文件系統涉及“被安裝設備”和“安裝點”兩個部分,安裝的過程就是把“安裝點”和“被安裝設備”關聯起來,這是通過一個“安裝連接件”結構 vfsmount 來完成的。
    vfsmount  將“安裝點”dentry 和“被安裝設備”的根目錄節點 dentry 關聯起來。
    每安裝一次文件系統,會導致:

  1. 創建一個 vfsmount
  2. 為“被安裝設備”創建一個 super_block,並由具體的文件系統來設置這個 super_block。(我們在“註冊文件系統”一節將再來分析這一步)
  3. 為被安裝設備的根目錄節點創建 dentry
  4. 為被安裝設備的根目錄節點創建 inode, 並由 super_operations->read_inode() 來設置此 inode
  5. 將 super_block 與“被安裝設備“根目錄節點 dentry 關聯起來
  6. 將 vfsmount 與“被安裝設備”的根目錄節點 dentry 關聯起來

    在內核將根設備安裝到“根安裝點”上后,內存中有如下結構關係:
 
     現在假設我們在 /mnt/win 下安裝了 /dev/sda1, /dev/sda1 下有 dir1,然後又在 dir1 下安裝了 /dev/sda2,那麼內存中就有了如下的結構關係
 

[admin via 研發互助社區 ] Linux 2.4.30 內核文件關鍵數據結構已經有1774次圍觀

http://cocdig.com/docs/show-post-42129.html