Linux 虛擬檔案系統四大物件:超級塊、inode、dentry、file之間關係

一口Linux發表於2021-03-02

更多嵌入式原創文章,請關注公眾號:一口Linux

一:檔案系統

1. 什麼是檔案系統?

作業系統中負責管理和儲存檔案資訊的軟體機構稱為檔案管理系統,簡稱檔案系統。

通常檔案系統是用於儲存和組織檔案的一種機制,便於對檔案進行方便的查詢與訪問。

檔案系統是對檔案儲存裝置的空間進行組織和分配,負責檔案儲存並對存入的檔案進行保護和檢索的系統。

它負責為使用者建立檔案,存入、讀出、修改、轉儲檔案,控制檔案的存取,當使用者不再使用時撤銷檔案等。

隨著檔案種類的增多,擴增了更多的檔案系統,為了對各種檔案系統進行統一的管理與組織。

2. Linux檔案系統

Linux將檔案系統分為了兩層:VFS(虛擬檔案系統)、具體檔案系統,如下圖所示:
VFS
VFS(Virtual Filesystem Switch)稱為虛擬檔案系統或虛擬檔案系統轉換,是一個核心軟體層,在具體的檔案系統之上抽象的一層,用來處理與Posix檔案系統相關的所有呼叫,表現為能夠給各種檔案系統提供一個通用的介面,使上層的應用程式能夠使用通用的介面訪問不同檔案系統,同時也為不同檔案系統的通訊提供了媒介。

VFS並不是一種實際的檔案系統,它只存在於記憶體中,不存在任何外存空間,VFS在系統啟動時建立,在系統關閉時消亡。

VFS由超級塊、inode、dentry、vfsmount等結構來組成。

Linux系統中存在很多的檔案系統,例如常見的ext2,ext3,ext4,sysfs,rootfs,proc...等等。

二 、VFS

1. VFS在linux架構中的位置

從使用者的使用角度,Linux下的檔案系統中巨集觀上主要分為三層:

  • 1.上層的檔案系統的系統呼叫(System-call );
  • 2.虛擬檔案系統VFS(Virtual File System)層,
  • 3.掛載到VFS中的各種實際檔案系統。

VFS在整個Linux系統中的架構檢視如下:

VFS

Linux系統的User使用GLIBC(POSIX標準、GUN C執行時庫)作為應用程式的執行時庫,然後通過作業系統,將其轉換為系統呼叫SCI(system-call interface),SCI是作業系統核心定義的系統呼叫介面,這層抽象允許使用者程式的I/O操作轉換為核心的介面呼叫。

2. 使用者如何透明的去處理檔案?

我們知道每個檔案系統是獨立的,有自己的組織方法,操作方法。那麼對於使用者來說,不可能所有的檔案系統都瞭解,那麼怎麼做到讓使用者透明的去處理檔案呢?

例如:我想寫檔案,那就直接read就OK,不管你是什麼檔案系統,具體怎麼去讀!這裡就需要引入虛擬檔案系統。

所以虛擬檔案系統就是:對於一個system,可以存在多個“實際的檔案系統”,例如:ext2,ext3,fat32,ntfs...例如我現在有多個分割槽,對於每一個分割槽我們知道可以是不同的“實際檔案系統”。

例如現在三個磁碟分割槽分別是:ext2,ext3,fat32,那麼每個“實際的檔案系統”的操作和資料結構肯定不一樣,那麼,使用者怎麼能透明使用它們呢?

這個時候就需要VFS作為中間一層!使用者直接和VFS打交道。

VFS是一種軟體機制,只存在於記憶體中,每次系統初始化期間Linux都會先在記憶體中構造一棵VFS的目錄樹(也就是原始碼中的namespace)。

VFS主要的作用是對上層應用遮蔽底層不同的呼叫方法,提供一套統一的呼叫介面,二是便於對不同的檔案系統進行組織管理。

VFS提供了一個抽象層,將POSIX API介面與不同儲存裝置的具體介面實現進行了分離,使得底層的檔案系統型別、裝置型別對上層應用程式透明。

例如read,write,那麼對映到VFS中就是sys_read,sys_write,那麼VFS可以根據你操作的是哪個“實際檔案系統”(哪個分割槽)來進行不同的實際的操作!這個技術也是很熟悉的“鉤子結構”技術來處理的。

其實就是VFS中提供一個抽象的struct結構體,然後對於每一個具體的檔案系統要把自己的欄位和函式填充進去,這樣就解決了異構問題(核心很多子系統都大量使用了這種機制)。

三、Linux虛擬檔案系統四大物件

