| zmwillow (journeyman)不知正確與否。 這樣是表示對作者的尊重。 如果有不對,請通知我,我將即時(shí)更改. 一 VFS分析 Linux 操作系統支持多種不同的文件系統,包括 ext2(the Second Extended file-system),nfs(the Network File-system),FAT(the MS-DOS File Allocation Table file system),minix,以及其他許多文件系統。為了使得 linux 內核中的高層子系統能夠以相同的方式處理這些不同的文件系統,Linux 定義了一個(gè)抽象層,即虛擬文件系統VFS,又叫作虛擬文件系統轉換(Virtual Filesystem Switch)。VFS 是 Linux 內核中的一個(gè)子系統,其他的子系統,如IPC,SCHED,MM,NET,都只與 VFS 聯(lián)系,換句話(huà)說(shuō),具體的邏輯文件系統對于 Linux 內核中的其他子系統是透明的。 而proc文件系統,對于 Linux 來(lái)說(shuō),正是一個(gè)邏輯文件系統,因此 proc 文件系統的實(shí)現,也完全遵循 VFS 的規范,在對 proc 文件系統進(jìn)行分析之前,我們必須對 VFS 進(jìn)行一個(gè)詳細的分析。 (一) 基本設計原理 對于邏輯文件系統來(lái)說(shuō),VFS 是一個(gè)管理者,而對于內核的其他部分,則是一個(gè)接口,整個(gè)linux 中文件系統的邏輯關(guān)系,如圖2.1.1所示。 VFS提供了一個(gè)統一的接口(即幾個(gè)有關(guān)操作的數據結構),一個(gè)邏輯文件系統要想被 Linux 支持,那么就必須按照這個(gè)接口來(lái)編寫(xiě)自己的操作函數,從而將自己的細節對其他子系統隱藏起來(lái)。因而,對于內核其他子系統來(lái)說(shuō),所有的文件系統都是一樣的。 (二) 基本對象與方法 虛擬文件系統的接口由一組對象及其由這些對象調用的一組方法所構成的。這些基本的對象是 files(文件),file-systems(文件系統),inodes (索引節點(diǎn))以及 names for inodes(索引節點(diǎn)名字),下面對這些對象進(jìn)行簡(jiǎn)單的介紹: 1 Files: 文件是一個(gè)可讀可寫(xiě)的對象,它也可以映射到內存中,這和 UNIX 中文件描述符的概念很接近。文件在 Linux 中使用一個(gè)"struct file"結構來(lái)實(shí)現,并且該結構有一組操作函數,保存在結構"struct file_operations"中。
圖 2.1.1 2 Inodes: 索引節點(diǎn)是文件系統中的基本對象。它可以是一個(gè)正常文件,一個(gè)目錄,一個(gè)符號鏈接,或者是其他什么東西。VFS 并不明顯地區分這些對象,而把它們留給真正的文件系統,讓它們自己實(shí)現適宜的行為。從而使內核的高層子系統對于不同的對象區別對待。 每一個(gè) 索引節點(diǎn)節點(diǎn)都由一個(gè)"struct inode"結構表現,它的一組方法保存在結構"struct inode_operations"中。 文件(Files)和索引節點(diǎn)(Inodes)也許看起來(lái)很相像,但它們之間有一些非常重要的不同,要注意的一點(diǎn)是,有些東西有索引節點(diǎn),但卻沒(méi)有文件,比如,一個(gè)符號鏈接。與之相對應,有些文件卻沒(méi)有索引節點(diǎn),如管道(pipes)和 sockets。 3 File_systems 文件系統就是 inode 的集合,其中有一個(gè)不同的節點(diǎn),被稱(chēng)為根結點(diǎn)(root)。其他的 inode 以 root 為起始點(diǎn)進(jìn)行訪(fǎng)問(wèn),并且通過(guò)文件名來(lái)查找其他的 inode 。 每一個(gè)文件系統有一組唯一的特征,應用于本文件系統內的所有 inode 之上。其中有一些是標志,比如只讀 READ-ONLY 標志。另一個(gè)重要的內容是 blocksize。 每一個(gè)文件系統都通過(guò)一個(gè)結構"struct super_block"來(lái)表現,而針對超級塊的一組方法則存儲在結構"struct super_operations"之中。 在 Linux 中,超級塊(super-blocks)和 設備號(device number)之間有緊密的聯(lián)系。每一個(gè)文件系統必須有一個(gè)唯一的設備號,該文件系統即建立在此設備之上。有一些文件系統(比如 nfs 和 我們要研究的 proc 文件系統)被標志為不需要真實(shí)的設備,因此,對于這些文件系統,主設備號(major number)為0的匿名設備將會(huì )自動(dòng)地分配給它們。 Linux VFS 了解不同的文件系統類(lèi)型,每一個(gè)文件系統類(lèi)型都使用一個(gè)"struct file_system_type"結構來(lái)表示,在這個(gè)結構中,只包含一個(gè)方法,即 "read_super",使用這個(gè)方法來(lái)實(shí)例化一個(gè)指定文件系統的超級塊。 4 Names 在一個(gè)文件系統內,所有的 inodes 都是通過(guò)名字來(lái)訪(fǎng)問(wèn)的。由于對于某些文件系統來(lái)說(shuō),名字到 inode 的轉換非常耗時(shí)的,因此,Linux 的 VFS 層為當前活動(dòng)的和最近使用的名字維護了一個(gè) cache,這個(gè) cache 被稱(chēng)為 目錄高速緩存(dcache)。 dcache 在內存中組織為樹(shù)狀結構。樹(shù)中的每一個(gè)節點(diǎn)都對應于一個(gè)指定目錄,指定名稱(chēng)的inode。一個(gè)inode可以與多個(gè)樹(shù)中的節點(diǎn)相聯(lián)系。 如果dcache不是一棵完整的文件樹(shù),那么它一定是文件樹(shù)的前綴部分,也就是說(shuō),如果一個(gè)文件樹(shù)的節點(diǎn)在cache中,那么該節點(diǎn)的所以祖先也一定在cache中。 每一個(gè)樹(shù)的節點(diǎn)都使用一個(gè)結構"struct dentry"來(lái)表現,它的一組方法存儲在"struct dentry_operations"之中。 dentry 在 Files 和 Inodes 之間扮演了中間人的角色。每一個(gè)打開(kāi)的文件都指向一個(gè)dentry,而每一個(gè)dentry 則指向它所涉及的inode。這意味著(zhù),對于每一個(gè)打開(kāi)的文件,該文件的dentry 和該文件所有的父節點(diǎn)都在內存中被cache,這使得被打開(kāi)文件的全路徑可以更容易地檢測。 (三) 文件系統的注冊和裝載過(guò)程 1 文件系統的注冊 在使用一個(gè)文件系統之前,必須要對該文件系統進(jìn)行注冊。在Linux編譯的時(shí)候,可以選定支持哪些文件系統,這些編譯進(jìn)內核的文件系統,在系統引導的時(shí)候,就會(huì )在VFS中注冊。而如果一個(gè)文件系統被編譯為內核可裝載模塊,那么將在模塊安裝的時(shí)候進(jìn)行注冊,在模塊卸載的時(shí)候注銷(xiāo)。 每一個(gè)文件系統,都會(huì )在自己的初始化例程中填寫(xiě)一個(gè) file_system_type 的數據結構,然后調用注冊函數register_filesystem(struct file_system_type *fs) 進(jìn)行注冊。下面我們分析一下 file_system_type 的結構: file_system_type 在 include/linux/fs.h 中定義: struct file_system_type { const char *name; int fs_flags; struct super_block *(*read_super) (struct super_block *, void *, int); struct module *owner; struct vfsmount *kern_mnt; /* For kernel mount, if it's FS_SINGLE fs */ struct file_system_type * next; }; 而文件系統的注冊和注銷(xiāo)函數也在該頭文件中聲明: extern int register_filesystem(struct file_system_type *); extern int unregister_filesystem(struct file_system_type *); 函數 register_filesystem 成功時(shí)返回0,當 fs == NULL時(shí)返回 -EINVAL,而當fs->next!=NULL 或者已經(jīng)有同名文件系統注冊時(shí),則返回-EBUSY。當文件系統作為模塊時(shí),必須直接或者間接地在init_module中調用這個(gè)注冊函數,而如果要編譯進(jìn)內核,則必須在fs/filesystem.c中的filesystem_setup中注冊。而unregister_filesystem 則只能在模塊的cleanup_module例程中調用。 所有的已注冊文件系統的 file_system_type 結構最終會(huì )形成一個(gè)鏈表,被稱(chēng)之為"注冊鏈表"。下圖即為內核中 file_system_type 的鏈表示意圖,鏈表頭由 file_systems 指定。
2 文件系統的安裝 要真正使用一個(gè)文件系統,僅僅注冊是不行的,還必須安裝這個(gè)文件系統。在安裝linux時(shí),已經(jīng)(默認)安裝了EXT2文件系統,作為根文件系統。我們可以在文件/etc/fstab中指定自動(dòng)安裝的文件系統,和使用mount命令一樣,我們要為每種文件系統的安裝提供三種信息:文件系統的名稱(chēng),包含該文件系統的物理設備,以及該文件系統的安裝點(diǎn)。例如下面的命令: mount -t vfat /dev/fd0 /mnt/floppy 將把軟盤(pán)(物理設備fd0)中的vfat文件系統安裝到/mnt/floppy目錄上,下面我們分析一下上述命令的執行過(guò)程: 尋找對應的文件系統的信息。VFS通過(guò)file_systems,在file_system_type組成的鏈表中根據指定的文件系統的名稱(chēng)查看文件系統的類(lèi)型信息。 如果在上述鏈表中找到匹配的文件系統,則說(shuō)明內核支持該文件系統,并已經(jīng)注冊。否則,說(shuō)明該文件系統有可能由LKM(LinuxKernelModule)可裝載模塊支持,因此,VFS會(huì )請求內核裝入相應的文件系統模塊,此時(shí),該文件系統在VFS中注冊并初始化。 1. 如果VFS仍然找到指定的文件系統,那么將返回錯誤。 2. 然后,VFS檢驗指定的物理塊設備是否已經(jīng)安裝。如果指定的物理塊設備已經(jīng)被安裝,那么將返回錯誤。也就是說(shuō),一個(gè)塊設備只能安裝到一個(gè)目錄,不能同時(shí)多次安裝。 3. VFS查找新文件系統的安裝點(diǎn)目錄的VFS索引節點(diǎn)。該VFS索引節點(diǎn)可能在索引節點(diǎn)高速緩存中,也有可能需要從安裝點(diǎn)所在的塊設備中讀取。 4. 如果該安裝點(diǎn)目錄已經(jīng)裝有其他的文件系統,那么將返回錯誤。因為在同一目錄只能同時(shí)安裝一個(gè)文件系統。 5. VFS安裝代碼為新的文件系統分配超級塊,并將安裝信息傳遞給該文件系統的超級塊讀取例程。系統中所有的VFS超級塊保存在由super_blocks指向的super_block數據結構指針數組中。 6. 文件系統的超級塊讀取例程將對應的文件系統的信息映射到VFS超級塊中。如果在此過(guò)程中發(fā)生錯誤,例如所讀取的超級塊魔數和指定的文件系統不一致,則返回錯誤。 7. 如果成功安裝,則所有已經(jīng)安裝的文件系統形成如下圖所示的結構:
已注冊文件示意圖 由圖可知,每一個(gè)已經(jīng)掛裝的文件系統由vfsmount結構描述。所有的vfsmount結構形成了一個(gè)鏈表,用vfsmntlist來(lái)指向鏈表頭。這個(gè)鏈表可以稱(chēng)為"已安裝文件系統鏈表"。系統中還有另外兩個(gè)指向這種結構體的指針,vfsmnttail和mru_vfsmnt分別指向鏈表尾和最近使用過(guò)的vfsmount結構。 fsmount結構在include/mount.h中定義: struct vfsmount { struct dentry *mnt_mountpoint; /* dentry of mountpoint */ struct dentry *mnt_root; /* root of the mounted tree */ struct vfsmount *mnt_parent; /* fs we are mounted on */ struct list_head mnt_instances; /* other vfsmounts of the same fs */ struct list_head mnt_clash; /* those who are mounted on (other instances) of the same dentry */ struct super_block *mnt_sb; /* pointer to superblock */ struct list_head mnt_mounts; /* list of children, anchored here */ struct list_head mnt_child; /* and going through their mnt_child */ atomic_t mnt_count; int mnt_flags; char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */ struct list_head mnt_list; uid_t mnt_owner; }; 每個(gè)vfsmount結構包含該文件系統所在的塊設備號、文件系統安裝點(diǎn)的目錄名稱(chēng),以及指向為該文件系統分配的VFS超級塊的指針。而VFS超級塊中則包含描述文件系統的file_system_type結構指針和該文件系統根結點(diǎn)指針。 下面三個(gè)函數是用來(lái)操作已安裝文件系統鏈表的,它們都在fs/super.c中實(shí)現: lookup_vfsmnt():在鏈表中尋找指定設備號的vfsmnt結構,成功則返回指向該結構的指針,否則返回0。 add_vfsmnt():在鏈表尾加入一個(gè)vfsmnt結構,返回指向該結構的指針。 remove_vfsmnt():從鏈表中移走指定設備號的vfsmnt結構,并釋放其所占有的內核內存空間。該函數無(wú)返回值。 3 文件系統的卸載 當文件系統被卸載的時(shí)候,系統將檢查在該文件系統上是否有正被使用。如果有文件正在使用,則不能被卸載。如果該文件系統中的文件或者目錄正在使用,則VFS索引節點(diǎn)高速緩存中可能包含相應的VFS索引節點(diǎn),檢查代碼將在索引節點(diǎn)高速緩存中,根據文件系統所在的設備標識符,查找是否有來(lái)自該文件系統的VFS索引節點(diǎn),如果有而且使用計數大于0,則說(shuō)明該文件系統正在被使用。因此,該文件系統不能被卸載。 否則,將查看對應的VFS超級塊,如果該文件系統的VFS超級塊標志為“臟”,那么必須將超級塊信息寫(xiě)回磁盤(pán)。 上述過(guò)程結束后,對應的VFS超級塊被釋放,vfsmount數據結構將從vfsmntlist鏈表中斷開(kāi)并釋放。 (四) VFS 數據結構分析 現在我們已經(jīng)大致了解了VFS操作的基本過(guò)程。下面我們分析一下在VFS中使用的幾個(gè)重要的數據結構,它們是VFS實(shí)現的核心,更是與邏輯文件系統交互的接口,因此必須進(jìn)行詳細的分析。 1 VFS超級塊及其操作 許多邏輯文件系統都有超級塊結構,超級塊是這些文件系統中最重要的數據結構,用來(lái)描述整個(gè)文件系統的信息,是一個(gè)全局的數據結構。MINIX、EXT2等都有自己的超級塊,VFS也有超級塊,但和邏輯文件系統的超級塊不同,VFS超級塊是存在于內存中的結構,它在邏輯文件系統安裝時(shí)建立,并且在文件系統卸載時(shí)自動(dòng)刪除,因此,VFS對于每一個(gè)邏輯文件系統,都有一個(gè)對應的VFS超級塊。 VFS超級塊在include/fs/fs.h中定義,即數據結構super_block,該結構主要定義如下: struct super_block { struct list_head s_list; /* Keep this first */ kdev_t s_dev; unsigned long s_blocksize; unsigned char s_blocksize_bits; unsigned char s_lock; unsigned char s_dirt; unsigned long long s_maxbytes; /* Max file size */ struct file_system_type *s_type; struct super_operations *s_op; struct dquot_operations *dq_op; unsigned long s_flags; unsigned long s_magic; struct dentry *s_root; wait_queue_head_t s_wait; struct list_head s_dirty; /* dirty inodes */ struct list_head s_files; struct block_device *s_bdev; struct list_head s_mounts; /* vfsmount(s) of this one */ struct quota_mount_options s_dquot; /*Diskquota specific options */
union { struct minix_sb_info minix_sb; struct ext2_sb_info ext2_sb; …… …… void *generic_sbp; } u; struct semaphore s_vfs_rename_sem; /*Kludge */ struct semaphore s_nfsd_free_path_sem; }; 下面對該結構的主要域進(jìn)行一個(gè)簡(jiǎn)單的分析: s_list:所有已裝載文件系統的雙向鏈表(參考 linux/list.h)。 s_dev:裝載該文件系統的設備(可以是匿名設備)標識號,舉例來(lái)說(shuō),對于/dev/hda1,其設備標識號為ox301。 s_blocksize:該文件系統的基本數據塊的大小。以字節為單位,并且必須是2的n次方。 s_blocksize_bits:塊大小所占的位數,即log2(s_blocksize)。 s_lock:用來(lái)指出當前超級塊是否被鎖住。 s_wait:這是一個(gè)等待隊列,其中的進(jìn)程都在等待該超級塊的s_lock。 s_dirt:這是一個(gè)標志位。當超級塊被改變時(shí),將置位;當超級塊被寫(xiě)入設備時(shí),將清位。(當文件系統被卸載或者調用sync 時(shí),有可能會(huì )將超級塊寫(xiě)入設備。) s_type:指向文件系統的file_system_type結構。 s_op:指向一個(gè)超級塊操作集super_operations,我們將在后面進(jìn)行討論。 dq_op:指向一個(gè)磁盤(pán)限額(DiscQuota)操作集。 s_flags:這是一組操作權限標志,它將與索引節點(diǎn)的標志進(jìn)行邏輯或操作,從而確定某一特定的行為。這里有一個(gè)標志,可以應用于整個(gè)文件系統,就是MS_RDONLY。一個(gè)設置了如此標志的文件系統將被以只讀的方式裝載,任何直接或者間接的寫(xiě)操作都被禁止,包括超級塊中裝載時(shí)間和文件訪(fǎng)問(wèn)時(shí)間的改變等等。 s_root:這是一個(gè)指向dentry結構的指針。它指向該文件系統的根。通常它是由裝載文件系統的根結點(diǎn)(root inode)時(shí)創(chuàng )建的,并將它傳遞給d_alloc_root。這個(gè)dentry將被mount命令加入到dcache中。 s_dirty:“臟”索引節點(diǎn)的鏈表。當一個(gè)索引節點(diǎn)被mark_inode_dirty標志為“臟”時(shí),該索引節點(diǎn)將被放入到這個(gè)鏈表中;當sync_inode被調用時(shí),這個(gè)鏈表中的所有索引節點(diǎn)將被傳遞給該文件系統的write_inode方法。 s_files:該文件系統所有打開(kāi)文件的鏈表。 u.generic_sbp:在聯(lián)合結構u中,包括了一個(gè)文件系統特定的超級塊信息,在上面的結構中,我們可以看到有minix_sb 和ext2_sb 等等結構。這些信息是編譯時(shí)可知的信息,對于那些當作模塊裝載的文件系統,則必須分配一個(gè)單獨的結構,并且將地址放入u.generic_sbp中。 s_vfs_rename_sem:這個(gè)信號量可以在整個(gè)文件系統的范圍內使用,當重命名一個(gè)目錄的時(shí)候,將使用它來(lái)進(jìn)行鎖定。這是為了防止把一個(gè)目錄重命名為它自己的子目錄。當重命名的目標不是目錄時(shí),則不使用該信號量。 針對上面的超級塊,定義了一組方法,也叫作操作,在結構super_operations中: struct super_operations { 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 *); 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 *); }; 因此在實(shí)現實(shí)現自己的邏輯文件系統時(shí),我們必須提供一套自己的超級塊操作函數。對這些函數的調用都來(lái)自進(jìn)程正文(process context),而不是來(lái)自在中斷例程或者bottom half,并且,所有的方法調用時(shí),都會(huì )使用內核鎖,因此,操作可以安全地阻塞,但我們也要避免并發(fā)地訪(fǎng)問(wèn)它們。 根據函數的名字,我們可以大概地了解其功能,下面簡(jiǎn)單地介紹一下: read_inode:該方法是從一個(gè)裝載的文件系統中讀取一個(gè)指定的索引節點(diǎn)。它由get_new_inode調用,而get_new_inode則由fs/inode.c中的iget調用。一般來(lái)說(shuō),文件系統使用iget來(lái)讀取特定的索引節點(diǎn)。 write_inode:當一個(gè)文件或者文件系統要求sync時(shí),該方法會(huì )被由mark_inode_dirty標記為“臟”的索引節點(diǎn)調用,用來(lái)確認所有信息已經(jīng)寫(xiě)入設備。 put_inode:如果該函數被定義了,則每當一個(gè)索引節點(diǎn)的引用計數減少時(shí),都會(huì )被調用。這并不意味著(zhù)該索引節點(diǎn)已經(jīng)沒(méi)人使用了,僅僅意味著(zhù)它減少了一個(gè)用戶(hù)。要注意的是,put_inode在i_count減少之前被調用,所以,如果put_inode想要檢查是否這是最后一個(gè)引用,則應檢查i_count是否為1。大多數文件系統都會(huì )定義該函數,用來(lái)在一個(gè)索引節點(diǎn)的引用計數減少為0之前做一些特殊的工作。 delete_inode:如果被定義,則當一個(gè)索引節點(diǎn)的引用計數減少至0,并且鏈接計數(i_nlink)也是0的時(shí)候,便調用該函數。以后,這個(gè)函數有可能會(huì )與上一個(gè)函數合并。 notify_change:當一個(gè)索引節點(diǎn)的屬性被改變時(shí),會(huì )調用該函數。它的參數struct iattr *指向一個(gè)新的屬性組。如果一個(gè)文件系統沒(méi)有定義該方法(即NULL),則VFS會(huì )調用例程fs/iattr.c:inode_change_ok,該方法實(shí)現了一個(gè)符合POSIX標準的屬性檢驗,然后VFS會(huì )將該索引節點(diǎn)標記為“臟”。如果一個(gè)文件系統實(shí)現了自己的notify_change方法,則應該在改變屬性后顯式地調用mark_inode_dirty(inode)方法。 put_super:在umount(2)系統調用的最后一步,即將入口從vfsmntlist中移走之前,會(huì )調用該函數。該函數調用時(shí),會(huì )對super_block上鎖。一般來(lái)說(shuō),文件系統會(huì )針對這個(gè)裝載實(shí)例,釋放特有的私有資源,比如索引節點(diǎn)位圖,塊位圖。如果該文件系統是由動(dòng)態(tài)裝載模塊實(shí)現的,則一個(gè)buffer header將保存該super_block,并且減少模塊使用計數。 write_super:當VFS決定要將超級塊寫(xiě)回磁盤(pán)時(shí),會(huì )調用該函數。有三個(gè)地方會(huì )調用它:fs/buffer.c:fs_fsync,fs/super.c:sync_supers和fs/super.c:do_umount,顯然只讀文件系統不需要這個(gè)函數。 statfs:這個(gè)函數用來(lái)實(shí)現系統調用statfs(2),并且如果定義了該函數,會(huì )被fs/open.c:sys_statfs調用,否則將返回ENODEV錯誤。 remountfs:當文件系統被重新裝載時(shí),也就是說(shuō),當mount(2)系統調用的標志MS_REMOUNT被設置時(shí),會(huì )調用該函數。一般用來(lái)在不卸載文件系統的情況下,改變不同的裝載參數。比如,把一個(gè)只讀文件系統變成可寫(xiě)的文件系統。 clear_inode:可選方法。當VFS清除索引節點(diǎn)的時(shí)候,會(huì )調用該方法。當一個(gè)文件系統使用了索引節點(diǎn)結構中的generic_ip域,向索引節點(diǎn)增加了特別的(使用kmalloc動(dòng)態(tài)分配的)數據時(shí),便需要此方法來(lái)做相應的處理。 2 VFS的文件及其操作 文件對象使用在任何需要讀寫(xiě)的地方,包括通過(guò)文件系統,或者管道以及網(wǎng)絡(luò )等進(jìn)行通訊的對象。 文件對象和進(jìn)程關(guān)系緊密,進(jìn)程通過(guò)文件描述符(file descriptors)來(lái)訪(fǎng)問(wèn)文件。文件描述符是一個(gè)整數,linux通過(guò)fs.h中定義的NR_OPEN來(lái)規定每個(gè)進(jìn)程最多同時(shí)使用的文件描述符個(gè)數: #define NR_OPEN (1024*1024) 一共有三個(gè)與進(jìn)程相關(guān)的結構,第一個(gè)是files_struct,在include/linux/sched.h中定義,主要是一個(gè)fd數組,數組的下標是文件描述符,其內容就是對應的下面將要介紹的file結構。 另外一個(gè)結構是fs_struct,主要有兩個(gè)指針,pwd指向當前工作目錄的索引節點(diǎn);root指向當前工作目錄所在文件系統的根目錄的索引節點(diǎn)。 最后一個(gè)結構是file結構,定義它是為了保證進(jìn)程對文件的私有記錄,以及父子進(jìn)程對文件的共享,這是一個(gè)非常巧妙的數據結構。我們將在下面進(jìn)行詳細的分析。 上述結構與進(jìn)程的關(guān)系如下圖所示:
仔細分析其聯(lián)系,對于我們理解進(jìn)程對文件的訪(fǎng)問(wèn)操作很有幫助。 結構file定義在linux/fs.h中: struct file { struct list_head f_list; struct dentry *f_dentry; struct vfsmount *f_vfsmnt; struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct fown_struct f_owner; unsigned int f_uid, f_gid; int f_error;
unsigned long f_version;
/* needed for tty driver, and maybe others */ void *private_data; }; 下面對其作一個(gè)簡(jiǎn)單的分析: f_list:該域將文件鏈接到打開(kāi)的文件鏈表中,鏈表由超級塊中的s_files開(kāi)始。 f_dentry:該域指向該文件的索引節點(diǎn)的dcache入口。如果文件的索引節點(diǎn)不在普通的文件系統中,而是諸如管道pipe之類(lèi)的對象,那么,dentry將是一個(gè)由d_alloc_root創(chuàng )建的root dentry。 f_vfsmnt:該域指向該文件所在文件系統的vfsmount結構。 f_op:指向應用于文件的操作集。 f_count:引用該文件的計數。是用戶(hù)進(jìn)程的引用數加上內部的引用數。 f_flags:該域存儲了進(jìn)程對該文件的訪(fǎng)問(wèn)類(lèi)型,比如O_NONBLOCK,O_APPEND等等。有些標志比如O_EXCL,O_CREAT等等,只在打開(kāi)文件的時(shí)候使用,因此并不存儲在f_flags中。 f_mode:對文件的操作標志,只讀,只寫(xiě),以及讀寫(xiě)。 f_pos:該域存儲了文件的當前位置。 f_reada, f_ramax, f_raend, f_ralen, f_rawin:這五個(gè)域用來(lái)跟蹤對文件的連續訪(fǎng)問(wèn),并決定預讀多少內容。 f_owner:該結構存儲了一個(gè)進(jìn)程id,以及當特定事件發(fā)生在該文件時(shí)發(fā)送的一個(gè)信號,比如當有新數據到來(lái)的時(shí)候等等。 f_uid, f_gid:打開(kāi)該文件的進(jìn)程的uid和gid,沒(méi)有實(shí)際的用途。 f_version:用來(lái)幫助底層文件系統檢查cache的狀態(tài)是否合法。當f_pos變化時(shí),它的值就會(huì )發(fā)生變化。 private_data:這個(gè)域被許多設備驅動(dòng)所使用,有些特殊的文件系統為每一個(gè)打開(kāi)的文件都保存一份額外的數據(如coda),也會(huì )使用這個(gè)域。 下面我們看一看針對文件的操作,在file結構中,有一個(gè)指針指向了一個(gè)文件操作集file_operations,它在linux/fs.h中被定義: 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 *); }; 這些操作用來(lái)將VFS對file結構的操作轉化為邏輯文件系統處理相應操作的函數。因此,要了解一個(gè)邏輯文件系統,就要從這些接口函數入手。下面對這些操作進(jìn)行一個(gè)簡(jiǎn)單的分析: llseek:該函數用來(lái)實(shí)現lseek系統調用。如果它沒(méi)有定義,則缺省執行fs/read_write.c中的default_llseek函數。它將更新fs_pos域,并且,也有可能會(huì )改變f_reada和f_version域。 read:該函數用來(lái)實(shí)現read系統調用,同時(shí)也支持其他諸如裝載可執行文件等等操作。 write:該方法用來(lái)寫(xiě)文件。但它并不關(guān)心數據是否真正寫(xiě)入到設備,只將數據放入隊列中。 readdir:該函數從一個(gè)假定為目錄的文件讀取目錄結構,并且使用回調函數filldir_t將其返回。當readdir到達目錄的結尾處時(shí),它會(huì )返回0。 poll:該函數用來(lái)實(shí)現select和poll系統調用。 ioctl:該函數實(shí)現專(zhuān)門(mén)的ioctl功能。如果一個(gè)ioctl請求不在標準請求中(FIBMAP,FIGETBSZ,FIONREAD),那么該請求將傳遞給底層的文件實(shí)現。 mmap:該例程用來(lái)實(shí)現文件的內存映射。它通常使用generic_file_map來(lái)實(shí)現。使用它的任務(wù)會(huì )被檢驗是否允許作映射,并且會(huì )設置vm_area_struct中的vm_ops。 open:如果該方法被定義,那么當一個(gè)新的文件在索引節點(diǎn)上被打開(kāi)時(shí),會(huì )調用它。它可以做一些打開(kāi)文件所必須的設置。在許多文件系統上,都不需要它。一個(gè)例外是coda,它需要在打開(kāi)時(shí)試圖獲得本地緩存的文件。 flush:當一個(gè)文件描述符被關(guān)閉時(shí),會(huì )調用該函數。由于此時(shí)可能有其他的描述符在該文件上被打開(kāi),因此,它并不意味著(zhù)該文件被最終關(guān)閉。目前在文件系統中,只有NFS的客戶(hù)端定義了該方法。 release:當文件的最后一個(gè)句柄被關(guān)閉時(shí),release將被調用。它會(huì )做一些必要的清理工作。該函數不能向任何方面返回錯誤值,因此應該將其定義為void。 fsync:該方法用來(lái)實(shí)現fsync和fdatasync系統調用(它們一般是相同的)。它將一直等到所有對該文件掛起的寫(xiě)操作全部成功寫(xiě)到設備后才返回。fsync可以部分地通過(guò)generic_buffer_fdatasync實(shí)現,這個(gè)函數將索引節點(diǎn)映射的頁(yè)面中所有標記為臟的緩沖區,全部寫(xiě)回。 fasync:該方法在一個(gè)文件的FIOASYNC標志被改變的時(shí)候被調用。它的int類(lèi)型的參數包含了該標志位的新值。目前還沒(méi)有文件系統實(shí)現該方法。 lock:該方法允許一個(gè)文件服務(wù)提供額外的POSIX鎖。它不被FLOCK類(lèi)型的鎖使用,它對于網(wǎng)絡(luò )文件系統比較有用。 3 VFS索引節點(diǎn)及其操作 Linux維護了一個(gè)活動(dòng)的及最近使用過(guò)的索引節點(diǎn)的高速緩存(cache)。有兩種方法來(lái)訪(fǎng)問(wèn)這些索引節點(diǎn)。第一種是通過(guò)dcache,我們將在下一節介紹。在dcache中的每一個(gè)dentry都指向一個(gè)索引節點(diǎn),并且因此而將索引節點(diǎn)維護在緩存中。第二種方法是通過(guò)索引節點(diǎn)的哈希表。每一個(gè)索引節點(diǎn)都被基于該文件系統超級塊的地址和索引節點(diǎn)的編號,被哈希為一個(gè)8位的數字。所有擁有同樣哈希值的索引節點(diǎn)通過(guò)雙項鏈表被鏈接在一起。 通過(guò)哈希表訪(fǎng)問(wèn)是通過(guò)函數iget而實(shí)現的。iget只被個(gè)別的文件系統實(shí)現所調用(當索引節點(diǎn)不再dcache中而進(jìn)行查找的時(shí)候)。 下面我們來(lái)分析索引節點(diǎn)inode的結構,在include/linux/fs.h中有inode的定義: struct inode { struct list_head i_hash; struct list_head i_list; struct list_head i_dentry;
struct list_head i_dirty_buffers;
unsigned long i_ino; atomic_t i_count; kdev_t i_dev; umode_t i_mode; nlink_t i_nlink; uid_t i_uid; gid_t i_gid; kdev_t i_rdev; loff_t i_size; time_t i_atime; time_t i_mtime; time_t i_ctime; unsigned long i_blksize; unsigned long i_blocks; unsigned long i_version; unsigned short i_bytes; struct semaphore i_sem; struct semaphore i_zombie; struct inode_operations *i_op; struct file_operations *i_fop; struct super_block * i_shadow; struct inode_shadow_operations * i_shadow_op; struct super_block *i_sb; wait_queue_head_t i_wait; struct file_lock *i_flock; struct address_space *i_mapping; struct address_space i_data; struct dquot *i_dquot[MAXQUOTAS]; struct pipe_inode_info *i_pipe; struct block_device *i_bdev;
unsigned long i_dnotify_mask; /* Directory notify events */ struct dnotify_struct *i_dnotify; /* for directory notifications */
unsigned long i_state;
unsigned int i_flags; unsigned char i_sock;
atomic_t i_writecount; unsigned int i_attr_flags; __u32 i_generation; union { struct minix_inode_info minix_i; struct ext2_inode_info ext2_i; ……… (略) struct proc_inode_info proc_i; struct socket socket_i; struct usbdev_inode_info usbdev_i; struct supermount_inode_info supermount_i; void *generic_ip; } u; }; 下面我們對它所一個(gè)分析,在上面的結構中,大部分字段的意義都很明顯,因此我們將對一些特殊的字段(針對linux)和一些特殊的地方進(jìn)行分析。 i_hash:i_hash將所有擁有相同哈希值的索引節點(diǎn)鏈接在一起。哈希值基于超級塊結構的地址和索引節點(diǎn)的索引號。 i_list:i_list用來(lái)將索引節點(diǎn)鏈接到不同的狀態(tài)上。inode_in_use鏈表將正在使用的未改變的索引節點(diǎn)鏈接在一起,inode_unused將未使用的索引節點(diǎn)鏈接在一起,而superblock->s_dirty維護指定文件系統內所有標記為“臟”的索引節點(diǎn)。 i_dentry:i_dentry鏈表中,鏈接了所有引用該索引節點(diǎn)的dentry結構。它們通過(guò)dentry中的d_alias鏈接在一起。 i_version:它被文件系統用來(lái)記錄索引節點(diǎn)的改變。一般來(lái)說(shuō),i_version被設置為全局變量event的值,然后event回自增。有時(shí)候,文件系統的代碼會(huì )把i_version的當前值分配給相關(guān)的file結構中的f_version,在隨后file結構的應用中,它可以被用來(lái)高速我們,inode是否被改變了,如果需要的話(huà),在file結構中緩存的數據要被刷新。 i_sem:這個(gè)信號燈用來(lái)保護對inode的改變。所有對inode的非原子操作代碼,都要首先聲明該信號燈。這包括分配和銷(xiāo)毀數據塊,以及通過(guò)目錄進(jìn)行查找等等操作。并且,不能對只讀操作聲明共享鎖。 i_flock:它指向在該inode上加鎖的file_lock結構鏈表。 i_state:對于2.4內核來(lái)說(shuō),共有六種可能的inode狀態(tài):I_DIRTY_SYNC, I_DIRTY_DATASYNC, I_DIRTY_PAGES, I_LOCK, I_FREEING和 I_CLEAR。所有臟節點(diǎn)在相應超級塊的s_dirty鏈表中,并且在下一次同步請求時(shí)被寫(xiě)入設備。在索引節點(diǎn)被創(chuàng )建,讀取或者寫(xiě)入的時(shí)候,會(huì )被鎖住,即I_LOCK狀態(tài)。當一個(gè)索引節點(diǎn)的引用計數和鏈接計數都到0時(shí),將被設置為I_CLEAR狀態(tài)。 i_flags:i_flags對應于超級塊中的s_flags,有許多標記可以被系統范圍內設置,也可以針對每個(gè)索引節點(diǎn)設置。 i_writecount:如果它的值為正數,那么它就記錄了對該索引節點(diǎn)有寫(xiě)權限的客戶(hù)(文件或者內存映射)的個(gè)數。如果是負數,那么該數字的絕對值就是當前VM_DENYWRITE映射的個(gè)數。其他情況下,它的值為0。 i_attr_flags:未被使用。 最后要注意的是,在linux 2.4中,inode結構中新增加了一項,就是struct file_operations *i_fop,它指向索引節點(diǎn)對應的文件的文件操作集,而原來(lái)它放在inode_operations中(即inode結構的另一個(gè)項目struct inode_operations *i_op之中),現在它已經(jīng)從inode_operations中移走了,我們可以從下面對inode_operations結構的分析中看到這一點(diǎn)。 下面我們分析一下對于inode進(jìn)行操作的函數。所有的方法都放在inode_operations結構中,它在include/linux/fs.h中被定義: struct inode_operations { int (*create) (struct inode *,struct dentry *,int); struct dentry * (*lookup) (struct inode *,struct dentry *); 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 *,int); int (*rmdir) (struct inode *,struct dentry *); int (*mknod) (struct inode *,struct dentry *,int,int); int (*rename) (struct inode *, struct dentry *, struct inode *, struct dentry *); int (*readlink) (struct dentry *, char *,int); int (*follow_link) (struct dentry *, struct nameidata *); void (*truncate) (struct inode *); int (*permission) (struct inode *, int); int (*revalidate) (struct dentry *); int (*setattr) (struct dentry *, struct iattr *); int (*getattr) (struct dentry *, struct iattr *); }; 同樣,我們對這些方法做一個(gè)簡(jiǎn)單的分析。 create:這個(gè)方法,以及下面的8個(gè)方法,都只在目錄索引節點(diǎn)中被維護。 當VFS想要在給定目錄創(chuàng )建一個(gè)給定名字(在參數dentry中)的新文件時(shí),會(huì )調用該函數。VFS將提前確定該名字并不存在,并且作為參數的dentry必須為負值(即其中指向inode的指針為NULL,根據include/dcache.h中的定義,其注釋為“NULL is negative”)。 如果create調用成功,將使用get_empty_inode從cache中得到一個(gè)新的空索引節點(diǎn),填充它的內容,并使用insert_inode_hash將其插入到哈希表中,使用mark_inode_dirty標記其為臟,并且使用d_instantiate將其在dcache中實(shí)例化。 int參數包含了文件的mode并指定了所需的許可位。 lookup:該函數用來(lái)檢查是否名字(由dentry提供)存在于目錄(由inode提供)中,并且如果存在的話(huà),使用d_add更新dentry。 link:該函數用來(lái)將一個(gè)名字(由第一個(gè)dentry提供)硬鏈接到在在指定目錄(由參數inode提供)中的另一個(gè)名字(由第二個(gè)dentry參數提供)。 unlink:刪除目錄中(參數inode指定)的名字(由參數dentry提供)。 symlink:創(chuàng )建符號鏈接。 mkdir:根據給定的父節點(diǎn),名字和模式,創(chuàng )建一個(gè)目錄。 rmdir:移除指定的目錄(如果為空目錄),并刪除(d_delete)dentry。 mknod:根據給定的父節點(diǎn),名字,模式以及設備號,創(chuàng )建特殊的設備文件,然后使用d_instantiate將新的inode在dentry中實(shí)例化。 rename:重命名。所有的檢測,比如新的父節點(diǎn)不能是舊名字的孩子等等,都已經(jīng)在調用前被完成。 readlink:通過(guò)dentry參數,讀取符號鏈接,并且將其拷貝到用戶(hù)空間,最大長(cháng)度由參數int指定。 permission:在該函數中,可以實(shí)現真正的權限檢查,與文件本身的mode無(wú)關(guān)。 4 VFS名字以及dentry 根據我們上面的介紹,可以看出,文件和索引節點(diǎn)的聯(lián)系非常緊密,而在文件和索引節點(diǎn)之間,是通過(guò)dentry結構來(lái)聯(lián)系的。 VFS層處理了文件路徑名的所有管理工作,并且在底層文件系統能夠看到它們之前,將其轉變?yōu)閐cache中的入口(entry)。唯一的一個(gè)例外是對于符號鏈接的目標,VFS將不加改動(dòng)地傳遞給底層文件系統,由底層文件系統對其進(jìn)行解釋。 目錄高速緩存dcache由許多dentry結構組成。每一個(gè)dentry都對應文件系統中的一個(gè)文件名,并且與之聯(lián)系。每一個(gè)dentry的父節點(diǎn)都必須存在于dcache中。同時(shí),dentry還記錄了文件系統的裝載關(guān)系。 dcache是索引節點(diǎn)高速緩存的管理者。不論何時(shí),只要在dcache中存在一個(gè)入口,那么相應的索引節點(diǎn)一定在索引節點(diǎn)高速緩存中。換句話(huà)說(shuō),如果一個(gè)索引節點(diǎn)在高速緩存中,那么它一定引用dcache中的一個(gè)dentry。 下面我們來(lái)分析一下dentry的結構,以及在dentry上的操作。在include/linux/dcache.h中,由其定義: struct dentry { atomic_t d_count; unsigned int d_flags; struct inode * d_inode; /* Where the name belongs to - NULL is negative */ struct dentry * d_parent; /* parent directory */ struct list_head d_vfsmnt; struct list_head d_hash; /* lookup hash list */ struct list_head d_lru; /* d_count = 0 LRU list */ struct list_head d_child; /* child of parent list */ struct list_head d_subdirs; /* our children */ struct list_head d_alias; /* inode alias list */ struct qstr d_name; unsigned long d_time; /* used by d_revalidate */ struct dentry_operations *d_op; struct super_block * d_sb; /* The root of the dentry tree */ unsigned long d_reftime; /* last time referenced */ void * d_fsdata; /* fs-specific data */ unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */ }; 在該結構的注釋中,大部分域的含義已經(jīng)非常的清楚,下面我再簡(jiǎn)單地介紹一下。 d_flags:在目前,只有兩個(gè)可取值,而且都是給特殊的文件系統使用的,它們是DCACHE_AUTOFS_PENDING和DCACHE_NFSFS_RENAMED,因此,在這里我們可以暫時(shí)忽略它。 d_inode:它簡(jiǎn)單地指向與該名字聯(lián)系的索引節點(diǎn)。這個(gè)域可以是NULL,它標明這是一個(gè)負入口(negative entry),暗示著(zhù)該名字并不存在。 d_hash:這是一個(gè)雙向鏈表,將所有擁有相同哈希值的入口鏈接在一起。 d_lru:它提供了一個(gè)雙向鏈表,鏈接高速緩存中未被引用的葉節點(diǎn)。這個(gè)鏈表的頭是全局變量dentry_unused,按照最近最少使用的順序存儲。 d_child:這是一個(gè)容易讓人誤會(huì )的名字,其實(shí),該鏈表鏈接d_parent的所有子節點(diǎn),因此把它稱(chēng)為d_sibling(同胞)更恰當一些。 d_subdirs:該鏈表將該dentry的所有子節點(diǎn)鏈接在一起,所以,它實(shí)際上是它子節點(diǎn)的d_child鏈表的鏈表頭。這個(gè)名字也容易產(chǎn)生誤會(huì ),因為它的子節點(diǎn)不僅僅包括子目錄,也可以是文件。 d_alias:由于文件(以及文件系統的其他一些對象)可能會(huì )通過(guò)硬鏈接的方法,擁有多個(gè)名字,因此有可能會(huì )有多個(gè)dentry指向同一個(gè)索引節點(diǎn)。在這種情況下,這些dentry將通過(guò)d_alias鏈接在一起。而inode的i_dentry就是該鏈表的頭。 d_name:該域包含了這個(gè)入口的名字,以及它的哈希值。它的子域name有可能會(huì )指向該dentry的d_iname域(如果名字小于等于16個(gè)字符),否則的話(huà),它將指向一個(gè)單獨分配出來(lái)的字符串。 d_op:指向dentry的操作函數集。 d_sb:指向該dentry對應文件所在的文件系統的超級塊。使用d_inode->i_sb有相同的效果。 d_iname:它存儲了文件名的前15個(gè)字符,目的是為了方便引用。如果名字適合,d_name.name將指向這里。
下面我們再看一下對dentry的操作函數,同樣在include/linux/dcache.h中有dentry_operations的定義: struct dentry_operations { int (*d_revalidate)(struct dentry *, int); int (*d_hash) (struct dentry *, struct qstr *); int (*d_compare) (struct dentry *, struct qstr *, struct qstr *); int (*d_delete)(struct dentry *); void (*d_release)(struct dentry *); void (*d_iput)(struct dentry *, struct inode *); }; 我們再簡(jiǎn)單地介紹一下: d_revalidate:這個(gè)方法在entry在dcache中做路徑查找時(shí)調用,目的是為了檢驗這個(gè)entry是否依然合法。如果它依舊可以被信賴(lài),則返回1,否則返回0。 d_hash:如果文件系統沒(méi)有提供名字驗證的規則,那么這個(gè)例程就被用來(lái)檢驗并且返回一個(gè)規范的哈希值。 d_compare:它被用來(lái)比較兩個(gè)qstr,來(lái)看它們是否是相同的。 d_delete:當引用計數到0時(shí),在這個(gè)dentry被放到dentry_unused鏈表之前,會(huì )調用該函數。 d_release:在一個(gè)dentry被最終釋放之前,會(huì )調用該函數。 d_iput:如果定義了該函數,它就被用來(lái)替換iput,來(lái)dentry被丟棄時(shí),釋放inode。它被用來(lái)做iput的工作再加上其他任何想要做的事情。
(五) 總結 在上面的部分中,我們對VFS進(jìn)行了一個(gè)大概的分析。了解了文件系統的注冊,安裝,以及卸載的過(guò)程,并對VFS對邏輯文件系統的管理,尤其是接口部分的數據結構,進(jìn)行了詳細的分析。 proc文件系統作為一個(gè)特殊的邏輯文件系統,其實(shí)現也遵循VFS接口,因此,根據上面對VFS的分析,我們可以基本確定對proc文件系統進(jìn)行分析的步驟。
|