Linux虛擬文件系統vfs及proc詳解

/proc文件系統下的多種文件提供的系統信息不是針對某個特定進程的,而是能夠在整個系統範圍的上下文中使用。可以使用的文件隨系統配置的變化而變化。命令procinfo能夠顯示基於其中某些文件的多種系統信息。以下詳細描述/proc下的文件。 

/proc/cmdline文件
這個文件給出了內核啓動的命令行。它和用於進程的cmdline項非常相似。
示例: 
[root@localhost proc]# cat cmdline
ro root=LABEL=/ rhgb quiet

/proc/cpuinfo文件
這個文件提供了有關係統CPU的多種信息。這些信息是從內核裏對CPU的測試代碼中得到的。文件列出了CPU的普通型號(386,486,586,686 等),以及能得到的更多特定信息(製造商,型號和版本)。文件還包含了以bogomips表示的處理器速度,而且如果檢測到CPU的多種特性或者bug,文件還會包含相應的標誌。這個文件的格式爲:文件由多行構成,每行包括一個域名稱,一個冒號和一個值。
示例: 
[root@localhost proc]# cat cpuinfo
processor : 0
vendor_id : AuthenticAMD
cpu family : 6
model : 8
model name : AMD Athlon(tm) XP 1700+
stepping : 1
cpu MHz : 1530.165
cache size : 256 KB
fdiv_bug : no
hlt_bug : no
f00f_bug : no
coma_bug : no
fpu : yes
fpu_exception : yes
cpuid level : 1
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic mtrr pge mca cmov pat pse36 mmx fxsr sse syscall mmxext 3dnowext 3dnow
bogomips : 2998.27

/proc/devices文件
這個文件列出字符和塊設備的主設備號,以及分配到這些設備號的設備名稱。
示例:
[root@localhost /]# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
6 lp
7 vcs
10 misc
13 input
14 sound
29 fb
36 netlink
116 alsa
128 ptm
136 pts
180 usb
Block devices:
1 ramdisk
2 fd
3 ide0
9 md
22 ide1
253 device-mapper
254 mdp 

/proc/dma文件 
這個文件列出由驅動程序保留的DMA通道和保留它們的驅動程序名稱。casade項供用於把次DMA控制器從主控制器分出的DMA行所使用;這一行不能用於其它用途。
示例:
[root@localhost ~]# cat /proc/dma
4: cascade

/proc/filesystems文件
這個文件列出可供使用的文件系統類型,一種類型一行。雖然它們通常是編入內核的文件系統類型,但該文件還可以包含可加載的內核模塊加入的其它文件系統類型。
示例:
[root@localhost proc]# cat /proc/filesystems 
nodev sysfs
nodev rootfs
nodev bdev
nodev proc
nodev sockfs
nodev binfmt_misc
nodev usbfs
nodev usbdevfs
nodev futexfs
nodev tmpfs
nodev pipefs
nodev eventpollfs
nodev devpts
ext2
nodev ramfs
nodev hugetlbfs
iso9660
nodev mqueue
nodev selinuxfs
ext3
nodev rpc_pipefs
nodev autofs 

/proc/interrupts文件
這個文件的每一行都有一個保留的中斷。每行中的域有:中斷號,本行中斷的發生次數,可能帶有一個加號的域(SA_INTERRUPT標誌設置),以及登記 這個中斷的驅動程序的名字。可以在安裝新硬件前,像查看/proc/dma和/proc/ioports一樣用cat命令手工查看手頭的這個文件。這幾個文件列出了當前投入使用的資源(但是不包括那些沒有加載驅動程序的硬件所使用的資源)。
示例:
[root@localhost SPECS]# cat /proc/interrupts
CPU0
0: 7039406 XT-PIC timer
1: 6533 XT-PIC i8042
2: 0 XT-PIC cascade
3: 0 XT-PIC uhci_hcd
5: 108 XT-PIC VIA8233, uhci_hcd
8: 1 XT-PIC rtc
9: 0 XT-PIC acpi
10: 0 XT-PIC ehci_hcd
11: 17412 XT-PIC uhci_hcd, eth0
12: 140314 XT-PIC i8042
14: 37897 XT-PIC ide0
15: 60813 XT-PIC ide1
NMI: 0
ERR: 1 

/proc/ioports文件
這個文件列出了諸如磁盤驅動器,以太網卡和聲卡設備等多種設備驅動程序登記的許多I/O端口範圍。
示例:
[root@localhost SPECS]# cat /proc/ioports
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-006f : keyboard
0070-0077
: rtc
0080-008f : dma page reg
00a0-00a1 : pic2
00c0-00df : dma2
00f0-00ff : fpu
0170-0177 : ide1
01f0-01f7 : ide0
0376-0376 : ide1
0378-037a : parport0
037b-037f : parport0
03c0-03df : vga+
03f6-03f6 : ide0
03f8-03ff : serial
0800-0803 : PM1a_EVT_BLK
0804-0805 : PM1a_CNT_BLK
0808-080b : PM_TMR
0810-0815 : ACPI CPU throttle
0820-0823 : GPE0_BLK
0cf8-0cff : PCI conf1
dc00-dcff : 0000:00:12.0
dc00-dcff : via-rhine
e000-e0ff : 0000:00:11.5
e000-e0ff : VIA8233
e400-e41f : 0000:00:10.0
e400-e41f : uhci_hcd
e800-e81f : 0000:00:10.1
e800-e81f : uhci_hcd
ec00-ec1f : 0000:00:10.2
ec00-ec1f : uhci_hcd
fc00-fc0f : 0000:00:11.1
fc00-fc07 : ide0
fc08-fc0f : ide1 

/proc/kcore文件
這個文件是系統的物理內存以core文件格式保存的文件。例如,GDB能用它考察內核的數據結構。它不是純文本,而是/proc目錄下爲數不多的幾個二進制格式的項之一。

/proc/kmsg文件
這個文件用於檢索用printk生成的內核消息。任何時刻只能有一個具有超級用戶權限的進程可以讀取這個文件。也可以用系統調用syslog檢索這些消息,通常使用工具dmesg或守護進程klogd檢索這些消息。

/proc/ksyms文件
這個文件列出了已經登記的內核符號;這些符號給出了變量或函數的地址。每行給出一個符號的地址,符號名稱以及登記這個符號的模塊。程序ksyms,insmod和kmod使用這個文件。它還列出了正在運行的任務數,總任務數和最後分配的PID。

/proc/loadavg文件
這個文件給出以幾個不同的時間間隔計算的系統平均負載,這就如同uptime命令顯示的結果那樣。前三個數字是平均負載。這是通過計算過去1分鐘,5分鐘,15分鐘裏運行隊列中的平均任務數得到的。隨後是正在運行的任務數和總任務數。最後是上次使用的進程號。
示例:
[root@localhost ~]# cat /proc/loadavg
0.11 0.16 0.14 3/126 3912 