為了對檔案系統進行統一的管理與組織,Linux建立了一個公共根目錄和全域性檔案系統樹。要訪問一個檔案系統中的檔案,必須先將這個檔案系統掛載在全域性檔案系統樹的某個根目錄下,這一掛載過程被稱作檔案系統的掛載,所掛載的目錄稱為掛載點。

傳統的檔案系統在磁碟上的佈局如下:

由上圖可知,檔案系統的開頭通常是由一個磁碟扇區所組成的引導塊,該部分的主要目的是用於對作業系統的引導。一般只在啟動作業系統時使用。

隨後是超級塊,超級塊主要存放了該物理磁碟中檔案系統結構的相關資訊,並且對各個部分的大小進行說明。

最後由i節點點陣圖,邏輯塊點陣圖、i節點、邏輯塊這幾部分分佈在物理磁碟上。

Linux為了對超級塊,i節點,邏輯塊這三部分進行高效的管理,Linux建立了幾種不同的資料結構,分別是檔案系統型別、inode、dentry等幾種。

其中,檔案系統型別規定了某種檔案系統的行為,利用該資料結構可以構造某種檔案系統型別的例項,另外,該例項也被稱為超級塊例項。

超級塊則是反映了檔案系統整體的控制資訊。超級塊能夠以多種的方式存在,對於基於磁碟的檔案系統,它以特定的格式存在於磁碟的固定區域(取決於檔案系統型別)上。在掛載檔案系統時,該超級塊中的內容被讀入磁碟中,從而構建出位於記憶體中的新的超級塊。

inode則反映了檔案系統物件中的一般後設資料資訊。dentry則是反映出某個檔案系統物件在全域性檔案系統樹中的位置。

Linux對這四種資料結構進行了相關的關聯。
如下圖:

結構體關係

1. 超級塊(super block)

超級塊:一個超級塊對應一個檔案系統(已經安裝的檔案系統型別如ext2,此處是實際的檔案系統,不是VFS)。

之前我們已經說了檔案系統用於管理這些檔案的資料格式和操作之類的,系統檔案有系統檔案自己的檔案系統,同時對於不同的磁碟分割槽也有可以是不同的檔案系統。那麼一個超級塊對於一個獨立的檔案系統。儲存檔案系統的型別、大小、狀態等等。

(“檔案系統”和“檔案系統型別”不一樣!一個檔案系統型別下可以包括很多檔案系統即很多的super_block)

既然我們知道對於不同的檔案系統有不同的super_block,那麼對於不同的super_block的操作肯定也是不同的,所以我們在下面的super_block結構中可以看到上面說的抽象的struct結構(例如下面的:struct super_operations):

(linux核心3.14)

1246 struct super_block {
1247     struct list_head    s_list;     /* Keep this first */
1248     dev_t           s_dev;      /* search index; _not_ kdev_t */                                                                                                                                                                                                                                                                                                                                                                                      
1249     unsigned char       s_blocksize_bits;
1250     unsigned long       s_blocksize;
1251     loff_t          s_maxbytes; /* Max file size */
1252     struct file_system_type *s_type;
1253     const struct super_operations   *s_op;
1254     const struct dquot_operations   *dq_op;
1255     const struct quotactl_ops   *s_qcop;
1256     const struct export_operations *s_export_op;
1257     unsigned long       s_flags;
1258     unsigned long       s_magic;
1259     struct dentry       *s_root;
1260     struct rw_semaphore s_umount;
1261     int         s_count;
1262     atomic_t        s_active;
1263 #ifdef CONFIG_SECURITY
1264     void                    *s_security;
1265 #endif
1266     const struct xattr_handler **s_xattr;
1267 
1268     struct list_head    s_inodes;   /* all inodes */
1269     struct hlist_bl_head    s_anon;     /* anonymous dentries for (nfs) exporting */
1270     struct list_head    s_mounts;   /* list of mounts; _not_ for fs use */
1271     struct block_device *s_bdev;
1272     struct backing_dev_info *s_bdi;
1273     struct mtd_info     *s_mtd;
1274     struct hlist_node   s_instances;
1275     struct quota_info   s_dquot;    /* Diskquota specific options */
1276 
1277     struct sb_writers   s_writers;
1278 
1279     char s_id[32];              /* Informational name */
1280     u8 s_uuid[16];              /* UUID */
1281 
1282     void            *s_fs_info; /* Filesystem private info */
1283     unsigned int        s_max_links;
1284     fmode_t         s_mode;
1285 
1286     /* Granularity of c/m/atime in ns.
1287        Cannot be worse than a second */
1288     u32        s_time_gran;
1289 
1290     /*
1291      * The next field is for VFS *only*. No filesystems have any business
1292      * even looking at it. You had been warned.
1293      */
1294     struct mutex s_vfs_rename_mutex;    /* Kludge */
1295 
1296     /*
1297      * Filesystem subtype.  If non-empty the filesystem type field
1298      * in /proc/mounts will be "type.subtype"
1299      */
1300     char *s_subtype;
1301 
1302     /*
1303      * Saved mount options for lazy filesystems using
1304      * generic_show_options()
1305      */
1306     char __rcu *s_options;
1307     const struct dentry_operations *s_d_op; /* default d_op for dentries */
1308 
1309     /*
1310      * Saved pool identifier for cleancache (-1 means none)
1311      */
1312     int cleancache_poolid;
1313 
1314     struct shrinker s_shrink;   /* per-sb shrinker handle */
1315 
1316     /* Number of inodes with nlink == 0 but still referenced */
1317     atomic_long_t s_remove_count;
1318 
1319     /* Being remounted read-only */
1320     int s_readonly_remount;
1321 
1322     /* AIO completions deferred from interrupt context */
1323     struct workqueue_struct *s_dio_done_wq;
1324 
1325     /*
1326      * Keep the lru lists last in the structure so they always sit on their
1327      * own individual cachelines.
1328      */
1329     struct list_lru     s_dentry_lru ____cacheline_aligned_in_smp;
1330     struct list_lru     s_inode_lru ____cacheline_aligned_in_smp;
1331     struct rcu_head     rcu;
1332 };

