zmwillow (journeyman)不知正確與否。 這樣是表示對作者的尊重。 如果有不對,請通知我,我將即時更改. 一 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 定義了一個抽象層,即虛擬文件系統VFS,又叫作虛擬文件系統轉換(Virtual Filesystem Switch)。VFS 是 Linux 內核中的一個子系統,其他的子系統,如IPC,SCHED,MM,NET,都只與 VFS 聯系,換句話說,具體的邏輯文件系統對于 Linux 內核中的其他子系統是透明的。 而proc文件系統,對于 Linux 來說,正是一個邏輯文件系統,因此 proc 文件系統的實現,也完全遵循 VFS 的規范,在對 proc 文件系統進行分析之前,我們必須對 VFS 進行一個詳細的分析。 (一) 基本設計原理 對于邏輯文件系統來說,VFS 是一個管理者,而對于內核的其他部分,則是一個接口,整個linux 中文件系統的邏輯關系,如圖2.1.1所示。 VFS提供了一個統一的接口(即幾個有關操作的數據結構),一個邏輯文件系統要想被 Linux 支持,那么就必須按照這個接口來編寫自己的操作函數,從而將自己的細節對其他子系統隱藏起來。因而,對于內核其他子系統來說,所有的文件系統都是一樣的。 (二) 基本對象與方法 虛擬文件系統的接口由一組對象及其由這些對象調用的一組方法所構成的。這些基本的對象是 files(文件),file-systems(文件系統),inodes (索引節點)以及 names for inodes(索引節點名字),下面對這些對象進行簡單的介紹: 1 Files: 文件是一個可讀可寫的對象,它也可以映射到內存中,這和 UNIX 中文件描述符的概念很接近。文件在 Linux 中使用一個"struct file"結構來實現,并且該結構有一組操作函數,保存在結構"struct file_operations"中。
圖 2.1.1 2 Inodes: 索引節點是文件系統中的基本對象。它可以是一個正常文件,一個目錄,一個符號鏈接,或者是其他什么東西。VFS 并不明顯地區分這些對象,而把它們留給真正的文件系統,讓它們自己實現適宜的行為。從而使內核的高層子系統對于不同的對象區別對待。 每一個 索引節點節點都由一個"struct inode"結構表現,它的一組方法保存在結構"struct inode_operations"中。 文件(Files)和索引節點(Inodes)也許看起來很相像,但它們之間有一些非常重要的不同,要注意的一點是,有些東西有索引節點,但卻沒有文件,比如,一個符號鏈接。與之相對應,有些文件卻沒有索引節點,如管道(pipes)和 sockets。 3 File_systems 文件系統就是 inode 的集合,其中有一個不同的節點,被稱為根結點(root)。其他的 inode 以 root 為起始點進行訪問,并且通過文件名來查找其他的 inode 。 每一個文件系統有一組唯一的特征,應用于本文件系統內的所有 inode 之上。其中有一些是標志,比如只讀 READ-ONLY 標志。另一個重要的內容是 blocksize。 每一個文件系統都通過一個結構"struct super_block"來表現,而針對超級塊的一組方法則存儲在結構"struct super_operations"之中。 在 Linux 中,超級塊(super-blocks)和 設備號(device number)之間有緊密的聯系。每一個文件系統必須有一個唯一的設備號,該文件系統即建立在此設備之上。有一些文件系統(比如 nfs 和 我們要研究的 proc 文件系統)被標志為不需要真實的設備,因此,對于這些文件系統,主設備號(major number)為0的匿名設備將會自動地分配給它們。 Linux VFS 了解不同的文件系統類型,每一個文件系統類型都使用一個"struct file_system_type"結構來表示,在這個結構中,只包含一個方法,即 "read_super",使用這個方法來實例化一個指定文件系統的超級塊。 4 Names 在一個文件系統內,所有的 inodes 都是通過名字來訪問的。由于對于某些文件系統來說,名字到 inode 的轉換非常耗時的,因此,Linux 的 VFS 層為當前活動的和最近使用的名字維護了一個 cache,這個 cache 被稱為 目錄高速緩存(dcache)。 dcache 在內存中組織為樹狀結構。樹中的每一個節點都對應于一個指定目錄,指定名稱的inode。一個inode可以與多個樹中的節點相聯系。 如果dcache不是一棵完整的文件樹,那么它一定是文件樹的前綴部分,也就是說,如果一個文件樹的節點在cache中,那么該節點的所以祖先也一定在cache中。 每一個樹的節點都使用一個結構"struct dentry"來表現,它的一組方法存儲在"struct dentry_operations"之中。 dentry 在 Files 和 Inodes 之間扮演了中間人的角色。每一個打開的文件都指向一個dentry,而每一個dentry 則指向它所涉及的inode。這意味著,對于每一個打開的文件,該文件的dentry 和該文件所有的父節點都在內存中被cache,這使得被打開文件的全路徑可以更容易地檢測。 (三) 文件系統的注冊和裝載過程 1 文件系統的注冊 在使用一個文件系統之前,必須要對該文件系統進行注冊。在Linux編譯的時候,可以選定支持哪些文件系統,這些編譯進內核的文件系統,在系統引導的時候,就會在VFS中注冊。而如果一個文件系統被編譯為內核可裝載模塊,那么將在模塊安裝的時候進行注冊,在模塊卸載的時候注銷。 每一個文件系統,都會在自己的初始化例程中填寫一個 file_system_type 的數據結構,然后調用注冊函數register_filesystem(struct file_system_type *fs) 進行注冊。下面我們分析一下 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; }; 而文件系統的注冊和注銷函數也在該頭文件中聲明: extern int register_filesystem(struct file_system_type *); extern int unregister_filesystem(struct file_system_type *); 函數 register_filesystem 成功時返回0,當 fs == NULL時返回 -EINVAL,而當fs->next!=NULL 或者已經有同名文件系統注冊時,則返回-EBUSY。當文件系統作為模塊時,必須直接或者間接地在init_module中調用這個注冊函數,而如果要編譯進內核,則必須在fs/filesystem.c中的filesystem_setup中注冊。而unregister_filesystem 則只能在模塊的cleanup_module例程中調用。 所有的已注冊文件系統的 file_system_type 結構最終會形成一個鏈表,被稱之為"注冊鏈表"。下圖即為內核中 file_system_type 的鏈表示意圖,鏈表頭由 file_systems 指定。
2 文件系統的安裝 要真正使用一個文件系統,僅僅注冊是不行的,還必須安裝這個文件系統。在安裝linux時,已經(默認)安裝了EXT2文件系統,作為根文件系統。我們可以在文件/etc/fstab中指定自動安裝的文件系統,和使用mount命令一樣,我們要為每種文件系統的安裝提供三種信息:文件系統的名稱,包含該文件系統的物理設備,以及該文件系統的安裝點。例如下面的命令: mount -t vfat /dev/fd0 /mnt/floppy 將把軟盤(物理設備fd0)中的vfat文件系統安裝到/mnt/floppy目錄上,下面我們分析一下上述命令的執行過程: 尋找對應的文件系統的信息。VFS通過file_systems,在file_system_type組成的鏈表中根據指定的文件系統的名稱查看文件系統的類型信息。 如果在上述鏈表中找到匹配的文件系統,則說明內核支持該文件系統,并已經注冊。否則,說明該文件系統有可能由LKM(LinuxKernelModule)可裝載模塊支持,因此,VFS會請求內核裝入相應的文件系統模塊,此時,該文件系統在VFS中注冊并初始化。 1. 如果VFS仍然找到指定的文件系統,那么將返回錯誤。 2. 然后,VFS檢驗指定的物理塊設備是否已經安裝。如果指定的物理塊設備已經被安裝,那么將返回錯誤。也就是說,一個塊設備只能安裝到一個目錄,不能同時多次安裝。 3. VFS查找新文件系統的安裝點目錄的VFS索引節點。該VFS索引節點可能在索引節點高速緩存中,也有可能需要從安裝點所在的塊設備中讀取。 4. 如果該安裝點目錄已經裝有其他的文件系統,那么將返回錯誤。因為在同一目錄只能同時安裝一個文件系統。 5. VFS安裝代碼為新的文件系統分配超級塊,并將安裝信息傳遞給該文件系統的超級塊讀取例程。系統中所有的VFS超級塊保存在由super_blocks指向的super_block數據結構指針數組中。 6. 文件系統的超級塊讀取例程將對應的文件系統的信息映射到VFS超級塊中。如果在此過程中發生錯誤,例如所讀取的超級塊魔數和指定的文件系統不一致,則返回錯誤。 7. 如果成功安裝,則所有已經安裝的文件系統形成如下圖所示的結構:
已注冊文件示意圖 由圖可知,每一個已經掛裝的文件系統由vfsmount結構描述。所有的vfsmount結構形成了一個鏈表,用vfsmntlist來指向鏈表頭。這個鏈表可以稱為"已安裝文件系統鏈表"。系統中還有另外兩個指向這種結構體的指針,vfsmnttail和mru_vfsmnt分別指向鏈表尾和最近使用過的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; }; 每個vfsmount結構包含該文件系統所在的塊設備號、文件系統安裝點的目錄名稱,以及指向為該文件系統分配的VFS超級塊的指針。而VFS超級塊中則包含描述文件系統的file_system_type結構指針和該文件系統根結點指針。 下面三個函數是用來操作已安裝文件系統鏈表的,它們都在fs/super.c中實現: lookup_vfsmnt():在鏈表中尋找指定設備號的vfsmnt結構,成功則返回指向該結構的指針,否則返回0。 add_vfsmnt():在鏈表尾加入一個vfsmnt結構,返回指向該結構的指針。 remove_vfsmnt():從鏈表中移走指定設備號的vfsmnt結構,并釋放其所占有的內核內存空間。該函數無返回值。 3 文件系統的卸載 當文件系統被卸載的時候,系統將檢查在該文件系統上是否有正被使用。如果有文件正在使用,則不能被卸載。如果該文件系統中的文件或者目錄正在使用,則VFS索引節點高速緩存中可能包含相應的VFS索引節點,檢查代碼將在索引節點高速緩存中,根據文件系統所在的設備標識符,查找是否有來自該文件系統的VFS索引節點,如果有而且使用計數大于0,則說明該文件系統正在被使用。因此,該文件系統不能被卸載。 否則,將查看對應的VFS超級塊,如果該文件系統的VFS超級塊標志為“臟”,那么必須將超級塊信息寫回磁盤。 上述過程結束后,對應的VFS超級塊被釋放,vfsmount數據結構將從vfsmntlist鏈表中斷開并釋放。 (四) VFS 數據結構分析 現在我們已經大致了解了VFS操作的基本過程。下面我們分析一下在VFS中使用的幾個重要的數據結構,它們是VFS實現的核心,更是與邏輯文件系統交互的接口,因此必須進行詳細的分析。 1 VFS超級塊及其操作 許多邏輯文件系統都有超級塊結構,超級塊是這些文件系統中最重要的數據結構,用來描述整個文件系統的信息,是一個全局的數據結構。MINIX、EXT2等都有自己的超級塊,VFS也有超級塊,但和邏輯文件系統的超級塊不同,VFS超級塊是存在于內存中的結構,它在邏輯文件系統安裝時建立,并且在文件系統卸載時自動刪除,因此,VFS對于每一個邏輯文件系統,都有一個對應的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; }; 下面對該結構的主要域進行一個簡單的分析: s_list:所有已裝載文件系統的雙向鏈表(參考 linux/list.h)。 s_dev:裝載該文件系統的設備(可以是匿名設備)標識號,舉例來說,對于/dev/hda1,其設備標識號為ox301。 s_blocksize:該文件系統的基本數據塊的大小。以字節為單位,并且必須是2的n次方。 s_blocksize_bits:塊大小所占的位數,即log2(s_blocksize)。 s_lock:用來指出當前超級塊是否被鎖住。 s_wait:這是一個等待隊列,其中的進程都在等待該超級塊的s_lock。 s_dirt:這是一個標志位。當超級塊被改變時,將置位;當超級塊被寫入設備時,將清位。(當文件系統被卸載或者調用sync 時,有可能會將超級塊寫入設備。) s_type:指向文件系統的file_system_type結構。 s_op:指向一個超級塊操作集super_operations,我們將在后面進行討論。 dq_op:指向一個磁盤限額(DiscQuota)操作集。 s_flags:這是一組操作權限標志,它將與索引節點的標志進行邏輯或操作,從而確定某一特定的行為。這里有一個標志,可以應用于整個文件系統,就是MS_RDONLY。一個設置了如此標志的文件系統將被以只讀的方式裝載,任何直接或者間接的寫操作都被禁止,包括超級塊中裝載時間和文件訪問時間的改變等等。 s_root:這是一個指向dentry結構的指針。它指向該文件系統的根。通常它是由裝載文件系統的根結點(root inode)時創建的,并將它傳遞給d_alloc_root。這個dentry將被mount命令加入到dcache中。 s_dirty:“臟”索引節點的鏈表。當一個索引節點被mark_inode_dirty標志為“臟”時,該索引節點將被放入到這個鏈表中;當sync_inode被調用時,這個鏈表中的所有索引節點將被傳遞給該文件系統的write_inode方法。 s_files:該文件系統所有打開文件的鏈表。 u.generic_sbp:在聯合結構u中,包括了一個文件系統特定的超級塊信息,在上面的結構中,我們可以看到有minix_sb 和ext2_sb 等等結構。這些信息是編譯時可知的信息,對于那些當作模塊裝載的文件系統,則必須分配一個單獨的結構,并且將地址放入u.generic_sbp中。 s_vfs_rename_sem:這個信號量可以在整個文件系統的范圍內使用,當重命名一個目錄的時候,將使用它來進行鎖定。這是為了防止把一個目錄重命名為它自己的子目錄。當重命名的目標不是目錄時,則不使用該信號量。 針對上面的超級塊,定義了一組方法,也叫作操作,在結構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 *); }; 因此在實現實現自己的邏輯文件系統時,我們必須提供一套自己的超級塊操作函數。對這些函數的調用都來自進程正文(process context),而不是來自在中斷例程或者bottom half,并且,所有的方法調用時,都會使用內核鎖,因此,操作可以安全地阻塞,但我們也要避免并發地訪問它們。 根據函數的名字,我們可以大概地了解其功能,下面簡單地介紹一下: read_inode:該方法是從一個裝載的文件系統中讀取一個指定的索引節點。它由get_new_inode調用,而get_new_inode則由fs/inode.c中的iget調用。一般來說,文件系統使用iget來讀取特定的索引節點。 write_inode:當一個文件或者文件系統要求sync時,該方法會被由mark_inode_dirty標記為“臟”的索引節點調用,用來確認所有信息已經寫入設備。 put_inode:如果該函數被定義了,則每當一個索引節點的引用計數減少時,都會被調用。這并不意味著該索引節點已經沒人使用了,僅僅意味著它減少了一個用戶。要注意的是,put_inode在i_count減少之前被調用,所以,如果put_inode想要檢查是否這是最后一個引用,則應檢查i_count是否為1。大多數文件系統都會定義該函數,用來在一個索引節點的引用計數減少為0之前做一些特殊的工作。 delete_inode:如果被定義,則當一個索引節點的引用計數減少至0,并且鏈接計數(i_nlink)也是0的時候,便調用該函數。以后,這個函數有可能會與上一個函數合并。 notify_change:當一個索引節點的屬性被改變時,會調用該函數。它的參數struct iattr *指向一個新的屬性組。如果一個文件系統沒有定義該方法(即NULL),則VFS會調用例程fs/iattr.c:inode_change_ok,該方法實現了一個符合POSIX標準的屬性檢驗,然后VFS會將該索引節點標記為“臟”。如果一個文件系統實現了自己的notify_change方法,則應該在改變屬性后顯式地調用mark_inode_dirty(inode)方法。 put_super:在umount(2)系統調用的最后一步,即將入口從vfsmntlist中移走之前,會調用該函數。該函數調用時,會對super_block上鎖。一般來說,文件系統會針對這個裝載實例,釋放特有的私有資源,比如索引節點位圖,塊位圖。如果該文件系統是由動態裝載模塊實現的,則一個buffer header將保存該super_block,并且減少模塊使用計數。 write_super:當VFS決定要將超級塊寫回磁盤時,會調用該函數。有三個地方會調用它:fs/buffer.c:fs_fsync,fs/super.c:sync_supers和fs/super.c:do_umount,顯然只讀文件系統不需要這個函數。 statfs:這個函數用來實現系統調用statfs(2),并且如果定義了該函數,會被fs/open.c:sys_statfs調用,否則將返回ENODEV錯誤。 remountfs:當文件系統被重新裝載時,也就是說,當mount(2)系統調用的標志MS_REMOUNT被設置時,會調用該函數。一般用來在不卸載文件系統的情況下,改變不同的裝載參數。比如,把一個只讀文件系統變成可寫的文件系統。 clear_inode:可選方法。當VFS清除索引節點的時候,會調用該方法。當一個文件系統使用了索引節點結構中的generic_ip域,向索引節點增加了特別的(使用kmalloc動態分配的)數據時,便需要此方法來做相應的處理。 2 VFS的文件及其操作 文件對象使用在任何需要讀寫的地方,包括通過文件系統,或者管道以及網絡等進行通訊的對象。 文件對象和進程關系緊密,進程通過文件描述符(file descriptors)來訪問文件。文件描述符是一個整數,linux通過fs.h中定義的NR_OPEN來規定每個進程最多同時使用的文件描述符個數: #define NR_OPEN (1024*1024) 一共有三個與進程相關的結構,第一個是files_struct,在include/linux/sched.h中定義,主要是一個fd數組,數組的下標是文件描述符,其內容就是對應的下面將要介紹的file結構。 另外一個結構是fs_struct,主要有兩個指針,pwd指向當前工作目錄的索引節點;root指向當前工作目錄所在文件系統的根目錄的索引節點。 最后一個結構是file結構,定義它是為了保證進程對文件的私有記錄,以及父子進程對文件的共享,這是一個非常巧妙的數據結構。我們將在下面進行詳細的分析。 上述結構與進程的關系如下圖所示:
仔細分析其聯系,對于我們理解進程對文件的訪問操作很有幫助。 結構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; }; 下面對其作一個簡單的分析: f_list:該域將文件鏈接到打開的文件鏈表中,鏈表由超級塊中的s_files開始。 f_dentry:該域指向該文件的索引節點的dcache入口。如果文件的索引節點不在普通的文件系統中,而是諸如管道pipe之類的對象,那么,dentry將是一個由d_alloc_root創建的root dentry。 f_vfsmnt:該域指向該文件所在文件系統的vfsmount結構。 f_op:指向應用于文件的操作集。 f_count:引用該文件的計數。是用戶進程的引用數加上內部的引用數。 f_flags:該域存儲了進程對該文件的訪問類型,比如O_NONBLOCK,O_APPEND等等。有些標志比如O_EXCL,O_CREAT等等,只在打開文件的時候使用,因此并不存儲在f_flags中。 f_mode:對文件的操作標志,只讀,只寫,以及讀寫。 f_pos:該域存儲了文件的當前位置。 f_reada, f_ramax, f_raend, f_ralen, f_rawin:這五個域用來跟蹤對文件的連續訪問,并決定預讀多少內容。 f_owner:該結構存儲了一個進程id,以及當特定事件發生在該文件時發送的一個信號,比如當有新數據到來的時候等等。 f_uid, f_gid:打開該文件的進程的uid和gid,沒有實際的用途。 f_version:用來幫助底層文件系統檢查cache的狀態是否合法。當f_pos變化時,它的值就會發生變化。 private_data:這個域被許多設備驅動所使用,有些特殊的文件系統為每一個打開的文件都保存一份額外的數據(如coda),也會使用這個域。 下面我們看一看針對文件的操作,在file結構中,有一個指針指向了一個文件操作集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 *); }; 這些操作用來將VFS對file結構的操作轉化為邏輯文件系統處理相應操作的函數。因此,要了解一個邏輯文件系統,就要從這些接口函數入手。下面對這些操作進行一個簡單的分析: llseek:該函數用來實現lseek系統調用。如果它沒有定義,則缺省執行fs/read_write.c中的default_llseek函數。它將更新fs_pos域,并且,也有可能會改變f_reada和f_version域。 read:該函數用來實現read系統調用,同時也支持其他諸如裝載可執行文件等等操作。 write:該方法用來寫文件。但它并不關心數據是否真正寫入到設備,只將數據放入隊列中。 readdir:該函數從一個假定為目錄的文件讀取目錄結構,并且使用回調函數filldir_t將其返回。當readdir到達目錄的結尾處時,它會返回0。 poll:該函數用來實現select和poll系統調用。 ioctl:該函數實現專門的ioctl功能。如果一個ioctl請求不在標準請求中(FIBMAP,FIGETBSZ,FIONREAD),那么該請求將傳遞給底層的文件實現。 mmap:該例程用來實現文件的內存映射。它通常使用generic_file_map來實現。使用它的任務會被檢驗是否允許作映射,并且會設置vm_area_struct中的vm_ops。 open:如果該方法被定義,那么當一個新的文件在索引節點上被打開時,會調用它。它可以做一些打開文件所必須的設置。在許多文件系統上,都不需要它。一個例外是coda,它需要在打開時試圖獲得本地緩存的文件。 flush:當一個文件描述符被關閉時,會調用該函數。由于此時可能有其他的描述符在該文件上被打開,因此,它并不意味著該文件被最終關閉。目前在文件系統中,只有NFS的客戶端定義了該方法。 release:當文件的最后一個句柄被關閉時,release將被調用。它會做一些必要的清理工作。該函數不能向任何方面返回錯誤值,因此應該將其定義為void。 fsync:該方法用來實現fsync和fdatasync系統調用(它們一般是相同的)。它將一直等到所有對該文件掛起的寫操作全部成功寫到設備后才返回。fsync可以部分地通過generic_buffer_fdatasync實現,這個函數將索引節點映射的頁面中所有標記為臟的緩沖區,全部寫回。 fasync:該方法在一個文件的FIOASYNC標志被改變的時候被調用。它的int類型的參數包含了該標志位的新值。目前還沒有文件系統實現該方法。 lock:該方法允許一個文件服務提供額外的POSIX鎖。它不被FLOCK類型的鎖使用,它對于網絡文件系統比較有用。 3 VFS索引節點及其操作 Linux維護了一個活動的及最近使用過的索引節點的高速緩存(cache)。有兩種方法來訪問這些索引節點。第一種是通過dcache,我們將在下一節介紹。在dcache中的每一個dentry都指向一個索引節點,并且因此而將索引節點維護在緩存中。第二種方法是通過索引節點的哈希表。每一個索引節點都被基于該文件系統超級塊的地址和索引節點的編號,被哈希為一個8位的數字。所有擁有同樣哈希值的索引節點通過雙項鏈表被鏈接在一起。 通過哈希表訪問是通過函數iget而實現的。iget只被個別的文件系統實現所調用(當索引節點不再dcache中而進行查找的時候)。 下面我們來分析索引節點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; }; 下面我們對它所一個分析,在上面的結構中,大部分字段的意義都很明顯,因此我們將對一些特殊的字段(針對linux)和一些特殊的地方進行分析。 i_hash:i_hash將所有擁有相同哈希值的索引節點鏈接在一起。哈希值基于超級塊結構的地址和索引節點的索引號。 i_list:i_list用來將索引節點鏈接到不同的狀態上。inode_in_use鏈表將正在使用的未改變的索引節點鏈接在一起,inode_unused將未使用的索引節點鏈接在一起,而superblock->s_dirty維護指定文件系統內所有標記為“臟”的索引節點。 i_dentry:i_dentry鏈表中,鏈接了所有引用該索引節點的dentry結構。它們通過dentry中的d_alias鏈接在一起。 i_version:它被文件系統用來記錄索引節點的改變。一般來說,i_version被設置為全局變量event的值,然后event回自增。有時候,文件系統的代碼會把i_version的當前值分配給相關的file結構中的f_version,在隨后file結構的應用中,它可以被用來高速我們,inode是否被改變了,如果需要的話,在file結構中緩存的數據要被刷新。 i_sem:這個信號燈用來保護對inode的改變。所有對inode的非原子操作代碼,都要首先聲明該信號燈。這包括分配和銷毀數據塊,以及通過目錄進行查找等等操作。并且,不能對只讀操作聲明共享鎖。 i_flock:它指向在該inode上加鎖的file_lock結構鏈表。 i_state:對于2.4內核來說,共有六種可能的inode狀態:I_DIRTY_SYNC, I_DIRTY_DATASYNC, I_DIRTY_PAGES, I_LOCK, I_FREEING和 I_CLEAR。所有臟節點在相應超級塊的s_dirty鏈表中,并且在下一次同步請求時被寫入設備。在索引節點被創建,讀取或者寫入的時候,會被鎖住,即I_LOCK狀態。當一個索引節點的引用計數和鏈接計數都到0時,將被設置為I_CLEAR狀態。 i_flags:i_flags對應于超級塊中的s_flags,有許多標記可以被系統范圍內設置,也可以針對每個索引節點設置。 i_writecount:如果它的值為正數,那么它就記錄了對該索引節點有寫權限的客戶(文件或者內存映射)的個數。如果是負數,那么該數字的絕對值就是當前VM_DENYWRITE映射的個數。其他情況下,它的值為0。 i_attr_flags:未被使用。 最后要注意的是,在linux 2.4中,inode結構中新增加了一項,就是struct file_operations *i_fop,它指向索引節點對應的文件的文件操作集,而原來它放在inode_operations中(即inode結構的另一個項目struct inode_operations *i_op之中),現在它已經從inode_operations中移走了,我們可以從下面對inode_operations結構的分析中看到這一點。 下面我們分析一下對于inode進行操作的函數。所有的方法都放在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 *); }; 同樣,我們對這些方法做一個簡單的分析。 create:這個方法,以及下面的8個方法,都只在目錄索引節點中被維護。 當VFS想要在給定目錄創建一個給定名字(在參數dentry中)的新文件時,會調用該函數。VFS將提前確定該名字并不存在,并且作為參數的dentry必須為負值(即其中指向inode的指針為NULL,根據include/dcache.h中的定義,其注釋為“NULL is negative”)。 如果create調用成功,將使用get_empty_inode從cache中得到一個新的空索引節點,填充它的內容,并使用insert_inode_hash將其插入到哈希表中,使用mark_inode_dirty標記其為臟,并且使用d_instantiate將其在dcache中實例化。 int參數包含了文件的mode并指定了所需的許可位。 lookup:該函數用來檢查是否名字(由dentry提供)存在于目錄(由inode提供)中,并且如果存在的話,使用d_add更新dentry。 link:該函數用來將一個名字(由第一個dentry提供)硬鏈接到在在指定目錄(由參數inode提供)中的另一個名字(由第二個dentry參數提供)。 unlink:刪除目錄中(參數inode指定)的名字(由參數dentry提供)。 symlink:創建符號鏈接。 mkdir:根據給定的父節點,名字和模式,創建一個目錄。 rmdir:移除指定的目錄(如果為空目錄),并刪除(d_delete)dentry。 mknod:根據給定的父節點,名字,模式以及設備號,創建特殊的設備文件,然后使用d_instantiate將新的inode在dentry中實例化。 rename:重命名。所有的檢測,比如新的父節點不能是舊名字的孩子等等,都已經在調用前被完成。 readlink:通過dentry參數,讀取符號鏈接,并且將其拷貝到用戶空間,最大長度由參數int指定。 permission:在該函數中,可以實現真正的權限檢查,與文件本身的mode無關。 4 VFS名字以及dentry 根據我們上面的介紹,可以看出,文件和索引節點的聯系非常緊密,而在文件和索引節點之間,是通過dentry結構來聯系的。 VFS層處理了文件路徑名的所有管理工作,并且在底層文件系統能夠看到它們之前,將其轉變為dcache中的入口(entry)。唯一的一個例外是對于符號鏈接的目標,VFS將不加改動地傳遞給底層文件系統,由底層文件系統對其進行解釋。 目錄高速緩存dcache由許多dentry結構組成。每一個dentry都對應文件系統中的一個文件名,并且與之聯系。每一個dentry的父節點都必須存在于dcache中。同時,dentry還記錄了文件系統的裝載關系。 dcache是索引節點高速緩存的管理者。不論何時,只要在dcache中存在一個入口,那么相應的索引節點一定在索引節點高速緩存中。換句話說,如果一個索引節點在高速緩存中,那么它一定引用dcache中的一個dentry。 下面我們來分析一下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 */ }; 在該結構的注釋中,大部分域的含義已經非常的清楚,下面我再簡單地介紹一下。 d_flags:在目前,只有兩個可取值,而且都是給特殊的文件系統使用的,它們是DCACHE_AUTOFS_PENDING和DCACHE_NFSFS_RENAMED,因此,在這里我們可以暫時忽略它。 d_inode:它簡單地指向與該名字聯系的索引節點。這個域可以是NULL,它標明這是一個負入口(negative entry),暗示著該名字并不存在。 d_hash:這是一個雙向鏈表,將所有擁有相同哈希值的入口鏈接在一起。 d_lru:它提供了一個雙向鏈表,鏈接高速緩存中未被引用的葉節點。這個鏈表的頭是全局變量dentry_unused,按照最近最少使用的順序存儲。 d_child:這是一個容易讓人誤會的名字,其實,該鏈表鏈接d_parent的所有子節點,因此把它稱為d_sibling(同胞)更恰當一些。 d_subdirs:該鏈表將該dentry的所有子節點鏈接在一起,所以,它實際上是它子節點的d_child鏈表的鏈表頭。這個名字也容易產生誤會,因為它的子節點不僅僅包括子目錄,也可以是文件。 d_alias:由于文件(以及文件系統的其他一些對象)可能會通過硬鏈接的方法,擁有多個名字,因此有可能會有多個dentry指向同一個索引節點。在這種情況下,這些dentry將通過d_alias鏈接在一起。而inode的i_dentry就是該鏈表的頭。 d_name:該域包含了這個入口的名字,以及它的哈希值。它的子域name有可能會指向該dentry的d_iname域(如果名字小于等于16個字符),否則的話,它將指向一個單獨分配出來的字符串。 d_op:指向dentry的操作函數集。 d_sb:指向該dentry對應文件所在的文件系統的超級塊。使用d_inode->i_sb有相同的效果。 d_iname:它存儲了文件名的前15個字符,目的是為了方便引用。如果名字適合,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 *); }; 我們再簡單地介紹一下: d_revalidate:這個方法在entry在dcache中做路徑查找時調用,目的是為了檢驗這個entry是否依然合法。如果它依舊可以被信賴,則返回1,否則返回0。 d_hash:如果文件系統沒有提供名字驗證的規則,那么這個例程就被用來檢驗并且返回一個規范的哈希值。 d_compare:它被用來比較兩個qstr,來看它們是否是相同的。 d_delete:當引用計數到0時,在這個dentry被放到dentry_unused鏈表之前,會調用該函數。 d_release:在一個dentry被最終釋放之前,會調用該函數。 d_iput:如果定義了該函數,它就被用來替換iput,來dentry被丟棄時,釋放inode。它被用來做iput的工作再加上其他任何想要做的事情。
(五) 總結 在上面的部分中,我們對VFS進行了一個大概的分析。了解了文件系統的注冊,安裝,以及卸載的過程,并對VFS對邏輯文件系統的管理,尤其是接口部分的數據結構,進行了詳細的分析。 proc文件系統作為一個特殊的邏輯文件系統,其實現也遵循VFS接口,因此,根據上面對VFS的分析,我們可以基本確定對proc文件系統進行分析的步驟。
|