/proc/locks文件
這個文件包含在打開的文件上的加鎖信息。文件中的每一行描述了特定文件和文檔上的加鎖信息以及對文件施加的鎖的類型。內核也可以需要時對文件施加強制性鎖。
示例:
[root@localhost redhat]# cat /proc/locks
1: POSIX ADVISORY READ 3822 03:0a:1067117 0 EOF
2: POSIX ADVISORY READ 3822 03:0a:1067138 0 EOF
3: POSIX ADVISORY WRITE 3326 03:0a:2326540 0 EOF
4: POSIX ADVISORY WRITE 2639 03:0a:2966595 0 EOF
5: FLOCK ADVISORY WRITE 2591 03:0a:2966586 0 EOF
6: POSIX ADVISORY WRITE 2540 03:0a:2966578 0 EOF
7: POSIX ADVISORY WRITE 2530 03:0a:2966579 0 EOF
8: POSIX ADVISORY WRITE 2402 03:0a:2966563 0 EOF
9: POSIX ADVISORY WRITE 2371 03:0a:2966561 0 EOF 

一、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 是一個管理者,而對於內核的其他部分,則是一個接口。VFS提供了一個統一的接口(即幾個有關操作的數據結構),一個邏輯文件系統要想被 Linux 支持,那麼就必須按照這個接口來編寫自己的操作函數,從而將自己的細節對其他子系統隱藏起來。因而,對於內核其他子系統來說,所有的文件系統都是一樣的。

(二) 基本對象與方法 
虛擬文件系統的接口由一組對象及其由這些對象調用的一組方法所構成的。這些基本的對象是 files(文件),file-systems(文件系統),inodes (索引節點)以及 names for inodes(索引節點名字),下面對這些對象進行簡單的介紹: 

1 Files: 
文件是一個可讀可寫的對象,它也可以映射到內存中,這和 UNIX 中文件描述符的概念很接近。文件在 Linux 中使用一個"struct file"結構來實現,並且該結構有一組操作函數,保存在結構"struct file_operations"中。 

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被最終釋放之前,會調用該函數。 

_iput:如果定義了該函數,它就被用來替換iput,來dentry被丟棄時,釋放inode。它被用來做iput的工作再加上其他任何想要做的事情。 

(五) 總結 
在上面的部分中,我們對VFS進行了一個大概的分析。瞭解了文件系統的註冊,安裝,以及卸載的過程,並對VFS對邏輯文件系統的管理,尤其是接口部分的數據結構,進行了詳細的分析。 

proc文件系統作爲一個特殊的邏輯文件系統,其實現也遵循VFS接口,因此根據上面對VFS的分析,我們可以基本確定對proc文件系統進行分析的步驟。 

二、proc文件系統分析 
根據前面的分析,我們可以基本確定對proc文件系統的分析步驟。我將按照proc文件系統註冊,安裝的順序對其進行分析,然後基於代碼,對proc文件 系統的結構進行分析,尤其是proc文件系統用於內部管理的數據結構。最後我們將根據分析結果,提出可行的xml封裝計劃。 

在對proc文件系統的數據結構的分析中,我將把重點放在數據輸出的分析上,它是提出一種標準的XML封裝方法的基礎。 

(一) Linux 相關源代碼簡介 
在linux代碼樹中,所有文件系統的代碼都放在linux/fs/目錄中,其中,proc文件系統的源代碼在linux/fs/proc中,下面我簡單介紹一下proc目錄中的源文件。 
在目錄中共有11個相關文件,它們是: 
procfs_syms.c inode.c generic.c base.c 
array.c root.c proc_tty.c proc_misc.c 
kmsg.c kcore.c proc_devtree.c 

其中,procfs_syms.c,generic.c以及inode.c與proc文件系統的管理相關,包括proc文件系統的註冊,以及向內核其他子系統提供的例程等等,這是最重要的一部分代碼,我們將從這裏開始對proc文件系統進行分析。 

源文件root.c與proc文件系統的根結點的管理相關。而base.c,array.c則用來處理/proc目錄中進程的信息,包括命令行,進程狀態,內存狀態等等與進程相關的內容。proc_tty.c用來處理/proc/tty信息,proc_misc.c則用來管理與/proc目錄中的大多數文件。除此之外,還有兩個非常重要的頭文件proc_fs.h,proc_fs_i.h,我們可以在/linux/include/linux/目錄中找到。 

(二) proc文件系統的註冊 
proc文件系統遵循VFS的規範,因此在使用之前,必須進行註冊。我們知道,每一個文件系統,都會在自己的初始化例程中填寫一個 file_system_type 的數據結構,然後調用註冊函數register_filesystem(struct file_system_type *fs) 進行註冊。