解釋欄位:

欄位 描述
s_list 指向超級塊連結串列的指標,這個struct list_head是很熟悉的結構了,裡面其實就是用於連線關係的prev和next欄位。核心單獨使用一個簡單的結構體將所有的super_block都連結起來。
s_dev 包含該具體檔案系統的塊裝置識別符號。例如,對於 /dev/hda1,其裝置識別符號為 0x301
s_blocksize_bits 上面的size大小佔用位數,例如512位元組就是9 bits
s_blocksize 檔案系統中資料塊大小,以位元組單位
s_maxbytes 允許的最大的檔案大小(位元組數)
*struct file_system_type s_type 檔案系統型別(也就是當前這個檔案系統屬於哪個型別?ext2還是fat32),要區分“檔案系統”和“檔案系統型別”不一樣!一個檔案系統型別下可以包括很多檔案系統即很多的super_block,後面會說!
*struct super_operations s_op 指向某個特定的具體檔案系統的用於超級塊操作的函式集合
*struct dquot_operations dq_op 指向某個特定的具體檔案系統用於限額操作的函式集合
*struct quotactl_ops s_qcop 用於配置磁碟限額的的方法,處理來自使用者空間的請求
s_flags 安裝標識
s_magic 區別於其他檔案系統的標識
s_root 指向該具體檔案系統安裝目錄的目錄項
s_umount 對超級塊讀寫時進行同步
s_count 對超級塊的使用計數
s_active 引用計數

超級塊方法

 
struct super_operations {
        //該函式在給定的超級塊下建立並初始化一個新的索引節點物件
   	struct inode *(*alloc_inode)(struct super_block *sb);
        //釋放指定的索引結點 。
	void (*destroy_inode)(struct inode *);
        //VFS在索引節點被修改時會呼叫此函式。
   	void (*dirty_inode) (struct inode *, int flags);
        // 將指定的inode寫回磁碟。
	int (*write_inode) (struct inode *, struct writeback_control *wbc);
        //刪除索引節點。
	int (*drop_inode) (struct inode *);
        
	void (*evict_inode) (struct inode *);
        //用來釋放超級塊
	void (*put_super) (struct super_block *);
        //使檔案系統的資料元素與磁碟上的檔案系統同步,wait引數指定操作是否同步。
	int (*sync_fs)(struct super_block *sb, int wait);
	int (*freeze_fs) (struct super_block *);
	int (*unfreeze_fs) (struct super_block *);
        //獲取檔案系統狀態。把檔案系統相關的統計資訊放在statfs中
	int (*statfs) (struct dentry *, struct kstatfs *);
	int (*remount_fs) (struct super_block *, int *, char *);
	void (*umount_begin) (struct super_block *);
 
	int (*show_options)(struct seq_file *, struct dentry *);
	int (*show_devname)(struct seq_file *, struct dentry *);
	int (*show_path)(struct seq_file *, struct dentry *);
	int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
	ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
	ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
#endif
	int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
	long (*nr_cached_objects)(struct super_block *, int);
	long (*free_cached_objects)(struct super_block *, long, int);
};

2. 索引節點(inode)

索引節點inode:
儲存的其實是實際的資料的一些資訊,這些資訊稱為“後設資料”(也就是對檔案屬性的描述)。

例如:檔案大小,裝置識別符號,使用者識別符號,使用者組識別符號,檔案模式,擴充套件屬性,檔案讀取或修改的時間戳,連結數量,指向儲存該內容的磁碟區塊的指標,檔案分類等等。

