天天看點

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結構,并将其加入到已裝載檔案系統的連結清單中(可參考圖-xx)。 

最後,傳回該vfsmount結構,并利用傳回值,使用指針proc_mnt指向該vfsmount結構。 

3.判斷傳回值是否錯誤,如果錯誤,那麼就解除安裝檔案系統。 

這樣,一個檔案系統就成功注冊到核心了。同樣,proc檔案系統的解除安裝,也非常簡單,代碼如下: 

static void __exit exit_proc_fs(void) 

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) 

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; 

該函數進行了如下幾步操作: 

1.在該函數裡,首先向作為參數傳入的超級塊寫入檔案系統的基本資訊,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) 

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; 

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; 

這個函數主要完成三部分的工作,第一,使用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; 

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; 

該函數在參數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結構的管理了。下面我們回過頭來,再看一下在檔案注冊函數中的一段代碼: 

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

1 對普通檔案的操作 

我們首先看一下普通proc檔案的函數集,根據代碼段: 

if (S_ISREG(dp->mode)) { 

我們可以看到,對于普通的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 

由此我們看出,該函數的實作依賴于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; 

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)) { 

我們可以看出,對于連結檔案,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) 

return vfs_follow_link(nd, s); 

和上面介紹的函數類似,它同樣利用VFS的函數實作其功能,對于vfs_follow_link,可以參考fs/namei.c檔案。其結構如下圖所示: 

3 對目錄檔案的操作 

最後我們看一下proc檔案系統對目錄檔案的操作函數集,在檔案注冊的時候,有如下代碼: 

從中我們可以看到,在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; 

for (de = de->subdir; de ; de = de->next) { 

if (!de || !de->low_ino) 

if (de->namelen != dentry->d_name.len) 

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); 

if (inode) { 

dentry->d_op = &proc_dentry_operations; 

d_add(dentry, inode); 

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結構了。)調用函數: 

這個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->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; 

__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)); 

if (de->proc_iops) 

inode->i_op = de->proc_iops; 

if (de->proc_fops) 

inode->i_fop = de->proc_fops; 

return inode; 

out_fail: 

我們根據程式流程,分析它的功能: 

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的一些域進行填充了。其中最重要的是使用語句: 

将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結建構立聯系。 

繼續閱讀