proc文件系統中與之相關的文件是procfs_syms.c,在該文件中,聲明瞭proc文件系統的類型: 
static DECLARE_FSTYPE(proc_fs_type, "proc", proc_read_super, FS_SINGLE); 
而我們在 fs.h 中可以找到宏DECLARE_FSTYPE的定義: 
#define DECLARE_FSTYPE(var,type,read,flags) / 
struct file_system_type var = { / 
name: type, / 
read_super: read, / 
fs_flags: flags, / 
owner: THIS_MODULE, / 


因此我們可以看到,我們聲明瞭一個文件類型proc_fs_type,它的名字是“proc”,讀取超級塊的函數是 proc_read_super,fs_flags設置爲FS_SINGLE,根據源碼中的說明,我們知道,當文件系統的fs_flags聲明爲 FS_SINGLE時,說明文件系統只有一個超級塊,並且必須在註冊函數之後調用kern_mount(),使得在內核範圍內的vfsmnt被放置在 ->kern_mnt處。 

下面就是proc文件系統的註冊,函數init_proc_fs()的代碼如下所示: 
static int __init init_proc_fs(void) { 
int err = register_filesystem(&proc_fs_type); 
if (!err) { 
proc_mnt = kern_mount(&proc_fs_type); 
err = PTR_ERR(proc_mnt); 
if
(IS_ERR(proc_mnt)) 
unregister_filesystem(&proc_fs_type); 
else 
err = 0; 

return err; 


可以看到,proc文件系統的註冊非常簡單,主要有如下幾個步驟: 
1.調用register_filesystem(&proc_fs_type),用一個非常巧妙的方法將proc文件類型加入到文件類型的單向鏈表中,如果發生錯誤,則返回。

2.調用kern_mount函數,該函數基本完成三個步驟,首先調用read_super()函數,在這個函數裏,VFS將爲proc文件系統分配一個 超級塊結構,並設置s_dev,s_flags等域,然後將調用proc文件系統的自己的read_super例程,對應proc文件系統,該例程是 proc_read_super(),該例程將設置超級塊結構的其他值。我們將在下一節進行分析。

其次,使用add_vfsmnt()函數建立proc文件系統的vfsmount結構,並將其加入到已裝載文件系統的鏈表中。 
最後,返回該vfsmount結構,並利用返回值,使用指針proc_mnt指向該vfsmount結構。 

3.判斷返回值是否錯誤,如果錯誤,那麼就卸載文件系統。 
這樣,一個文件系統就成功註冊到核心了。同樣,proc文件系統的卸載,也非常簡單,代碼如下: 
static void __exit exit_proc_fs(void){ 
unregister_filesystem(&proc_fs_type); 
kern_umount(proc_mnt); 


(三) 建立proc文件系統的超級塊 
我們剛纔看到,在kern_mount函數中,調用read_proc建立了超級塊結構,然後就會調用文件系統自己提供的讀取超級塊的例程,用來填充自己 的超級塊結構,下面我們看一下proc文件系統的超級塊讀取例程proc_read_super()是如何工作的,以及它最終完成了哪些工作,該函數在 fs/proc/inode.c中實現: 
struct super_block *proc_read_super(struct super_block *s,void *data, 
int silent) { 
struct inode * root_inode; 
struct task_struct *p; 
s->s_blocksize = 1024; 
s->s_blocksize_bits = 10; 
s->s_magic = PROC_SUPER_MAGIC; 
s->s_op = &proc_sops; 
s->s_maxbytes = MAX_NON_LFS; 
root_inode = proc_get_inode(s, PROC_ROOT_INO, &proc_root); 
if (!root_inode) 
goto out_no_root; 
/* 
* Fixup the root inode's nlink value 
*/ 
read_lock(&tasklist_lock); 
for_each_task(p) if (p->pid) root_inode->i_nlink++; 
read_unlock(&tasklist_lock); 
s->s_root = d_alloc_root(root_inode); 
if (!s->s_root) 
goto out_no_root; 
parse_options(data, &root_inode->i_uid, &root_inode->i_gid); 
return s; 
out_no_root: 
printk("proc_read_super: get root inode failed/n"); 
iput(root_inode); 
return NULL; 


該函數進行了如下幾步操作: 
在該函數裏,首先向作爲參數傳入的超級塊寫入文件系統的基本信息,s_blocksize設置爲1024,由於1024=2^10,因此,s_blocksize_bit設置爲10,然後是proc文件系統的魔數,爲PROC_SUPER_MAGIC。超級塊的函數集設置爲 proc_sops,對於proc文件系統來講,只實現了4個超級塊函數,我們將在後面進行分析。然後,設置proc文件系統中的文件最大字節數爲 MAX_NON_LFS,在fs.h中,定義這個宏爲 ((1ULs_root = d_alloc_root(root_inode) 

其中root_inode 的類型是struct inode *, 而s_root的類型是struct dentry *。我們在介紹VFS的時候知道,目錄高速緩存以樹狀結構存在,因此在建立文件系統的根結點後,需要使用d_alloc_root()函數建立一個根目 錄(root dentry),也就是說,該dentry結構的。 

最終成功返回超級塊,這時超級塊已經填上了必要的數據信息。因此可以看到,超級塊讀取例程主要完成了兩部分的工作,首先向超級塊寫入必要的數據,其次建立了該文件系統的根結點,並在目錄高速緩存中建立了相應的dentry結構。

(四) proc文件系統超級塊的操作函數集 
在上一節我們看到了proc文件系統如何設置自己的超級塊,並且將超級塊操作函數集設置爲proc_sops,這一節我們就分析一下,對於proc文件系統的超級塊,需要提供什麼操作,以及如何實現這些操作。 

在文件fs/proc/inode.c中,有如下定義: 
static struct super_operations proc_sops = { 
read_inode: proc_read_inode, 
put_inode: force_delete, 
delete_inode: proc_delete_inode, 
statfs: proc_statfs, 
}; 

我們可以看到,proc文件系統僅僅實現了4個超級塊操作函數。它使用了一種比較特殊的方法來初始化結構,這種方法叫作labeled elements,這是GNU的C擴展,這樣在初始化結構時,不必按照結構的順序,只要指明域名,就可初始化其值,而對於沒有提到的域,將自動設置爲0。 

所以我們看到,proc文件系統僅僅定義了4個超級塊操作函數,我們看一下爲什麼其他的操作函數不必定義。 
首先,我們知道,proc文件系統僅僅存在於內存中,並不需要物理設備,因此write_inode函數就不需要定義了。而函數 notify_change,在索引節點的屬性被改變的時候會被調用,而對於proc文件系統的inode來說,並未提供setattr 函數,換句話說,文件的屬性不會被改變,所以,notif_change也就不會被調用(proc文件系統對於inode_operations,同樣僅 僅提供了很少的幾種操作,並且,在建立文件樹的時候,還針對不同的文件/目錄,設置了不同的索引節點操作函數,這將在以後進行詳細的介紹)。基於類似的原 因,其他的函數,諸如put_super,write_super,以及clear_inode等等函數,都沒有進行定義。 

下面我們看一下定義的這4個函數: 
1 read_inode: proc_read_inode 
這個函數用來從已裝載文件系統中,讀取指定索引節點的信息。實際上,在需要讀取特定的索引節點時,會調用VFS的iget(sb, ino)函數,其中,sb指定了文件系統的超級塊,而ino是索引節點的標號。這個函數會在該超級塊的dcache中尋找該索引節點,如果找到,則返回索引節點,否則就必須從邏輯文件系統中讀取指定的索引節點,這時會調用get_new_inode()函數,在這個函數裏,會分配一個inode結構,填寫一些基本的信息,然後就會調用超級塊的操作函數read_inode,對於proc文件系統而言,就是proc_read_inode()函數。 

在後面的介紹裏我們會知道,proc文件系統爲了方便自己對文件的管理,對於每一個已經註冊的proc文件,都建立並維護了一個的 proc_dir_entry結構。這個結構非常的重要,對於proc文件系統來說,這個結構是自己的私有數據,相當於其他邏輯文件系統(比如ext2文 件系統)在物理硬盤上的索引節點。因此,只有在必要的時候,纔會把proc文件系統的proc_dir_entry結構鏈接到VFS的索引節點中。 

因此,proc_read_inode函數的主要目的,是建立一個新的索引節點,只需填充一些基本的信息即可。所以我們可以看到proc_read_inode函數非常的簡單: 
static void proc_read_inode(struct inode * inode){ 
inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME; 


要說明的是,在調用proc_read_inode函數之前,VFS的get_new_inode()函數已經爲inode設置了其他的基本信息,比如i_sb,i_dev,i_ino,i_flags 以及i_count等等。 

2 put_inode: force_delete 
put_inode函數是在索引節點的引用計數減少的時候調用,我們看到,proc文件系統沒有實現自己的put_inode函數,而是簡單地設置了VFS的force_delete 函數,我們看一下這個函數的內容: 
void force_delete(struct inode *inode){ 
/* 
* Kill off unused inodes ... iput() will unhash and 
* delete the inode if we set i_nlink to zero. 
*/ 
if (atomic_read(&inode->i_count) == 1) 
inode->i_nlink = 0; 


我們知道,put_inode函數是在引用計數i_count減少之前調用的,因此,對於proc文件系統來說,在每一次inode引用計數減少之前,都要檢查引用計數會不會減少至零,如果是,那麼就將改索引節點的鏈接數直接設置爲零。

3 delete_inode: proc_delete_inode 
當一個索引節點的引用計數和鏈接數都到零的時候,會調用超級塊的delete_inode函數。由於我們使用force_delete實現了proc超級 塊的put_inode方法,因此我們知道,對於proc文件系統來說,當一個inode的引用計數爲零的時候,它的鏈接數也必爲零。我們看一下該函數的源碼: 
/* 
* Decrement the use count of the proc_dir_entry. 
*/ 
static void proc_delete_inode(struct inode *inode) { 
struct proc_dir_entry *de = inode->u.generic_ip;/* for the procfs, inode->u.generic_ip is a 'proc_dir_entry' */ 
inode->i_state = I_CLEAR; 
if (PROC_INODE_PROPER(inode)) { 
proc_pid_delete_inode(inode); 
return; 

if (de) { 
if (de->owner) 
__MOD_DEC_USE_COUNT(de->owner); 
de_put(de); 



我們看到,這個函數基本上做了三個工作,首先,將這個索引節點的狀態位設置爲I_CLEAR,這標誌着,這個inode結構已經不再使用了。其次根據這個索引節點的ino號,檢查它是否是pid目錄中的索引節點,因爲pid目錄的索引節點號使用。
#define fake_ino(pid,ino) (((pid)f_type = PROC_SUPER_MAGIC; /* here use the super_block's s_magic ! */ 
buf->f_bsize = PAGE_SIZE/sizeof(long); /* optimal transfer block size */ 
buf->f_bfree = 0; /* free blocks in fs */ 
buf->f_bavail = 0; /* free blocks avail to non-superuser */ 
buf->f_ffree = 0; /* free file nodes in fs */ 
buf->f_namelen = NAME_MAX; /* maximum length of filenames */ 
return 0; 


我們看到,它將文件系統的統計數據填充到一個buf中,文件系統類型爲PROC_SUPER_MAGIC,在文件系統中的空閒塊以及文件系統中的文件節點都設置爲0,因此對於只存在於內存中的proc文件系統來說,這些統計數據是沒有意義的。 

(五) 對proc文件的管理 
前面我們提過,相對於其他邏輯文件系統的具體文件組織形式(比如ext2文件系統的inode),proc文件系統也有自己的組織結構,那就是 proc_dir_entry結構,所有屬於proc文件系統的文件,都對應一個proc_dir_entry結構,並且在VFS需要讀取proc文件的 時候,把這個結構和VFS的inode建立鏈接(即由inode->u.generic_ip指向該prc_dir_entry結構)。 

因此,proc文件系統實現了一套對proc_dir_entry結構的管理,下面我們就此進行一個分析。 

1 proc_dir_entry結構 
首先我們看一下proc_dir_entry結構,這個結構在proc_fs.h中定義: 
struct proc_dir_entry { 
unsigned short low_ino; 
unsigned short namelen; 
const char *name; 
mode_t mode; 
nlink_t nlink; 
uid_t uid; 
gid_t gid; 
unsigned long size; 
struct inode_operations * proc_iops; 
struct file_operations * proc_fops; 
get_info_t *get_info; 
struct module *owner; 
struct proc_dir_entry *next, *parent, *subdir; 
void *data; 
read_proc_t *read_proc; 
write_proc_t *write_proc; 
atomic_t count; /* use count */ 
int deleted; /* delete flag */ 
kdev_t rdev; 
}; 

在這個結構中,描述了一個proc文件的全部信息,每一個proc文件正是使用proc_dir_entry結構來表示的。下面我們看一下它最重要的幾個域: 

low_ino:這是用來唯一標誌proc_dir_entry結構的節點號,也就是proc文件系統內的索引節點的標號,除了根結點,其他的節點號都是在創建proc_dir_entry的時候,由make_inode_number()動態創建的。 

name:即這個proc文件的名字。 

mode:該proc文件的模式由兩部分用位或運算組成,第一部分是文件的類型,可以參考include/linux/stat.h中的定義,比 如,S_IFREG表示普通文件,而S_IFDIR表示目錄文件。第二部分是該文件的權限,同樣可以參考include/linux/stat.h中的定 義,比如,S_IRUSR表示該文件能夠被擁有者讀,S_IROTH 表示該文件可以被其他人讀取。但真正的權限檢查,我們可以放到後面提到的inode_operations結構中。 

size:即我們使用“ls”命令時,所顯示出的文件大小。 

proc_iops:這是一個inode_operations結構,其中設置了針對這個proc索引節點的操作函數,這樣,我們就可以針對不同類型的 proc文件,提供不同的方法,以完成不同的工作。比如我們上面提到的對proc文件的權限檢查,就可以放在這個結構中。 

proc_fops:這是一個file_operations結構,其中放置了針對這個proc文件的操作函數,我們可以把對proc文件的讀寫操作,放在這個結構中,用以實現對/proc目錄中的文件的讀,寫功能。 

get_info:當用戶向proc文件讀取的數據小於一個頁面大小時,可以使用這個函數向用戶返回數據。 
struct proc_dir_entry *next, *parent, *subdir:使用這些鏈表,在內存中,proc_dir_entry結構就以樹的形式鏈接在一起。read_proc_t *read_proc 和write_proc_t *write_proc:這兩個函數提供了對proc文件進行操作的簡單接口。我們知道,對於proc文件,我們可以從中讀取核心數據,還可以向其中寫入 數據,因此,對於一些功能比較簡單的proc文件,我們只要實現這兩個函數(或其中之一)即可,而不用設置inode_operations結構,這樣整個操作比較簡單。實際上,我們會在後面的分析中看到,在註冊proc文件的時候,會自動爲proc_fops設置一個缺省的 file_operations結構,如果我們只實現了上面提到的兩個讀寫操作,而沒有設置自己file_operations結構,那麼會由缺省的 inode_operations結構中的讀寫函數檢查調用這兩個函數。 

atomic_t count:該結構的使用計數。當一個proc_dir_entry結構的count減爲零時,會釋放該結構,這種結果就像把一個ext2文件系統的文件從磁盤上刪除掉一樣。

int deleted:這是一個刪除標誌,當我們調用remove_proc_entry函數要刪除一個proc_dir_entry時,如果發現該結構還在使用,就會設置該標誌並且推出。

2 建立proc文件 
在瞭解了proc_dir_entry結構之後,我們來看一看proc文件系統是如何管理自己的文件結構的。首先我們看一看它是如何創建proc文件的,參考文件fs/proc/generic.c,其中,有一個函數create_proc_entry,由它創建並註冊proc文件,下面我們看一下它的源碼: 
struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent) { 
struct proc_dir_entry *ent = NULL; 
const char *fn = name; 
int len; 
if (!parent && xlate_proc_name(name, &parent, &fn) != 0) 
goto out; 
len = strlen(fn); 
ent = kmalloc(sizeof(struct proc_dir_entry) + len + 1, GFP_KERNEL); 
if (!ent) 
goto out; 
memset(ent, 0, sizeof(struct proc_dir_entry)); 
memcpy(((char *) ent) + sizeof(*ent), fn, len + 1); 
ent->name = ((char *) ent) + sizeof(*ent); 
ent->namelen = len; 
if (S_ISDIR(mode)) { 
if ((mode & S_IALLUGO) == 0) 
mode |= S_IRUGO | S_IXUGO; 
ent->proc_fops = &proc_dir_operations; 
ent->proc_iops = &proc_dir_inode_operations; 
ent->nlink = 2; 
} else { 
if ((mode & S_IFMT) == 0) 
mode |= S_IFREG; 
if ((mode & S_IALLUGO) == 0) 
mode |= S_IRUGO; 
ent->nlink = 1; 

ent->mode = mode; 
proc_register(parent, ent); /* link ent to parent */ 
out: 
return ent; 


我們看到,首先該函數會做一些必要的檢查,比如要確保它的父節點必須存在等等。其次會創建一個proc_dir_entry結構,並且爲該文件的名字也分配空間,並用->name指向它。再次,會根據該文件的類型,設置適當的模式和鏈接數。最後,會調用proc_register(parent, ent)函數,將這個結構鏈接到proc文件樹中。下面我們看一下它的實現代碼: 
static int proc_register(struct proc_dir_entry * dir, struct proc_dir_entry * dp) { 
int i; 
i = make_inode_number(); 
if (i low_ino = i; 
dp->next = dir->subdir; 
dp->parent = dir; 
dir->subdir = dp; 
if (S_ISDIR(dp->mode)) { 
if (dp->proc_iops == NULL) { 
dp->proc_fops = &proc_dir_operations; 
dp->proc_iops = &proc_dir_inode_operations; 

dir->nlink++; 
} else if (S_ISLNK(dp->mode)) { 
if (dp->proc_iops == NULL) 
dp->proc_iops = &proc_link_inode_operations; 
} else if (S_ISREG(dp->mode)) { 
if (dp->proc_fops == NULL) 
dp->proc_fops = &proc_file_operations; 

return 0; 


這個函數主要完成三部分的工作,
第一,使用make_inode_number()函數動態的到一個節點號,並且設置low_ino。
第二步,將這個 proc_dir_entry結構鏈接到它的父節點上。
第三步,根據文件類型的不同,設置不同的(索引節點和文件)缺省操作函數集。這樣,一個proc文件就註冊成功了。

3 刪除proc文件 
在同一源文件中,提供了刪除proc_dir_entry結構的函數,即remove_proc_entry,下面我們分析一下它的實現過程。
void remove_proc_entry(const char *name, struct proc_dir_entry *parent) { 
struct proc_dir_entry **p; 
struct proc_dir_entry *de; 
const char *fn = name; 
int len; 
if (!parent && xlate_proc_name(name, &parent, &fn) != 0) 
goto out; 
len = strlen(fn); 
for (p = &parent->subdir; *p; p=&(*p)->next ) { 
if (!proc_match(len, fn, *p)) 
continue; 
de = *p; 
*p = de->next; 
de->next = NULL; 
if (S_ISDIR(de->mode)) 
parent->nlink--; 
clear_bit(de->low_ino-PROC_DYNAMIC_FIRST, 
(void *) proc_alloc_map); 
proc_kill_inodes(de); 
de->nlink = 0; 
if (!atomic_read(&de->count)) 
free_proc_entry(de); 
else { 
de->deleted = 1; 
printk("remove_proc_entry: %s/%s busy, count=%d/n", 
parent->name, de->name, atomic_read(&de->count)); 

break; 

out: 
return; 


該函數在參數parent的所有孩子中查找指定的名字,如果找到匹配的節點,即proc_match(len, fn, *p),那麼,就將該結構從樹結構中去掉。然後,如果刪除的proc_dir_entry是目錄結構,那麼就減少其父節點的鏈接數。 

然後調用clear_bit(de->low_ino-PROC_DYNAMIC_FIRST, (void *) proc_alloc_map)函數,清除該節點號。最後,將該結構的鏈接數置零,並調用atomic_read(&de->count)來檢查它的引用計數,如果是零,那麼就使用函數free_proc_entry釋放該節點,否則就將它的刪除標記位置一,在以後適當地機會中,再將其釋放。 

4 其他管理函數
除此之外,我們看到還有一些函數,可以方便我們管理和使用proc文件系統,我們簡單地介紹一下: 
struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent)函數,這個函數用來在proc文件系統中註冊一個子目錄,根據它的參數,我們就可以看出它的功能。在這個函數裏,將動態分配一個 proc_dir_entry結構以及它的名字,然後設置目錄文件的缺省操作(proc_iops以及proc_fops)以及nlink值,最後,調 用proc_register函數將其註冊。

struct proc_dir_entry *proc_mknod(const char *name, mode_t mode, struct proc_dir_entry *parent, kdev_t rdev)函數,用來在proc文件系統中建立一個設備文件,因此,在創建proc_dir_entry結構後,沒有設置缺省操作,而是使用 ->rdev = rdev指定了設備。最後,調用proc_register函數將其註冊。

struct proc_dir_entry *proc_symlink(const char *name, struct proc_dir_entry *parent, const char *dest)函數,該函數創建了一個鏈接文件,使用->mode = S_IFLNK|S_IRUGO|S_IWUGO|S_IXUGO來標誌,它和其他文件的建立很相似,只是它將鏈接的目標文件名放在了 ->data域中。最後它同樣調用proc_register函數將該結構註冊。 

(六) 對proc文件默認操作的分析 
現在,我們已經基本清楚了proc文件系統對自己proc_dir_entry結構的管理了。下面我們回過頭來,再看一下在文件註冊函數中的一段代碼: 
if (S_ISDIR(dp->mode)) { 
if (dp->proc_iops == NULL) { 
dp->proc_fops = &proc_dir_operations; 
dp->proc_iops = &proc_dir_inode_operations; 

dir->nlink++; 
} else if (S_ISLNK(dp->mode)) { 
if (dp->proc_iops == NULL) 
dp->proc_iops = &proc_link_inode_operations; 
} else if (S_ISREG(dp->mode)) { 
if (dp->proc_fops == NULL) 
dp->proc_fops = &proc_file_operations; 


我在前面已經提過,這段代碼根據註冊的proc文件類型的不同,爲proc_dir_entry結構設置了不同的操作函數集。也就是說,我們使用封裝的 create_proc_entry函數在proc文件系統中註冊文件時,可以不用去管這些操作函數集,因爲該結構總是自動地設置了相應的 proc_iops和proc_fops操作函數。下面我們就對這些默認的操作進行一個分析,因爲這對我們瞭解proc文件系統和VFS的結構非常重要。 

1 對普通文件的操作 
我們首先看一下普通proc文件的函數集,根據代碼段: 
if (S_ISREG(dp->mode)) { 
if (dp->proc_fops == NULL) 
dp->proc_fops = &proc_file_operations; 


我們可以看到,對於普通的proc文件,只設置了文件操作,即proc_file_operations,從這一點上可以看出,對於普通的proc文件,只缺省提供了文件操作,因此在必要的時候,我們必須手工設置需要的索引節點操作函數集,比如inode_operations中的權限檢查函數 permission等等。 對於proc_file_operations,我們可以看到,只實現了三個函數: 
static struct file_operations proc_file_operations = { 
llseek: proc_file_lseek, 
read: proc_file_read, 
write: proc_file_write, 
}; 

下面我們簡單的看一下它們實現的功能: 
(1)llseek: proc_file_lseek 
這個函數,用來實現lseek系統調用,其功能是設置file結構的->f_pos域,因此根據第三個參數orig的不同,將f_pos設置爲相應的值,該函數非常簡單,因此不作過多的介紹。 

(2)read: proc_file_read 
這個函數是file_operations結構中的成員,在後面我們將看到,在proc_dir_entry結構中實現的file_operations 和inode_operations將鏈接至VFS的inode中,因此該函數將用來實現read系統調用。在這個函數中,首先根據file結構,得到相應的inode,然後由 
struct proc_dir_entry
* dp; 
dp = (struct proc_dir_entry *) inode->u.generic_ip; 

而得到proc_dir_entry結構,然後,開始調用該proc_dir_entry結構中的函數,向用戶空間返回指定大小的數據,我們看一下下面的代碼片斷: 
if (dp->get_info) { 
/* 
* Handle backwards compatibility with the old net 
* routines. 
*/ 
n = dp->get_info(page, &start, *ppos, count); 
if (n read_proc) { 
n = dp->read_proc(page, &start, *ppos, 
count, &eof, dp->data); 
} else 
break; 

由此我們看出,該函數的實現依賴於proc_dir_entry結構中的get_info和read_proc函數,因此如果我們要註冊自己的proc 文件,在不設置自己的proc_fops操作函數集的時候,必須實現上面兩個函數中的一個,否則,這個缺省的proc_file_read函數將做不了任何工作:

在這個函數中,實現了從內核空間向用戶空間傳遞數據的功能,其中使用了許多技巧,在這裏就不作討論了,具體實現可以參考源碼。 

(3)write: proc_file_write 
與上面的函數類似,我們可以看到proc_file_write函數同樣依賴於proc_dir_entry中的write_proc(file, buffer, count, dp->data)函數,它的實現非常簡單: 
static ssize_t 
proc_file_write(struct file * file, const char * buffer, 
size_t count, loff_t *ppos) { 
struct inode *inode = file->f_dentry->d_inode; 
struct proc_dir_entry * dp; 
dp = (struct proc_dir_entry *) inode->u.generic_ip; 
if (!dp->write_proc) 
return -EIO; 
/* FIXME: does this routine need ppos? probably... */ 
return dp->write_proc(file, buffer, count, dp->data); 


我們看到,它只是簡單地檢測了->write_proc函數是否存在,如果我們在proc_dir_entry結構中實現了這個函數,那麼就調用它,否則就退出。 

根據上面的討論,我們看到,對於普通文件的操作函數,proc文件系統爲我們提供了一個簡單的封裝,因此,我們只要在proc_dir_entry中實現相關的讀寫操作即可。 

但是,如果我們想提供讀寫操作之外的函數,那麼我們就可以定義自己的file_operations函數集,並且在proc文件註冊後,將它鏈接到proc_dir_entry的proc_fops上,這樣,就可以使用自己的函數集了。 

2 對鏈接文件的操作 
根據代碼段: 
else if (S_ISLNK(dp->mode)) { 
if (dp->proc_iops == NULL) 
dp->proc_iops = &proc_link_inode_operations; 

我們可以看出,對於鏈接文件,proc文件系統爲它設置了索引節點操作proc_iops。因爲我們知道,一個符號鏈接,只擁有inode結構,而沒有文件結構,所以,爲它提供proc_link_inode_operations函數集就可以了。下面我們看一下,這個函數集的內容: 
static struct inode_operations proc_link_inode_operations = { 
readlink: proc_readlink, 
follow_link: proc_follow_link, 
}; 

這個函數集實現了和鏈接相關的兩個函數,我們分別來看一下: 
(1)readlink: proc_readlink 
該函數用來實現readlink系統調用,它的功能是獲得目標文件的文件名,我們在前面看到,對於一個鏈接文件,在註冊時已經將鏈接目標的文件放在了 proc_dir_entry結構的->data域中(參考前面介紹的函數proc_symlink),因此,我們只要將->data中的數 據返回就可以了,它的代碼如下: 
static int proc_readlink(struct dentry *dentry, char *buffer, int buflen) { 
char *s= 
((struct proc_dir_entry *)dentry->d_inode->u.generic_ip)->data; 
return vfs_readlink(dentry, buffer, buflen, s); 


我們看到,這個函數使用一個指針指向->data,然後,使用VFS函數vfs_readlink將數據返回到用戶空間,非常的簡單。 

(2)follow_link: proc_follow_link 
這個函數代碼如下: 
static int proc_follow_link(struct dentry *dentry, struct nameidata *nd) { 
char *s= 
((struct proc_dir_entry *)dentry->d_inode->u.generic_ip)->data; 
return vfs_follow_link(nd, s); 

和上面介紹的函數類似,它同樣利用VFS的函數實現其功能,對於vfs_follow_link,可以參考fs/namei.c文件。

3 對目錄文件的操作
最後我們看一下proc文件系統對目錄文件的操作函數集,在文件註冊的時候,有如下代碼: 
if (S_ISDIR(dp->mode)) { 
if (dp->proc_iops == NULL) { 
dp->proc_fops = &proc_dir_operations; 
dp->proc_iops = &proc_dir_inode_operations; 

dir->nlink++; 


從中我們可以看到,在proc文件系統中註冊目錄文件的時候,它會檢查是否該proc_dir_entry結構已經註冊了proc_iops函數集,如果沒有,那麼就爲proc_fops和proc_iops設置相應的缺省函數集。下面我們對它們分別進行討論:
1.對目錄的文件操作proc_dir_operations: 
static struct file_operations proc_dir_operations = { 
read: generic_read_dir, 
readdir: proc_readdir, 
}; 

這個函數集的主要功能,是在由proc_dir_entry結構構成的proc文件樹中解析目錄。下面我們對這兩個函數進行一個簡單的分析:

(1)read: generic_read_dir 
我們知道,對於read系統調用,當其參數文件句柄指向目錄的時候,將返回EISDIR錯誤。因此,目錄文件的read函數將完成這個工作。generic_read_dir函數是VFS提供的通用函數,可以參考fs/read_write.c文件: 
ssize_t generic_read_dir(struct file *filp, char *buf, size_t siz, loff_t *ppos){ 
return –EISDIR; 


這個函數很簡單,只要返回錯誤碼就可以了。 

(2)readdir: proc_readdir 
這個函數用來實現readdir系統調用,它從目錄文件中讀出dirent結構到內存中。我們可以參考fs/readdir.c中的filldir()函數。 
2.對目錄文件索引節點的操作函數:proc_dir_inode_operations 
首先,我們看一下proc_dir_inode_operations的定義: 
/* 
* proc directories can do almost nothing.. 
*/ 
static struct inode_operations proc_dir_inode_operations = { 
lookup: proc_lookup, 
}; 

我們看到,對於目錄文件的索引節點,只定義了一個函數lookup。因爲我們在前面對VFS進行分析的時候知道,以下操作,是隻在目錄節點中定義的:
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 *); 

但是經過我們對proc文件系統的分析,我們知道,proc文件系統中的文件都是在內核代碼中通過proc_dir_entry實現的,因此它不提供目錄索引節點的create,link,unlink,symlink,mkdir,rmdir,mknod,rename方法,也就是說,用戶是不能通過 shell命令在/proc目錄中對proc文件進行改名、刪除、建子目錄等操作的。這也算是proc文件系統的一種保護策略。 

而在內核中,則使用proc_mkdir,proc_mknod等函數,在覈心內通過代碼來維護proc文件樹。由此可以看出虛擬文件系統的一些特性。對目錄文件的默認操作,可以參見下面的講解: 

下面我們就來看一下唯一定義的函數lookup: proc_lookup,到底實現了什麼功能。 

在進行具體分析之前,我們先考慮一個問題,我們知道,proc文件系統維護了自己的proc_dir_entry結構,因此提供了 create_proc_entry,remove_proc_entry等等函數,並且爲了方便實現對proc文件的讀寫功能,特意在 proc_dir_entry結構中設置了get_info,read_proc和write_proc函數指針(我們在前面介紹過,這三個函數被封裝在 proc_file_operations中),並且提供了自己的inode_operations和file_operations,分別是 proc_iops 和proc_fops。也就是說,我們在建立proc文件以及爲proc文件建立操作函數的時候,似乎可以不用考慮VFS的實現,只要建立並註冊該 proc_dir_entry結構,然後實現其proc_iops 和proc_fops(或者get_info,read_proc和write_proc)就可以了。 

但是我們知道,在linux系統中,所有的子系統都是與VFS層交互,而VFS是通過inode結構進行管理的,並且在其上的操作(文件和索引節點的操 作)也是通過該inode結構的inode_operations和file_operations實現的。因此,proc文件系統必須將自己的文件與 VFS的inode鏈接起來。 

那麼proc文件系統是在何時,通過何種方法將自己的proc_dir_entry結構和VFS的inode聯繫在一起的,並且將對inode的 inode_operations和file_operations操作定位到自己結構中的proc_iops 和proc_fops上呢?通過我們對lookup: proc_lookup的分析,就會明白這一過程。我們先看一下它的代碼: 
struct dentry *proc_lookup(struct inode * dir, struct dentry *dentry){ 
struct inode *inode; 
struct proc_dir_entry * de; 
int error; 
error = -ENOENT; 
inode = NULL; 
de = (struct proc_dir_entry *) dir->u.generic_ip; 
if (de) { 
for (de = de->subdir; de ; de = de->next) { 
if (!de || !de->low_ino) 
continue; 
if (de->namelen != dentry->d_name.len) 
continue; 
if (!memcmp(dentry->d_name.name, 
de->name, de->namelen)) { 
int ino = de->low_ino; 
error = -EINVAL; 
inode = proc_get_inode(dir->i_sb, ino, de); 
break; 



if (inode){ 
dentry->d_op = &proc_dentry_operations; 
d_add(dentry, inode); 
return NULL; 

return ERR_PTR(error); 


這個函數的參數是struct inode * dir和struct dentry *dentry,它的功能是查找由dentry指定的文件,是否在由dir指定的目錄中。 

我們知道,proc文件系統通過proc_dir_entry結構維護文件信息,並且該結構與相應的inode->u.generic_ip聯繫,因此這個函數首先通過struct inode * dir得到了相應目錄文件的proc_dir_entry結構,並使用指針de指向它,然後,開始在該結構的孩子中查找指定的dentry。 

判斷是否找到的條件很簡單,就是de->namelen等於 dentry->d_name.len,並且dentry->d_name.name等於de->name,根據程序流程,如果沒有找到,那麼將返回-ENOENT錯誤(使用inode指針作爲判斷條件),如果找到該文件,那麼就根據ino = de->low_ino(要注意的是,這時候的de已經指向由dentry確定的proc_dir_entry結構了。)調用函數: 
inode = proc_get_inode(dir->i_sb,
ino, de); 

這個proc_get_inode的功能很容易猜到,就是從由超級塊i_sb確定的文件系統中,得到索引節點號爲ino的inode。因此考慮兩種情況,第一種情況,這個索引節點已經被讀入緩存了,那麼直接返回該inode即可。第二種情況是,指定ino的索引節點不在緩存中,那麼就需要調用相應的函數,將該索引節點從邏輯文件系統中讀入inode中。 

下面我們就來分析一下proc_get_inode函數,尤其注意上面所說的第二種情況,因爲這正是inode和proc_dir_entry建立聯繫並重定位操作函數集的時機。先看一下源碼: 
struct inode * proc_get_inode(struct super_block * sb, int ino, 
struct proc_dir_entry * de) { 
struct inode * inode; 
/* 
* Increment the use count so the dir entry can't disappear. 
*/ 
de_get(de); 
#if 1 
/* shouldn't ever happen */ 
if (de && de->deleted) 
printk("proc_iget: using deleted entry %s, count=%d/n", de->name, atomic_read(&de->count)); 
#endif 
inode = iget(sb, ino); 
if (!inode) 
goto out_fail; 
inode->u.generic_ip = (void *) de; /* link the proc_dir_entry to inode */ 
/* 
* set up other fields in the inode 
*/ 
if (de) { 
if (de->mode) { 
inode->i_mode = de->mode; 
inode->i_uid = de->uid; 
inode->i_gid = de->gid; 

if (de->size) 
inode->i_size = de->size; 
if (de->nlink) 
inode->i_nlink = de->nlink; 
if (de->owner) 
__MOD_INC_USE_COUNT(de->owner); 
if (S_ISBLK(de->mode)||S_ISCHR(de->mode)||S_ISFIFO(de->mode)) 
init_special_inode(inode,de->mode,kdev_t_to_nr(de->rdev)); 
else { 
if (de->proc_iops) 
inode->i_op = de->proc_iops; 
if (de->proc_fops) 
inode->i_fop = de->proc_fops; 


out: 
return inode; 
out_fail: 
de_put(de); 
goto out; 


我們根據程序流程,分析它的功能: 
1.使用de_get(de)增加proc_dir_entry結構de的引用計數。 

2.使用VFS的iget(sb, ino)函數,從sb指定的文件系統中得到節點號爲ino的索引節點,並使用指針inode指向它。如果沒有得到,則直接跳到標號out_fail,減少de的引用計數後退出。 

因此我們要了解一下iget,這個函數由VFS提供,可以參考源文件fs/inode.c和頭文件include/linux/fs.h,在fs.h頭文件中,有如下定義: 
static inline struct inode *iget(struct super_block *sb, unsigned long ino) { 
return iget4(sb, ino, NULL, NULL); 


因此該函數是由fs/inode.c中的iget4實現的。主要步驟是,首先根據sb和ino得到要查找的索引節點的哈希鏈表,然後調用 find_inode函數在該鏈表中查找該索引節點。如果找到了,那麼就增加該索引節點的引用計數,並將其返回;否則調用get_new_inode函 數,以便從邏輯文件系統中讀出該索引節點。 

get_new_inode函數也很簡單,它分配一個inode結構,並試圖重新查找指定的索引節點,如果還是沒有找到,那麼就給新分配的索引節點加入 到哈希鏈表和使用鏈表中,並設置一些基本信息,如i_ino,i_sb,i_dev等,並且,將其引用計數i_count初始化爲1。然後調用超級塊 sb的read_inode函數,來作邏輯文件系統自己特定的工作,但對於proc文件系統來說,read_inode函數基本沒有實質性的功能,可參考 前文對該函數的分析。最後返回這個新建的索引節點。 

3.這時,我們已經得到了指定的inode(或者是從緩存中返回,或者是利用get_new_inode函數剛剛創建),那麼就使用語句 
inode->u.generic_ip = (void *) de; 
將proc_dir_entry結構de與相應的索引節點鏈接起來。因此我們就可以在其他時刻,利用proc文件索引節點的->u.generic_ip得到相應的proc_dir_entry結構了。 

對於新創建的inode來說,將其->u.generic_ip域指向(void *) de沒什麼問題,因爲該域還沒有被賦值,但是如果這個inode是從緩存中得到的,那麼,說明該域已經指向了一個proc_dir_entry結構,這樣 直接賦值,會不會引起問題呢? 

這有兩種情況,第一種情況,它指向的proc_dir_entry結構沒有發生過變化,那麼,由於索引節點是由ino確定的,而且在一個文件系統中,確保 了索引節點號ino的唯一性,因此,使用inode->u.generic_ip = (void *) de語句對其重新進行賦值,不會發生任何問題。 

另一種情況是在這之前,程序曾調用remove_proc_entry要將該proc_dir_entry結構刪除,那麼由於它的引用計數count不等於零,因此,該結構不會被釋放,而只是打上了刪除標記。所以這種情況下,該賦值語句也不會引起問題。 

我們知道,當inode的i_count變爲0的時候,會調用sb的proc_delete_inode函數,這個函數將inode的i_state設置 爲I_CLEAR,這可以理解爲將該inode刪除了,並調用de_put,減少並檢查proc_dir_entry的引用計數,如果到零,也將其釋放。因此我們看到,引用計數的機制使得VFS的inode結構和proc的proc_dir_entry結構能夠保持同步,也就是說,對於一個存在於緩存中的 的inode,必有一個proc_dir_entry結構存在。 

4.這時,我們已經得到了inode結構,並且將相應的proc_dir_entry結構de與inode鏈接在了一起。因此,就可以根據de的信息,對inode的一些域進行填充了。其中最重要的是使用語句: 
if (de->proc_iops) 
inode->i_op = de->proc_iops; 
if (de->proc_fops) 
inode->i_fop = de->proc_fops; 

將inode的操作函數集重定向到proc_dir_entry結構提供的函數集上。這是因爲我們可以通過proc_dir_entry結構進行方便的設 置和調整,但最終要將文件提交至VFS進行管理。正是在這種思想下,proc文件系統提供提供了一套封裝函數,使得我們可以只對 proc_dir_entry結構進行操作,而忽略與VFS的inode的聯繫。 

5.最後,成功地返回所要的inode結構。 

(七) 小結 
至此,已經對proc文件系統進行了一個粗略的分析,從文件系統的註冊,到proc_dir_entry結構的管理,以及與VFS的聯繫等等。下面我們對proc文件系統的整體結構作一個總結。 

proc文件系統使用VFS接口,註冊自己的文件類型,並且通過註冊時提供的proc_read_super函數,創建自己的超級塊,然後裝載 vfsmount結構。在proc文件系統內部,則使用proc_dir_entry結構來維護自己的文件樹,並且通過目錄文件的lookup函數,將 proc_dir_entry結構與VFS的inode結構建立聯繫。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章