( 注意資料分成:後設資料+資料本身 )

同時注意:inode有兩種,一種是VFS的inode,一種是具體檔案系統的inode。前者在記憶體中,後者在磁碟中。所以每次其實是將磁碟中的inode調進填充記憶體中的inode,這樣才是算使用了磁碟檔案inode。

inode怎樣生成的?

每個inode節點的大小,一般是128位元組或256位元組。inode節點的總數,在格式化時就給定(現代OS可以動態變化),一般每2KB就設定一個inode。

一般檔案系統中很少有檔案小於2KB的,所以預定按照2KB分,一般inode是用不完的。所以inode在檔案系統安裝的時候會有一個預設數量,後期會根據實際的需要發生變化。

注意inode號:inode號是唯一的,表示不同的檔案。其實在Linux內部的時候,訪問檔案都是通過inode號來進行的,所謂檔名僅僅是給使用者容易使用的。

當我們開啟一個檔案的時候,首先,系統找到這個檔名對應的inode號;然後,通過inode號,得到inode資訊,最後,由inode找到檔案資料所在的block,現在可以處理檔案資料了。

inode和檔案的關係?

當建立一個檔案的時候,就給檔案分配了一個inode。一個inode只對應一個實際檔案,一個檔案也會只有一個inode。inodes最大數量就是檔案的最大數量。

527 struct inode {
 528     umode_t         i_mode;  /* 訪問許可權控制  */
 529     unsigned short      i_opflags;
 530     kuid_t          i_uid;  /* 使用者的id */
 531     kgid_t          i_gid;   /* 使用組id  */
 532     unsigned int        i_flags; /* 檔案系統標誌 */
 533 
 534 #ifdef CONFIG_FS_POSIX_ACL
 535     struct posix_acl    *i_acl;
 536     struct posix_acl    *i_default_acl;
 537 #endif
 538 
 539     const struct inode_operations   *i_op; /*索引節點操作表*/
 540     struct super_block  *i_sb;             /* 相關的超級塊  */
 541     struct address_space    *i_mapping;   /* 相關的地址對映 */
 542 
 543 #ifdef CONFIG_SECURITY
 544     void            *i_security;
 545 #endif
 546 
 547     /* Stat data, not accessed from path walking */
 548     unsigned long       i_ino;   /* 索引節點號 */
 549     /*
 550      * Filesystems may only read i_nlink directly.  They shall use the
 551      * following functions for modification:
 552      *
 553      *    (set|clear|inc|drop)_nlink
 554      *    inode_(inc|dec)_link_count
 555      */
 556     union {
 557         const unsigned int i_nlink;
 558         unsigned int __i_nlink;   /* 硬連線數 */
 559     };
 560     dev_t           i_rdev;  /* 實際裝置識別符號號 */
 561     loff_t          i_size;
 562     struct timespec     i_atime; /* 最後訪問時間 */
 563     struct timespec     i_mtime; /* 最後修改時間 */
 564     struct timespec     i_ctime; /* 最後改變時間  */
 565     spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
 566     unsigned short          i_bytes;  /* 使用的位元組數 */
 567     unsigned int        i_blkbits;
 568     blkcnt_t        i_blocks;  /* 檔案的塊數 */
 569 
 570 #ifdef __NEED_I_SIZE_ORDERED
 571     seqcount_t      i_size_seqcount;
 572 #endif
573 
 574     /* Misc */
 575     unsigned long       i_state;
 576     struct mutex        i_mutex;
 577 
 578     unsigned long       dirtied_when;   /* jiffies of first dirtying 首次修改時間*/
 579 
 580     struct hlist_node   i_hash;   /*  hash值,提高查詢效率 */
 581     struct list_head    i_wb_list;  /* backing dev IO list */
 582     struct list_head    i_lru;      /* inode LRU list 未使用的inode*/
 583     struct list_head    i_sb_list; /* 連結一個檔案系統中所有inode的連結串列 */
 584     union {
 585         struct hlist_head   i_dentry;  /* 目錄項鍊表  */
 586         struct rcu_head     i_rcu;
 587     };
 588     u64         i_version;
 589     atomic_t        i_count;    /* 引用計數 */
 590     atomic_t        i_dio_count;
 591     atomic_t        i_writecount;   /* 寫者計數 */
 592     const struct file_operations    *i_fop; /* former ->i_op->default_file_ops 檔案操作*/
 593     struct file_lock    *i_flock;   /* 檔案鎖連結串列 */
 594     struct address_space    i_data; /* 表示被inode讀寫的頁面 */
 595 #ifdef CONFIG_QUOTA
 596     struct dquot        *i_dquot[MAXQUOTAS];/* 節點的磁碟限額 */
 597 #endif
 598     struct list_head    i_devices;  /* 裝置連結串列(共用同一個驅動程式的裝置形成的連結串列。) */
 599     union {
 600         struct pipe_inode_info  *i_pipe; /* 管道資訊 */
 601         struct block_device *i_bdev; /* 塊裝置驅動節點 */
 602         struct cdev     *i_cdev;   /* 字元裝置驅動節點 */
 603     };
 604 
 605     __u32           i_generation;    /* 索引節點版本號 */
 606 
 607 #ifdef CONFIG_FSNOTIFY
 608     __u32           i_fsnotify_mask; /* all events this inode cares about */
 609     struct hlist_head   i_fsnotify_marks;
 610 #endif
 611 
 612 #ifdef CONFIG_IMA
 613     atomic_t        i_readcount; /* struct files open RO */
 614 #endif
 615     void            *i_private; /* fs or device private pointer 使用者私有資料*/
 616 };

注意管理inode的四個連結串列:

static struct hlist_head *inode_hashtable __read_mostly;

節點方法

struct inode_operations {
	struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
	void * (*follow_link) (struct dentry *, struct nameidata *);
	int (*permission) (struct inode *, int);
	struct posix_acl * (*get_acl)(struct inode *, int);
 
	int (*readlink) (struct dentry *, char __user *,int);
	void (*put_link) (struct dentry *, struct nameidata *, void *);
 
	int (*create) (struct inode *,struct dentry *, umode_t, bool);
	int (*link) (struct dentry *,struct inode *,struct dentry *);
	int (*unlink) (struct inode *,struct dentry *);
	int (*symlink) (struct inode *,struct dentry *,const char *);
	int (*mkdir) (struct inode *,struct dentry *,umode_t);
	int (*rmdir) (struct inode *,struct dentry *);
	int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
	int (*rename) (struct inode *, struct dentry *,
			struct inode *, struct dentry *);
	int (*rename2) (struct inode *, struct dentry *,
			struct inode *, struct dentry *, unsigned int);
	int (*setattr) (struct dentry *, struct iattr *);
	int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
	int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
	ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
	ssize_t (*listxattr) (struct dentry *, char *, size_t);
	int (*removexattr) (struct dentry *, const char *);
	int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
		      u64 len);
	int (*update_time)(struct inode *, struct timespec *, int);
	int (*atomic_open)(struct inode *, struct dentry *,
			   struct file *, unsigned open_flag,
			   umode_t create_mode, int *opened);
	int (*tmpfile) (struct inode *, struct dentry *, umode_t);
	int (*set_acl)(struct inode *, struct posix_acl *, int);
} ____cacheline_aligned;

對其中一些重要的結果進行分析:

方法 含義
create() 如果該inode描述一個目錄檔案,那麼當在該目錄下建立或開啟一個檔案時,核心必須為這個檔案建立一個inode。VFS通過呼叫該inode的i_op->create()函式來完成上述新inode的建立。該函式的第一個引數為該目錄的 inode,第二個引數為要開啟新檔案的dentry,第三個引數是對該檔案的訪問許可權。如果該inode描述的是一個普通檔案,那麼該inode永遠都不會呼叫這個create函式;
lookup() 查詢指定檔案的dentry;
link() 用於在指定目錄下建立一個硬連結。這個link函式最終會被系統呼叫link()呼叫。該函式的第一個引數是原始檔案的dentry,第二個引數即為上述指定目錄的inode,第三個引數是連結檔案的dentry。
unlink () 在某個目錄下刪除指定的硬連結。這個unlink函式最終會被系統呼叫unlink()呼叫。 第一個引數即為上述硬連結所在目錄的inode,第二個引數為要刪除檔案的dentry。
symlink () 在某個目錄下新建
mkdir() 在指定的目錄下建立一個子目錄,當前目錄的inode會呼叫i_op->mkdir()。該函式會被系統呼叫mkdir()呼叫。第一個引數即為指定目錄的inode,第二個引數為子目錄的dentry,第三個引數為子目錄許可權;
rmdir () 從inode所描述的目錄中刪除一個指定的子目錄時,該函式會被系統呼叫rmdir()最終呼叫;
mknod() 在指定的目錄下建立一個特殊檔案,比如管道、裝置檔案或套接字等。

3)目錄項(dentry)

目錄項是描述檔案的邏輯屬性,只存在於記憶體中,並沒有實際對應的磁碟上的描述,更確切的說是存在於記憶體的目錄項快取,為了提高查詢效能而設計。

注意不管是資料夾還是最終的檔案,都是屬於目錄項,所有的目錄項在一起構成一顆龐大的目錄樹。

例如:open一個檔案/home/xxx/yyy.txt,那麼/、home、xxx、yyy.txt都是一個目錄項,VFS在查詢的時候,根據一層一層的目錄項找到對應的每個目錄項的inode,那麼沿著目錄項進行操作就可以找到最終的檔案。

注意:目錄也是一種檔案(所以也存在對應的inode)。開啟目錄,實際上就是開啟目錄檔案。

108 struct dentry {
109     /* RCU lookup touched fields */
110     unsigned int d_flags;       /* protected by d_lock */
111     seqcount_t d_seq;       /* per dentry seqlock */
112     struct hlist_bl_node d_hash;    /* lookup hash list */
113     struct dentry *d_parent;    /* parent directory 父目錄*/
114     struct qstr d_name;
115     struct inode *d_inode;      /* Where the name belongs to - NULL is
116                      * negative 與該目錄項關聯的inode*/
117     unsigned char d_iname[DNAME_INLINE_LEN];    /* small names 短檔名*/
118 
119     /* Ref lookup also touches following */
120     struct lockref d_lockref;   /* per-dentry lock and refcount */
121     const struct dentry_operations *d_op;  /* 目錄項操作 */
122     struct super_block *d_sb;   /* The root of the dentry tree 這個目錄項所屬的檔案系統的超級塊(目錄項樹的根)*/
123     unsigned long d_time;       /* used by d_revalidate 重新生效時間*/
124     void *d_fsdata;         /* fs-specific data 具體檔案系統的資料 */
125 
126     struct list_head d_lru;     /* LRU list 未使用目錄以LRU 演算法連結的連結串列 */
127     /*
128      * d_child and d_rcu can share memory
129      */
130     union {
131         struct list_head d_child;   /* child of parent list 目錄項通過這個加入到父目錄的d_subdirs中*/
132         struct rcu_head d_rcu;
133     } d_u;
134     struct list_head d_subdirs; /* our children 本目錄的所有孩子目錄連結串列頭 */
135     struct hlist_node d_alias;  /* inode alias list 索引節點別名連結串列*/
136 };

一個有效的dentry結構必定有一個inode結構,這是因為一個目錄項要麼代表著一個檔案,要麼代表著一個目錄,而目錄實際上也是檔案。所以,只要dentry結構是有效的,則其指標d_inode必定指向一個inode結構。但是inode卻可以對應多個。

整個結構其實就是一棵樹,如果看過我的裝置模型kobject就能知道,目錄其實就是檔案(kobject、inode)再加上一層封裝,這裡所謂的封裝主要就是增加兩個指標,一個是指向父目錄,一個是指向該目錄所包含的所有檔案(普通檔案和目錄)的連結串列頭。

這樣才能有我們的目錄操作(比如回到上次目錄,只需要一個指標步驟【..】,而進入子目錄需要連結串列索引需要多個步驟)

dentry相關的操作(inode裡面已經包含了mkdir,rmdir,mknod之類的操作了)

struct dentry_operations {
        /* 該函式判斷目錄物件是否有效。VFS準備從dcache中使用一個目錄項時,會呼叫該函式. */
	int (*d_revalidate)(struct dentry *, unsigned int);       
	int (*d_weak_revalidate)(struct dentry *, unsigned int);
        /* 該目錄生成雜湊值,當目錄項要加入到雜湊表時,VFS要呼叫此函式。 */
	int (*d_hash)(const struct dentry *, struct qstr *);    
        /* 該函式來比較name1和name2這兩個檔名。使用該函式要加dcache_lock鎖。 */
	int (*d_compare)(const struct dentry *, const struct dentry *,
			unsigned int, const char *, const struct qstr *);
        /* 當d_count=0時,VFS呼叫次函式。使用該函式要叫 dcache_lock鎖。 */
	int (*d_delete)(const struct dentry *);
        /* 當該目錄物件將要被釋放時,VFS呼叫該函式。 */
	void (*d_release)(struct dentry *);
	void (*d_prune)(struct dentry *);
        /* 當一個目錄項丟失了其索引節點時,VFS就掉用該函式。 */
	void (*d_iput)(struct dentry *, struct inode *);
	char *(*d_dname)(struct dentry *, char *, int);
	struct vfsmount *(*d_automount)(struct path *);
	int (*d_manage)(struct dentry *, bool);
} ____cacheline_aligned;
 

4)檔案物件(file)

檔案物件描述的是程式已經開啟的檔案。因為一個檔案可以被多個程式開啟,所以一個檔案可以存在多個檔案物件。但是由於檔案是唯一的,那麼inode就是唯一的,目錄項也是定的!

程式其實是通過檔案描述符來操作檔案的,每個檔案都有一個32位的數字來表示下一個讀寫的位元組位置,這個數字叫做檔案位置。

一般情況下開啟檔案後,開啟位置都是從0開始,除非一些特殊情況。Linux用file結構體來儲存開啟的檔案的位置,所以file稱為開啟的檔案描述。file結構形成一個雙連結串列,稱為系統開啟檔案表。

file

775 struct file {
 776     union {
 777         struct llist_node   fu_llist; /* 每個檔案系統中被開啟的檔案都會形成一個雙連結串列 */
 778         struct rcu_head     fu_rcuhead;
 779     } f_u;
 780     struct path     f_path;
 781 #define f_dentry    f_path.dentry
 782     struct inode        *f_inode;   /* cached value */
 783     const struct file_operations    *f_op; /* 指向檔案操作表的指標 */
 784 
 785     /*
 786      * Protects f_ep_links, f_flags.
 787      * Must not be taken from IRQ context.
 788      */
 789     spinlock_t      f_lock;
 790     atomic_long_t       f_count;  /* 檔案物件的使用計數 */
 791     unsigned int        f_flags;  /* 開啟檔案時所指定的標誌 */
 792     fmode_t         f_mode;       /* 檔案的訪問模式(許可權等) */
 793     struct mutex        f_pos_lock;
 794     loff_t          f_pos;       /* 檔案當前的位移量 */
 795     struct fown_struct  f_owner;
 796     const struct cred   *f_cred;
 797     struct file_ra_state    f_ra; /* 預讀狀態 */
 798 
 799     u64         f_version;   /* 版本號 */
 800 #ifdef CONFIG_SECURITY
 801     void            *f_security;  /* 安全模組 */
 802 #endif
 803     /* needed for tty driver, and maybe others */
 804     void            *private_data; /* 私有資料 */
 805 
 806 #ifdef CONFIG_EPOLL
 807     /* Used by fs/eventpoll.c to link all the hooks to this file */
 808     struct list_head    f_ep_links;
 809     struct list_head    f_tfile_llink;
 810 #endif /* #ifdef CONFIG_EPOLL */
 811     struct address_space    *f_mapping;/* 頁快取對映 */
 812 #ifdef CONFIG_DEBUG_WRITECOUNT
 813     unsigned long f_mnt_write_state;
 814 #endif
 815 } __attribute__((aligned(4)));  /* lest something weird decides that 2 is OK */

重點解釋一些重要欄位:

  1. 首先,f_flags、f_mode和f_pos代表的是這個程式當前操作這個檔案的控制資訊。這個非常重要,因為對於一個檔案,可以被多個程式同時開啟,那麼對於每個程式來說,操作這個檔案是非同步的,所以這個三個欄位就很重要了。
  2. 對於引用計數f_count,當我們關閉一個程式的某一個檔案描述符時候,其實並不是真正的關閉檔案,僅僅是將f_count減一,當f_count=0時候,才會真的去關閉它。對於dup,fork這些操作來說,都會使得f_count增加,具體的細節,以後再說。
  3. f_op也是很重要的!是涉及到所有的檔案的操作結構體。例如:使用者使用read,最終都會呼叫file_operations中的讀操作,而file_operations結構體是對於不同的檔案系統不一定相同。裡面一個重要的操作函式是release函式,當使用者執行close時候,其實在核心中是執行release函式,這個函式僅僅將f_count減一,這也就解釋了上面說的,使用者close一個檔案其實是將f_count減一。只有引用計數減到0才關閉檔案。

注意:對於“正在使用”和“未使用”的檔案物件分別使用一個雙向連結串列進行管理。

files_struct

上面的file只是對一個檔案而言,對於一個程式(使用者)來說,可以同時處理多個檔案,所以需要另一個結構來管理所有的files!

即:使用者開啟檔案表--->files_struct

172 struct files_struct {
173         atomic_t count;
174         rwlock_t file_lock;     /* Protects all the below members.  Nests inside tsk->alloc_lock */
175         int max_fds;
176         int max_fdset;
177         int next_fd;
178         struct file ** fd;      /* current fd array */
179         fd_set *close_on_exec;
180         fd_set *open_fds;
181         fd_set close_on_exec_init;
182         fd_set open_fds_init;
183         struct file * fd_array[NR_OPEN_DEFAULT];
184 }; 

解釋一些欄位:

欄位 描述
count 引用計數
file_lock 鎖,保護下面的欄位
max_fds 當前檔案物件的最大的數量
max_fdset 檔案描述符最大數
next_fd 已分配的最大的檔案描述符+1
fd 指向檔案物件指標陣列的指標,一般就是指向最後一個欄位fd_arrray,當檔案數超過NR_OPEN_DEFAULT時候,就會重新分配一個陣列,然後指向這個新的陣列指標!
close_on_exec 執行exec()時候需要關閉的檔案描述符
open_fds 指向開啟的檔案描述符的指標
close_on_exec_init 執行exec()時候需要關閉的檔案描述符初始化值
open_fds_init 檔案描述符初值集合
fd_array 檔案物件指標的初始化陣列

fs_struct

上面的file和files_struct記錄的是與程式相關的檔案的資訊,但是對於程式本身來說,自身的一些資訊用什麼表示,這裡就涉及到fs_struct結構體。

  5 struct fs_struct {
  6         atomic_t count;
  7         rwlock_t lock;
  8         int umask;
  9         struct dentry * root, * pwd, * altroot;
 10         struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
 11 }; 

解釋一些欄位:

欄位 描述
count 引用計數
lock 保護鎖
umask 開啟檔案時候預設的檔案訪問許可權
root 程式的根目錄
pwd 程式當前的執行目錄
altroot 使用者設定的替換根目錄

注意:實際執行時,這三個目錄不一定都在同一個檔案系統中。例如,程式的根目錄通常是安裝於“/”節點上的ext檔案系統,而當前工作目錄可能是安裝於/etc的一個檔案系統,替換根目錄也可以不同檔案系統中。
rootmnt,pwdmnt,altrootmnt:對應於上面三個的安裝點。

檔案方法(操作)file_operations

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	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);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

上面這個對我們驅動開發人員應該是最熟悉的,也是必須掌握的了。

欄位 描述
owner 用於指定擁有這個檔案操作結構體的模組,通常取THIS_MODULE;
llseek 用於設定檔案的偏移量。第一個引數指明要操作的檔案,第二個引數為偏移量,第三個引數為開始偏移的位置(可取SEEK_SET,SEEK_CUR和SEEK_END之一)。
read 從檔案中讀資料。第一個引數為原始檔,第二個引數為目的字串,第三個引數指明欲讀資料的總位元組數,第四個引數指明從原始檔的某個偏移量處開始讀資料。由系統呼叫read()呼叫;
write 往檔案裡寫資料。第一個引數為目的檔案,第二個引數源字串,第三個引數指明欲寫資料的總位元組數,第四個引數指明從目的檔案的某個偏移量出開始寫資料。由系統呼叫write()呼叫;
mmap 將指定檔案對映到指定的地址空間上。由系統呼叫mmap()呼叫;
open 開啟指定檔案,並且將這個檔案和指定的索引結點關聯起來。由系統呼叫open()呼叫;
release 釋放以開啟的檔案,當開啟檔案的引用計數(f_count)為0時,該函式被呼叫;
fsync() 檔案在緩衝的資料寫回磁碟;

四、程式與這四者之間的關係

核心中用於管理程式的結構體是task_struct。
程式開啟檔案就涉及到上述4個重要的資料結構:

file 
fs_struct 
files_struct 
namespace

每個程式都有自己的namespace。

fs_struct用於表示程式與檔案系統之間的結構關係,比如當前的工作目錄,程式的根目錄等等。

files_struct 用於表示當前程式開啟的檔案。

而對於每一個開啟的檔案,由file物件來表示。

Linux中,常常用檔案描述符(file descriptor)來表示一個開啟的檔案,這個描述符的值往往是一個大於或等於0的整數。
而這個整數,其實就是在files_struct中file陣列fd的下標。
對於所有開啟的檔案, 這些檔案描述符會儲存在open_fds的點陣圖中。

程式與超級塊、檔案、索引結點、目錄項的關係

從圖中可知:

  1. 程式通過task_struct中的一個域files->files_struct 來了解它當前所開啟的檔案物件;而我們通常所說的檔案描述符其實是程式開啟的檔案物件陣列的索引值。
  2. 檔案物件通過域f_dentry找到它對應的dentry物件,再由dentry物件的域d_inode找到它對應的索引節點(通過索引節點又可以得到超級塊的資訊,也就可以得到最終操作檔案的方法,在open檔案的時候就是使用這樣一個過程),這樣就建立了檔案物件與實際的物理檔案的關聯。
  3. 檔案物件所對應的檔案操作函式列表是通過索引節點的域i_fop得到的,而i_fop最終又是通過struct super_operations *s_op來初始化的。

VFS檔案系統中的inode和dentry與實際檔案系統的inode和dentry有一定的關係,但不能等同。

真實磁碟檔案的inode和dentry是存在於物理外存上的,但VFS中的inode和dentry是存在於記憶體中的,系統讀取外存中的inode和dentry資訊進行一定加工後,生成記憶體中的inode和dentry。

虛擬的檔案系統也具有inode和dentry結構,只是這是系統根據相應的規則生成的,不存在於實際外存中。

五、磁碟與檔案系統

假設一塊磁碟被分為好幾個分割槽,每個分割槽都是不同的檔案系統。
磁碟與檔案系統

相關文章