前言
Linux是目前蠻熱門的一個操作系統。很多人都知道它很是免費的,而且它也很穩定,更重要的是,它不會出現藍色畫面。可是,你知道嗎? Linux所支持的檔案系統高達十幾個,除了為它量身打造的Ext2之外,它還支持了Minix,FAT,VFAT,NFS,NTFS…等等。有沒有想過,它是怎麼做到使得可以同時支持十多個檔案系統呢? 沒錯,就是VFS,也就是這篇文章的重點。在這篇文章裡,我會跟各位介紹Linux檔案系統的結構,VFS所扮演的角色。
Linux檔案系統結構
圖:http://linuxfab.cx/Columns/17/Image2.gif
Linux的檔案系統在外型上其實跟UNIX檔案系統是一樣的。它是一個反轉過來的樹。最上層是系統的根目錄,也就是"/"。系統根目錄底下可以是目錄也可以是檔案,目錄裡也可以包含目錄,檔案等。如此就形成一個反轉過來的樹。我們知道,在Windows,如果你有二個partition,一個叫C,另一個叫D。當你要到D這個partition時,只要打"D:"就可以了。但是在Linux裡可不是這樣。要去讀取另一個partition的資料必須要經由mount的動作。像
mount -t ext2 /dev/hda3 /mnt
就會將硬盤第三個partition掛在/mnt這個目錄底下。Mount完之後,/mnt原本的內容會看不到,只會看到hda3裡的內容。其中/mnt我們稱為hda3的mount point。而/mnt這個目錄則是被hda3所cover。經過mount以後,我們就可以經由/mnt去讀取hda3的內容,就好象hda3的內容本來就放在/mnt底下一樣。整個過程,如圖1所示。
圖1(a)是原本的檔案結構,圖1(b)則是hda3這個partition的內容,將hda3 mount到/mnt之後,整個檔案系統就變成圖1(c)的樣子。不管如何,Linux會保持其檔案系統為一個tree的形狀。這樣mount下去,我們很容易可以推想到,從根目錄開始的這個tree很有可能包含好幾種的檔案系統,可能掛在/mnt上的是Ext2檔案系統,掛在/home上的是FAT,而掛在/cdrom上的則是iso9660檔案系統。我們知道,當使用者去讀取這些目錄裡的內容時,他本身是不用去管這個目錄掛的檔案系統是什麼。基本,使用者也不會感到有什麼不同。而就programmer的觀點來看,我們也不會說去讀/mnt裡的檔案和去讀/home裡的檔案要下不同的參數。Linux是怎麼做到這一點的呢? 它就是利用VFS來做到的。
1. VFS架構
Linux檔案系統其實可以分為三個部分,第一部分叫Virtual File System Switch,簡稱VFS。這是Linux檔案系統對外的接口。任何要使用檔案系統的程序都必須經由這層接口來使用它。另外二部分是屬於檔案系統的內部。其中一個是cache,另一個就是真正最底層的檔案系統,像Ext2,VFAT之類的東西,整個Linux檔案系統可以用圖2來表示
為了避免困擾,底下我們所講的檔案系統都是指Ext2,FAT等底層的檔案系統,至於包含VFS,Ext2,Buffer Cache等等我們總稱為VFS。
在圖2裡,我們可以清楚的看到當Kernel要使用檔案系統時,都是經由VFS這層接口來使用。剛才我們有提到一個問題,就是當使用者或程序設計師去讀取一個檔案的內容時,它不會因為這個檔案位於不同的檔案系統就需要使用不同的方式來讀取。因為這件事VFS已經幫我們做了。當我們要讀取的檔案位於CDROM時,VFS就自動幫我們把這個讀取的要求交由iso9660檔案系統來做,當我們要讀取的檔案在FAT裡時,VFS則自動呼叫FAT的函式來幫我們做到。當然,有需要時,VFS也會直接透過Disk driver去讀取資料。但是當我們要求讀寫檔案時,難道iso9660或FAT檔案系統會直接透過driver去讀寫嗎? 不是的。就像PC上除了內存之外,還有一層的cache來加快速度,在Linux檔案系統其實也是有一個Cache的機制以加快速度。叫做Buffer Cache。底層的檔案系統要讀寫磁盤上的資料時都要經過Buffer Cache。如果資料在Buffer Cache裡有的話,就直接讀取,如果沒有的話,才透過Buffer Cache要求driver去讀寫。除了Buffer Cache之外,其實,Linux檔案系統裡還有一個Cache,叫Directory Cache。你知道嗎? 如果我們去統計使用者的行為的話,ls這種命令其實占的比重是蠻大的。每次的ls或讀寫檔案其實都要對目錄的內容做search。因此,如果在目錄這方面能做個Cache的話,那系統整統的速度就會再往上提升。Directory Cache的功能就在此。其實,Linux檔案系統裡還有一個Cache,叫Inode Cache。故名思義,它是針對Inode做的Cache。Directory Cache跟Inode Cache其實關系是很密切的。
2. 檔案的表示
從使用者的觀點來看,我們可以用檔案的絕對路徑來代表某個檔案而不會出錯。在VFS裡,它並不是用路徑來代表檔案的。它是用一個叫Inode的東西來代表的。基本上,檔案系統裡的每一個檔案,系統都會給它一個Inode,只要Inode不一樣,就表示這二個檔案不是同一個,如果兩個的Inode一樣,就表示它們是同一個檔案。其實,Inode是VFS所定義的,而我們知道VFS裡包含了好幾種的檔案系統,並不是每一個檔案系統都會有Inode的這種概念。就像FAT,事實上它跟本沒有所的Inode概念。但是當VFS要求FAT去讀取某個檔案時,事實上它是把那個檔案的Inode傳給FAT去讀。所以,在VFS來講,每一個檔案都有其對應的Inode,但是在底層的檔案系統不見得是這種情形。因此,VFS跟底層的檔案系統溝通也是經過一層的接口。比方說,VFS要open一個位於FAT的檔案時,VFS會配置一個Inode,並把這個Inode傳給FAT,FAT要負責填入一些資料到Inode裡,必要時,也可以在Inode裡加入自己所需要的資料。再打個比方,VFS要讀取FAT裡檔案的內容,反正VFS就是把Inode給FAT,並且告訴它從那裡開始讀,讀多少個byte。其余的就是FAT的事,它就要想辦法讀出來。用"上有政策,下有對策"這句話來描述VFS跟底層的檔案系統的互動可能還蠻適合的。VFS的政策就是要以Inode為單位,但是底層的檔案系統還是照自己的方式去存放檔案,只要表面上將Inode填好,VFS要的東西給他就行了。
在VFS裡,Inode是檔案的單位,那檔案系統呢? 在VFS裡底層的檔案系統又是用什麼來表示呢? 要講這個之前,我們先來講講硬盤的layout。
3. Disk的Layout
圖:http://linuxfab.cx/Columns/17/Image3.gif
在這裡所講的disk是指硬盤,有關於floopy disk則不討論。我們知道一顆硬盤最多可以有8個partition,其中4個是primary partition,另4個則是extended partition。所謂partition就是在邏輯上將disk做切割。所以,我們可以把一顆硬盤想象成是由最多8個partition所組成的。除了partition之外,disk的第一個扇區我們稱為MBR。如圖3所示
http://linuxfab.cx/Columns/17/Image4.gif
在Linux裡,檔案放的位置是以一個partition為單位的,也就是說一個檔案系統是放在partition裡,而不能跨partition的。接下來讓我們來看看檔案系統在partition裡的layout是怎麼樣的,如圖4所示。
基本上,第一個block是boot block。用來開機用的。Super block則記錄了檔案系統重要的資料,接下來的東西就是記錄著inode和資料的區塊。
在VFS裡,每一個檔案系統是由其super block來表示的。之所以這樣,是因為super block裡存放了這個檔案系統重要信息。從一個檔案系統的super block就可以存取這個檔案系統中的任何檔案。因此,在Linux裡,檔案系統的管理以super block為單位,從super block可以取得這個檔案系統裡任何一個檔案的inode,從檔案的inode則可以對這個檔案做讀寫的動作,進而完成對Linux底下檔案的控制。因此,Kernel分別定義了一個super_block和inode的結構來描述檔案系統的super block和及inode。底下我們就分別來介紹super block和inode的結構。
Super block結構
super_block結構定義在裡,整個結構可分為基本資料,一組用來使用super block結構的函式,一組跟quota管理有關的函式,管理super block所屬檔案系統inode的信息,一些字段用來做super block的synchronization,以及各個檔案系統本身所特有的資料。
4. 基本資料
struct list_heads s_list;
kdev_t s_dev;
unsigned long s_blocksize;
unsigned char s_blocksize_bits;
unsigned char s_rd_only;
unsigned char s_dirt;
struct file_system_type *s_type;
unsigned long s_flags;
unsigned long s_magic;
unsigned long s_time;
struct dentry *s_root;
以上這些字段是我認為super_block結構裡屬於基本資料的部分,在這裡,我沒有依照原始程序的寫法依序將字段列出來,而是將相關的整理在一起。s_list這個字段是用來將super block串在一起的。在Linux裡,同一時間Kernel可能會擁有好幾個檔案系統的super block,因此,它有它自己一套的super block管理方式,平常也許我們會另外寫一個linked list,裡面用一個字段存放super block,用這種方式把super block串在一起,但是,Kernel不是這樣做,它也是用一個串行來把super block放在一起。但是,它把它寫到super block結構裡,s_list就是用來將super block串起來的。用法跟一般人寫法不同,在super block的管理我將為各位介紹。
s_dev是此super block所屬檔案系統所在的device代碼。檔案系統內部的管理不是用檔案做單位,而是以block為存取的單位,而s_blocksize就是用來記錄一個block是幾個byte。因此,如果一個block是1024 byte的話,那s_blocksize為1024,而s_blocksize_bits就是10,這個字段是指一個block需要幾個bit來表示。而s_rd_only從字面上來看應該是記錄檔案系統或super block是否只讀,目前這個字段是被設為0,還沒有被使用。至於s_dirt則是記錄此super block的內容是否被改過,用來判斷是否最後要將super block寫回disk裡,當super block被更動之後,s_dirt會被設為1。s_type的型別是file_system_type,這是一種來描述檔案系統的結構,在這裡,是用來記錄這個super block是屬於那一個檔案系統。有關這種型別,我們將會在super block的管理中探討。當我們使用檔案系統時,第一步就是要做mount的動作,在mount的時候,還需要給它參數,像是mount成只讀或可擦寫等,這些參數就是記錄在s_flags裡。在Linux或UNIX裡,magic number通常是用來做識別用的,而檔案系統的magic number就是設在s_magic字段裡,像目前Ext2的magic number就是0xEF53。從檔案系統的super block我們可以讀取到這個檔案系統任一個檔案,但是,前提是我們必須要先知道這個檔案系統的根目錄在那裡才可以。就像給我們一個絕對路徑我們可以找到那個檔案,但是,找的方式是先從根目錄,再往下層去找。因此,super block必須記錄它所代表的檔案系統根目錄在那裡,這就記錄在s_root裡。
5. super block的synchronization
unsigned char s_lock;
struct wait_queue *s_wait;
上面這兩個字段是用來做super block的synchronization。s_lock記錄著目前super block是否被鎖住,如果是,其值為1,若不是,則為0。s_wait是一個wait queue的結構,被放到queue裡行程將會進入sleep的狀態,直到被叫醒為止。基本,如果要改變super block的內容,需要先呼叫lock_super()鎖住super block以免產生race condition。改完之後則要呼叫unlock_super()將lock釋放掉。而lock_super()跟unlock_super()就是利用這兩個字段來做的。
6. 管理Quota的函式
這個字段所記錄的是一組的quota管理函式,檔案系統本身可以自己提供一套的quota管理函式,不過,很幸運的是VFS本身已經提供了一組通用的quota管理函式。目前Linux的檔案系統中只有ext2有支持quota管理,同時,ext2是直接使用VFS所提供的函式。VFS所提供的這組函式定義在裡,存放在dquot_operations變量裡。如果檔案系統要使用quota的函式時,必須先將dq_op這個字段填好,但是,要注意的是,dq_op這個字段並不是在super block初始化時填入的,它是當檔案系統已經mount好,才在由激活quota的程序填入的,這一點跟其它字段倒是不太相同,在Linux裡,dq_op的填入是經由quotaon這個程序去呼叫quotactl()這個系統呼叫去做的。
7. 儲存檔案系統本身資料的字段
super_block結構是所有檔案系統所共同使用的一個結構,但是,除了共同的部分之外,檔案系統之間也有著相當的差異性,因此,為協調此差異性,在super_block結構有一個字段是專門來存放各個檔案系統所獨自享有的信息。這些信息不用說當然是在呼叫檔案系統提供的read_super()時所填入的。在Kernel 2.2.1裡,這個字段是這樣子的
union {
struct minix_sb_info minix_sb;
struct ext2_sb_info ext2_sb;
…
struct hpfs_sb_info hpfs_sb;
struct hfs_sb_info hfs_sb;
struct adfs_sb_info adfs_sb;
struct qnx4_sb_info qnx4_sb;
void *generic_sbp;
} u;
因為每個super_block在同一時間內最多只會記錄一個檔案系統的資料,所以,這個字段是union。像ext2_sb就是專門存放ext2檔案系統本身所額外需要的信息,由ext2_read_super()函式填入的。
8. 管理inode的字段
一個檔案系統裡有許多的inode,但是,有的inode可能因為使用者的關系,其內容被更改,此時,我們稱此inode為dirty。所有的dirty inode都應該被記錄,以便在適當時候寫入disk,而這個存放的位置在super block是蠻適合的。每一個super_block代表一個檔案系統,把這個檔案系統的dirty inode記錄在它自己的super_block中應該是不錯的想法。在Kernel 2.2.1中在super_block裡跟inode有關的字段有4個。
struct inode *s_ibasket;
short int s_ibasket_count;
short int s_ibasket_max;
struct list_head s_dirty;
其中s_dirty就是用來存放dirty inode用的。我們可以看到s_dirty的型別跟s_list的型別是一樣的,所以,其實s_dirty也是一個串行。每一個inode結構裡都有一個struct list_head結構的字段,s_dirty的工作就是將這個dirty inode的struct list_head結構的字段串起來,最後我們經由super_block的s_dirty就可以讀取到這些dirty inode裡list_head結構字段的地址,然後再經由這些字段讀取到其對應的inode的地址。
除了s_dirty之外,還有三個字段是跟inode有關的,分別是s_ibasket,s_ibasket_count,s_ibasket_max。有關於這三個字段的用法目前我還不是很清楚,只是大概知道當檔案系統的剩余空間太少時,Kernel會根據這三個字段的值呼叫一個callback的函式來做些處理。使用者可以在register_file_system()時將FS_IBASKET的參值傳給它,系統就會激活這項功能,但是,很可惜的是,在Kernel 2.2.1中,這項功能還有bug,所以尚未正式使用。
9. 操作Super block的函式
super block裡有一個字段是用來記錄一組的函式,這個字段的型別是super_operations。這個結構在Kernel 2.2.1裡包含了11個函式指針。這些指針是要讓VFS來呼叫的。因此,這是VFS和檔案系統之間的一個接口,經由這層接口,super block可以控制檔案系統底下的檔案或目錄。
struct super_operations *s_op;
在super_block結構裡,s_op就是用來記錄這一組的函式。這組函式必須由寫檔案系統的人來提供。底下我們就來看看super_operations裡各個函式應該要提供什麼樣的功能。
struct super_operations {
void (*read_inode) (struct inode *);
void (*write_inode) (struct inode *);
void (*put_inode) (struct inode *);
void (*delete_inode) (struct inode *);
int (*notify_change) (struct dentry *,struct iattr *);
void (*put_super) (struct super_block *);
void (*write_super) (struct super_block *);
int (*statfs) (struct super_block *,struct statfs *,int);
int (*remount_fs) (struct super_block *,int *,char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);
};
不知各位有沒有發現,在這11個函式裡,居然沒有一個函式是用來讀super block的。其實也沒什麼好奇怪的,因為讀取super block的函式早在注冊檔案系統時就要給了,要不然,是誰把super block讀出來。因此,我們可以發現super_operations裡只有write_super(),put_super()等等,而沒有read_super()。以下就分別來討論各個函式應該提供什麼樣的功能,如果你想自己寫一個檔案系統,這部分可是很重要的喔。
· write_super(sb)
故名思義,這個函式主要就是用來將sb這個super block寫到磁盤上的。在正常情況下,write_super()應該要檢查sb->s_dirt是否為True,只有當s_dirt為True時才將super block寫回disk裡。當然,我想這部分主要還是看各個檔案系統是如何的implement,但是,記得write_super()最後應該要將s_dirt設為0,表示這個super block不再是dirty。還有一件事,就是write_super()應該要檢查檔案系統是否被mount成只讀(檢查sb->s_flags&MS_RDONLY),或檔案系統本身就是只讀,像iso9660檔案系統,在這種情況下,由於系統是只讀,所以,系統設計者可以不提供write_super()或讓write_super()不做事。
· put_super(sb)
當檔案系統被umount時,VFS就會呼叫檔案系統的put_super()。所以,put_super()要做的事就是將super block所配置的buffer釋放掉。此外,一般來講,如果檔案系統是寫成module的話,通常在put_super()也會呼叫MOD_DEC_USE_COUNT將module的reference count減一。至於MOD_INC_USE_COUNT則是應該在read_super()時做的。除此之外,有點要注意,有的人會認為put_super()應該要將sb釋放掉,但是,根據Kernel 2.2.1版的原始碼看來,這部分的工作是由VFS來做的。(所謂釋放掉並不是呼叫kfree()將super block所占的內存釋放,而是將sb->s_dev設成0而已。VFS會自動將s_dev為0的super block視為空的,而拿來重復使用。)
· read_inode(inode)
我想大家看名字就知道了,read_inode()就是去讀一個inode,並將它放到傳過來的inode結構裡。在VFS裡,read_inode()只會被get_new_inode()呼叫,而get_new_inode()又只會被iget()呼叫,所以,事實上,當我們去看Kernel的原始碼的時候,我們不會看到直接呼叫read_inode()的情形出現,通常是呼叫iget()傳回所要的inode。有的人會有疑問,read_inode()怎麼知道要讀那一個inode出來呢? 很簡單,在呼叫read_inode()之前,VFS會先在inode結構裡填入一些資料,以讓read_inode()得知要讀那個inode。在VFS裡,它會填入以下的值:
inode->i_sb = sb;
inode->i_dev = sb->s_dev;
inode->i_ino = ino;
inode->i_flags = 0;
inode->i_count = 1;
inode->i_state = I_LOCK;
當然,在你自己寫的read_inode()裡是不會用到這麼多資料的,會用到的大概只有i_sb,i_no這兩個而已。其實,就跟super block結構一樣,inode結構也有一個字段是用來放檔案系統自己認為需要的資料,這個字段通常也是在read_inode()中做的,除此之外,read_inode()最重要的一件事就是填入i_op這個字段,這個字段也是一組的函式,這組的函式是用來運作inode的。底下這些程序代碼是從Ext2檔案系統的ext2_read_inode()中取出的,它們就是用來填i_op這個字段。
else if (S_ISREG(inode->i_mode))
inode->i_op = &ext2_file_inode_operations;
else if (S_ISDIR(inode->i_mode))
inode->i_op = &ext2_dir_inode_operations;
else if (S_ISLNK(inode->i_mode))
inode->i_op = &ext2_symlink_inode_operations;
else if (S_ISCHR(inode->i_mode))
inode->i_op = &chrdev_inode_operations;
else if (S_ISBLK(inode->i_mode))
inode->i_op = &blkdev_inode_operations;
· write_inode(inode)
write_inode()要做的事就是將inode寫回disk。
· put_inode(inode)
put_inode()是跟read_inode()是相對的。基本上,呼叫一次read_inode()就應該呼叫一次put_inode()。但是,在Kernel中,put_inode()跟read_inode()一樣是不會直接被使用的。put_inode()只有在iput()中會被呼叫,iget()根據super block以及inode number以取得inode結構,而iput()則是iget()的相反,用iget()取得的inode應該用iput()釋放掉。其實,inode結構裡有一個i_count的字段,這是用來記錄這個inode的reference count,所以,當我們取得inode時,應該對i_count加1,而在釋放inode時則應該將i_count減1,但是,很幸運的,iget()和iput()已經幫我們做好這件事,我們不用在read_inode()和put_inode()中做了。那put_node()應該做什麼呢? 它不用將inode中所配置的內存釋放掉,因為這件事應該要在inode的reference count等於0而hard link的個數等於0的時候做,VFS會自動幫我們呼叫適當的函式。put_inode()所做的事是根據檔案系統而有不同的需求,像在Ext2中就是將inode的prealloc的block釋放掉。但切記,盡管呼叫put_inode()之後,inode->i_count不見得就會變成0,有可能有別的行程在使用這個inode,所以,put_inode()做的事不應該防礙別的行程對此inode的運作。
· delete_inode(inode)
delete_inode()做的事當然就是將inode刪除掉啰。之前說過,當inode的reference count等於0時,VFS會開始將inode所占的內存釋放掉。但是,如果這個時候hard link的個數也是0的時候,VFS就會呼叫delete_inode()將inode()從disk上刪除掉。所以,user所提供的delete_inode()要做的事就是把disk上關於這個inode的資料以及檔案系統自己本身所配置的東西刪除掉。至於釋放掉inode所占的內存則交由VFS來做吧。
· clear_inode(inode)
這個函式是用來將inode結構裡的信息清除。檔案系統應該只清除自己加在上面的資料,其余的部分應交由VFS來做。在Kernel裡,是不會直接呼叫s_op->clear_inode()的,VFS提供一個函式也叫clear_inode(),它會呼叫s_op->clear_inode()。所以,如果有需要用到clear_inode()應該呼叫VFS提供的clear_inode()而不是s_op->clear_inode()。
· statfs(sb,statfs,size)
這個函式是用來取得檔案系統的統計資料,statfs,fstatfs,和ustat這幾個系統呼叫其實都是直接呼叫statfs來將傳入的statfs結構填滿,檔案系統本身的統計資料本來就只有檔案系統自己最清楚,所以,statfs()由super block來提供也是最為適當。
· remount_fs(sb,flags,options)
當一個檔案系統已經被mount之後,如果我們想改變mount時所給予給的參數,可以執行mount這個命令,並在其-o參數後加入remount就可以了。基本上,remount所造成的參數改變VFS會幫我們做好,只是,為了怕參數的改變會對檔案系統本身造成行為上的改變,所以,當user要求remount時,VFS會再呼叫叫s_op->remount_fs()以告訴檔案系統user要改變mount的參數,如果檔案系統本身有需要的話,可以在remount_fs()裡做適當的調整,如果覺得不需要,那甚至可以不用提供這個函式讓VFS使用。
· umount_begin(sb)
不知道各位有沒有遇到過樣的情形,當我們在某個檔案系統中時,如果我們正在讀寫一個檔案,可是由於某種不知名的原因,造成segmentation fault或是什麼。結果當我們要將檔案系統umount時,系統卻告訴我們device is busy,所以無法umount。因此,新版的umount支持一個選項叫強迫性的umount,在上面這種沒辦法正常umount系統的情況下,就可以使用強迫性的umount。但是,事實上,VFS雖然有提供這樣的功能,但是還是得要底層的檔案系統支持才行。支持的方式是底層的檔案系統要提供umount_begin()這個函式才行。要不然,盡管VFS支持,強迫性的umount仍然是做不到。老實說,umount_begin()要做什麼我也不太清楚,因為好象沒什麼檔案系統有提供這個函式。不過,根據VFS的原始碼來看,它的工作應該是要把檔案系統內部的state設回正常情況才對,其它的事就不用做了,交給VFS就對了。
· notify_change(dentry,attr)
在很多情況下,我們會對一個檔案或目錄的inode做出改變。比方說,我們可以對某個檔案呼叫utime()改變這個檔案的access time,或者是可以呼叫truncate()把檔案的長度減短,這些系統呼叫都會改變檔案的inode的屬性。有的人會想到,那我們直接把inode拿來改改就好了嘛,何必提供這麼個函式呢? 沒錯,VFS是的確把inode拿來改一改,但是,我們有說過,VFS做的事是屬於全部檔案系統所共同的部分,而檔案系統之間的差異性,必須由各個檔案系統提供函式來做。因此,萬一使用者改了某個跟檔案系統有很大關系的屬性時,檔案系統本身必須被告知才行。因此,notify_change()就是VFS告知檔案系統的接口。跟上面很多函式一樣,檔案系統的notify_change()並不會直接被呼叫,s_op->notify_change()是被包裝在VFS函式裡,這個函式也叫notify_change(),它做的事就是把inode裡的字段做user所要求的改變,並呼叫檔案所屬的檔案系統的notify_change()。可以改變的屬性是放在一個叫iattr結構裡。
struct iattr {
unsigned int ia_valid;
umode_t ia_mode;
uid_t ia_uid;
gid_t ia_gid;
off_t ia_size;
time_t ia_atime;
time_t ia_mtime;
time_t ia_ctime;
unsigned int ia_attr_flags;
};
這個結構的宣告可以在裡找到。ia_valid這個字段用來描述底下這幾個字段那些是要改變的,ia_mode指的是新的權限,ia_uid為使用者id,ia_gid為群組id,ia_size是檔案大小,ia_atime是access time,ia_mtime為modification time,ia_ctime是creation time,ia_valid的值可以是底下這幾個常數的OR值。
#define ATTR_MODE 1
#define ATTR_UID 2
#define ATTR_GID 4
#define ATTR_SIZE 8
#define ATTR_ATIME 16
#define ATTR_MTIME 32
#define ATTR_CTIME 64
#define ATTR_ATIME_SET 128
#define ATTR_MTIME_SET 256
#define ATTR_FORCE 512
#define ATTR_ATTR_FLAG 1024
Inode 結構
inode在Linux裡算是一個蠻大的結構,跟super_block比起來可是不惶多讓,基本上,跟super_block結構一樣,我們一樣可以把inode結構分成幾部分來看: 串行管理字段,基本資料,用來做inode synchronization的資料,跟內存管理有關的資料,Quota管理字段,跟file lock有關的字段,以及一組用來操作inode的函式。以下我們分別來說明這些字段的意義。
1. 串行管理字段
inode結構前三個字段就是用來幫助將inode串起來的字段,分別是
struct list_head i_hash;
struct list_head i_list;
struct list_head i_dentry;
這跟我們在super block那裡所看到的s_list是屬於同樣的型別,都是struct list_head。list_head這種結構在Kernel裡實在用的很多,事實上,它也的確很好用。我們將在這篇文章的最後跟您徹底討論list_head結構以及它的用法。現在我們只要知道list_head可以幫我們將一些結構串行在一起就夠了。在VFS裡,有四個串行是用來管理inode的,分別是inode_unused用來將目前還沒使用的inode串在一起,它就是使用i_list這個字段。第二個是inode_in_use用來將目前正在使用的inode串在一起,當一個inode被使用時,它會從inode_unused中被取出來,因此,此時i_list不會被用到,接著它會利用i_list字段放到inode_in_use中。第三個是sb->s_dirty用來將dirty inode串行在一起。這個串行的開頭位於super block的s_dirty字段,一樣也是使用i_list串接。所有正在使用中的inode都可以經由inode_in_use串行找到,但是,因為系統的inode太多,所以,串行可能會很長,如果慢慢找,在速度上並不理想,因此,每個使用中的inode都會計算出其hash value,並且放到hash table,但是hash table有時會有collision的情形出現,因此每一個entry是由一個list串接起來,這個list就是利用i_hash字段來串接的。至於i_dentry是在dcache中使用的,dcache利用這個字段將inode串接起來。
2. 基本資料
inode的基本資料蠻多,在此我很簡略的跟各位介紹一下
unsigned long i_ino;
每一個inode都有一個序號,經由super block結構和其序號,我們可以很輕易的找到這個inode。
unsigned int i_count;
在Kernel裡,很多的結構都會記錄其reference count,以確保如果某個結構正在使用,它不會被不小心釋放掉,i_count就是其reference count。
kdev_t i_dev; /* inode所在的device代碼 */
umode_t i_mode; /* inode的權限 */
nlink_t i_nlink; /* hard link的個數 */
uid_t i_uid; /* inode擁有者的id */
gid_t i_gid; /* inode所屬的群組id */
kdev_t i_rdev; /* 如果inode代表的是device的話,
那此字段將記錄device的代碼 */
off_t i_size; /* inode所代表的檔案大小 */
time_t i_atime; /* inode最近一次的存取時間 */
time_t i_mtime; /* inode最近一次的修改時間 */
time_t i_ctime; /* inode的產生時間 */
unsigned long i_blksize; /* inode在做IO時的區塊大小 */
unsigned long i_blocks; /* inode所使用的block數,一個block為512 byte*/
unsigned long i_version; /* 版本號碼 */
unsigned long i_nrpages; /* inode所使用的page個數 */
struct page *i_pages;
/* inode使用的page會被放在串行裡,這個字段記錄著此串行的開頭 */
struct super_block *i_sb; /* inode所屬檔案系統的super block */
unsigned long i_state;
/* inode目前的狀態,可以是I_DIRTY,I_LOCK和 I_FREEING的OR組合 */
unsigned int i_flags; /* 記錄此inode的參數 */
unsigned char i_pipe; /* 用來記錄此inode是否為pipe */
unsigned char i_sock; /* 用來記錄此inode是否為socket */
unsigned int i_attr_flags; /* 用來記錄此inode的屬性參數 */
struct file_lock *i_flock; /* 用來做file lock */
3. 內存映對
在Linux裡,我們可以利用mmap()將檔案或device的某個區塊映像到記體裡使用。在inode裡這兩個字段就是跟它有關的:
struct vm_area_struct *i_mmap;
int i_writecount;
i_writecount這個字段的值是用來記錄目前有多少個行程是以可寫入的模式開啟此檔案的。為什麼需要這個值呢? 因為系統沒辦法支持可以對一個檔案寫入,而又同時將這個檔案映像為MAP_DENYWRITE的模式,所以,用這個字段來代表目前有多個行程可對此inode做寫入的動作或是有多少個行程將它映像成MAP_DENYWRITE的模式。它的值有以下三種情形:
0: 沒有行程將它開啟為可寫入,也沒有行程對它做MAP_DENYWRITE的映像
< 0: 有-i_writecount個行程對它做MAP_DENYWRITE的映像。
> 0: 有i_writecount個行程將它開啟為可寫入模式。
至於i_mmap這個字段就是用來做內存映像的字段。
4. inode synchronization
就跟super_block結構一樣,Kernel裡的重要結構在修改時,都必須做好synchronization的動作,以免產生race condition,造成系統出錯。因此,當我們要修改某個inode結構時,必須先確定沒有人在使用這個inode才行。這件事是使用semaphore和wait queue來完成的。
struct wait_queue *i_wait;
struct semaphore i_sem;
除了這兩個字段之外,新版的Kernel又多加了一個字段叫i_atomic_write,這也是一個semaphore,那它的用途又是什麼呢? 相信如果你用過pipe的話,一定知道當我們寫資料到pipe裡的時候,資料長度必須小於等於PIPE_BUF這個值,所以當寫入的資料小於等於PIPE_BUF時,Kernel要確保寫入的動作是atomic的,因此加了這個字段來做控制。
struct semaphore i_atomic_write;
5. Quota相關字段
在前面講super_block時,我們說過裡面有個字段dq_op是用來存放quota函式用的。因為在Linux裡,quota的管理可分為兩種,一是所使用的block數限制,另一種則是使用的inode數目的限制。所以,將quota管理的資料放在inode是蠻適合的。至於將quota函式放在super block裡則是因為同一個檔案系統會使用相同的quota管理方式,而剛好從任一個inode都可以經由i_sb取得其super_block結構,所以,這也就是為什麼quota函式要放在super block裡。
struct dquot *i_dquot[MAXQUOTAS];
目前的quota管理還可以分為user quota管理和group quota管理,所以,其實MAXQUOTAS這個常數的值是2。在i_dquot裡,一個是用來管理user quota,另一個則是管理group quota。
6. 操作inode的函式
就跟super_block結構一樣,每一個inode都有一個i_op的字段用來記錄一組操做inode的函式。
struct inode_operations *i_op;
接下來,我們就來看看inode_operations結構裡各個函式是做什麼用的:
· create(dir,dentry,mode)
當我們要產生一個新的檔案時,Kernel必須要先為這個檔案產生一個inode,當然,配置inode內存這種事是屬於VFS的工作范圍,但是產生一個inode這件事跟檔案系統本身有蠻大的關系,因此,VFS會呼叫檔案系統裡i_op->create()來做些額外的事,那i_op這個字段是打那兒來的呢? 因為一個檔案一定是位於某個目錄底下,所以,i_op這個字段就是從檔案所在目錄的inode裡取出來的。而傳給create()的dir就是那個目錄的dentry指針,dentry則是我們要產生的檔案的dentry (此時dentry已經配置好,但內部的數據卻還沒填入),而mode則是產生檔案時所給的模式。我們知道Linux裡有很多種的inode,有的是代表普通檔案,有的則是代表目錄,還有代表socket,pipe的。不同種類的inode其i_op所提供的函式都不盡相同,像一個普通的檔案,我們根本不可能去呼叫它的create()函式,因為它不是目錄,它沒辦法在目錄底下產生一個inode。而像代表目錄的inode就必須要提供create()才行,不然沒辦法在其底下產生子目錄或檔案。
· lookup(dir,dentry)
這個函式也是代表目錄的inode所應該提供的。比方說我們有一個檔案叫/usr/tmp/hello.txt,如果我們想讀取這個檔案的內容時,第一步就是要開啟這個檔案,如果要開啟這個檔案,我們首先就得先找到這個檔案的inode。那Kernel是怎麼找到它的inode的呢? 它會呼叫根目錄的inode->i_op->lookup()找到/usr的dentry,則呼叫/usr目錄的inode->i_op_lookup()找到/usr/tmp的dentry,接著再呼叫/usr/tmp的inode->i_op->lookup()找到/usr/tmp/hello.txt的dentry。而從它的dentry我們自然可以取得它的inode。而lookup()的用處就是從dir目錄底下找到名稱跟dentry指定的相同的檔案dentry,基本上,這是屬於檔案系統應該做的事,VFS只負責幫你配置好dentry結構,並填入要找的文件名稱。
· link(old_dentry,dir,dentry)
在Linux裡,除了symbolic link之外,還有一種叫hard link的東西,symbolic link有它自己的inode,只是其內容指到別的檔案的路徑而已,但是hard link卻是跟指到的檔案共享一個inode,但是,hard link只能跟指到的檔案位於同一個檔案系統而已。當被指到的檔案被刪除時,只是你看不到那個檔案而已,事實上,檔案仍然是存在的,你可以使用之前建立的hard link來讀取它。系統有提供一個叫ln的命令可以產生hard link,有興趣的朋友可以試試看。而就programmer來講,系統也提供了一個叫link()的系統呼叫來做hard link。link()在准備好一切之後,會呼叫i_op->link()去處理檔案系統方面要做的事,i_op->link()至少應該要將inode->i_nlink的值加一才行。在i_op->link()的參數裡,old_dentry是指被指到檔案的dentry,dir是指我們所要產生的link所在目錄的dentry,至於dentry則是要產生的link的dentry。這個函式是代表目錄的inode所應該提供的。
· unlink(dir,dentry)
相信很多人都用過unlink()這個系統呼叫,這是用來將dir指到的目錄底下的dentry檔案刪除掉。在真正刪除之前,它會去檢查dentry->d_inode->i_nlink是否歸0,只有在nlink的值是0時才會刪除。unlink()系統呼叫最後會呼叫i_op->unlink()去做檔案系統額外要做的事,它至少應該把dentry->d_inode->i_nlink的值減一才對。跟i_op->link()一樣,i_op->unlink()也是目錄型別的inode所應提供的。
· symlink(dir,dentry,symname)
這個函式故名思義就是用來產生symbolic link用的。dir是symbolic link所在的目錄dentry,symname則是symbolic link的內容,通常是個路徑名稱,至於dentry則是symbolic link本身的dentry。系統提供了一個symlink()的系統呼叫,就是用來做symbolic link的,它最後也是呼叫i_op->symlink()來處理。當然,每個檔案系統內部要如何產生symbolic link的方式不盡相同,以ext2來講,如果symname的長度小於60個byte的話,那在Ext2而言,這是一個fast symbolic link,因為路徑名稱就直接存在inode結構裡,不用另外讀取disk,所以,當i_op->symblink()被呼叫時,它的工作就是將路徑名稱加到inode裡,但是如果大於等於60個byte,那就稱為slow symbolic link,symname的內容會被放到disk上的block裡,此時,i_op->symlink()就需要配置一個block存放symname的內容。這個函式也是目錄型別的inode所應提供的,當然,如果你不想提供的話,也可以直接設成NULL。
· mkdir(dir,dentry,mode)
這個函式就是在產生一個目錄時用,系統有提供mkdir()系統呼叫來產生目錄,而這個系統呼叫最後會呼叫i_op->mkdir()來做底層的事情。dir是指我們要產生的目錄所在的目錄,至於dentry則是要產生的目錄dentry,mode則是目錄的權限。之前我們曾說過,每個inode->i_nlink記錄了hard link的個數,而事實上,在代表目錄的inode裡,i_nlink的意義則跟它很像,它的意思是指目錄裡有幾個檔案或子目錄,所以,每個目錄剛產生時,它的i_nlink的都是2,因為,每個目錄至少有二個子目錄,分別是"."和".."。當產生完子目錄之後,dir->d_inode->i_nlink的值也應該加1才對。如果inode是目錄的話,那它應該提供這個函式才對。
· rmdir(dir,dentry)
i_op->rmdir()所做的事是跟mkdir()是相反的。跟mkdir()一樣,i_op->rmdir()最後也會被rmdir()系統呼叫所使用。當VFS要呼叫rmdir()之前,它會先替我們把要刪除的目錄名稱dentry找到,並把其父目錄的dentry也找到,其中dir就是其父目錄dentry,dentry就是指要刪除的目錄的dentry。當然,在呼叫i_op->rmdir()去刪除目錄時,VFS會先呼叫permission()並檢查我們是否可以刪除此目錄並檢查目錄此時的狀態,比方像目錄是否現在被mount,是否為系統根目錄,以及使用者要刪除的是否為目錄等等。所以,i_op->rmdir()要做的事就是純粹檢查目錄是否為空的,是否目前還有別人在使用它,並做好刪除目錄的事情。如果inode是目錄的話,那它應該提供這個函式才對。
· mknod(dir,dentry,mode,rdev)
在Linux裡,也有一個命令是叫mknod。mknod命令主要是用來產生的special file,像是character device,block device,或fifo,socket之類的東西。同時,系統裡也有一個系統呼叫mknod(),這個mknod()系統呼叫不盡可以產生特殊檔案,也可以產生一般的檔案,詳情可見其man page。而事實上,mknod()系統呼叫最後也是呼叫檔案系統的mknod()函式。mknod()系統呼叫會先檢查user給的參數是否對,像是如果你指定要產生一個目錄,VFS就先把你踢掉,除此之外,VFS還會先替你產生一個空的dentry用來放要產生的檔案,當然,它也會檢查user是否有權力產生這個檔案,最後,它會把重頭戲都交給i_op->mknod()去做。而i_op->mknod()要做什麼呢? 當然,這部分是跟各個檔案系統內部有關,基本上,這個函式需要在dir這個目錄底下產生一個inode,其模式為mode,如果mknod()要產生的檔案是device的話,那rdev就這個device的major number與minor number組合。除此之外,最重要的一件事就是要根據mode,在inode->i_op填入適當的值,比方說,如果產生一個character device,那inode->i->op應該指定一組操作character device的函式,如果產生的是普通檔案的話,那inode->i_op也應填入操作普通檔案inode的函式。這個函式對代表inode的目錄而言,也是應該要提供的。
· rename(old_dir,old_dentry,new_dir,new_dentry)
這個函式是用來將位於old_dir裡的old_dentry檔案改名為new_dir裡的new_dentry文件名稱。檔案系統所提供的rename()要做的事就是根據系統的implementation把改名字的事情做好,其它像是權限的檢查在上層VFS會幫我們做好。要注意的是,old_dir->d_inode->i_nlink的值應該減一,而new_dir->d_inode->i_nlink的值則是應該加一。這個函式在Kernel裡只有被系統呼叫rename()呼叫而已。有的人可能會以為這個函式應該由代表檔案的inode提供,但事實上,這個函式必須要由代表目錄的inode提供。理由就留給各位去想了。
· readlink(dentry,buffer,buflen)
只有當inode是代表一個symbolic link時,才需要提供這個函式,其它諸如檔案或目錄是不用提供這個函式的。這個函式的用處在於讀取symbolic link的內容,也就是讀取symbolic link指到的檔案路徑。跟上面其它的函式一樣,這個函式最後也是會被系統呼叫readlink()所呼叫。至於readlink()要如何做是跟檔案系統的implementation有關。像ext2,當檔案路徑的長度小於60個時,會直接從inode裡讀出資料,如果不是,則會讀取disk上記錄路徑的block內容。dentry是代表symbolic link的inode,buffer是要路徑存放的位置,至於buflen則是buffer的長度。
· follow_link(dentry,base,follow)
跟前一個函式一樣,follow_link()這個函式只有symbolic link的inode需要提供。我們知道,當我們讀到一個symbolic link叫a時,如果a指到/usr/hello.txt的話,那當我們讀a時,事實上會讀到/usr/hello.txt。這部分的工作就是由follow_link()完成的。這部分的轉換就使用者的觀點來看是不會感覺到的。在Linux裡,並沒有一個系統呼叫會呼叫follow_link()的。這個函式事實上是由lookup_dentry()呼叫do_follow_link(),再由do_follow_link()呼叫i_op->follow_link()。在Kernel裡,尋找某個檔案的inode是由namei(),再由它呼叫lookup_dentry()完成的,lookup_dentry()會由目錄的最上層一層一層的找,如果找到的檔案是symbolic link時,它最後會呼叫symbolic link的follow_link(),而follow_link()應該要讀取所指到的檔案路徑,並且再呼叫lookup_dentry()去找這個檔案,找到之後,再把它的dentry傳回去。
· readpage(file,page)
在Linux裡,每一個inode都代表一個檔案或目錄,而每一個檔案在系統中則是由一個file結構所記錄,readpage()就是將此file裡的page內容讀進來。基本上,VFS已經提供了一個readpage()的函式叫generic_readpage(),定義在,可以直接使用這個函式。
· writepage(file,page)
這個函式則是跟readpage()相反,是將page中的內容寫回file裡。但是,在VFS裡並沒有提供這樣的一個函式可供使用,所以,如果有需要的話,需要自己提供。
· bmap(inode,block)
bmap()主要是用在做內存映像時用的。block是一個數字,它代表的是inode所代表的檔案邏輯上的第幾個block,bmap()負責將這個block的序號轉換成disk上的區塊序號。
· truncate(inode)
truncate()的作用就是用來將inode所代表的檔案長度減小或增加,當然,詳細的implementation是要依照系統而有所不同。至於最後的長度應該是多少,則是由VFS在呼叫i_op->truncate()之前將想要改變的長度填在inode->i_size裡。
· permission(inode,mask)
故名思義,這個函式用來檢查inode的權限,一般來講,i_op->permission()在Kernel裡並不會被直接呼叫,VFS提供一個也叫permission()函式,這個函式會去呼叫i_op->permission()。一般系統裡如果要檢查權限都是直接呼叫VFS提供的permission(),再由VFS的permission()去呼叫i_op->permission()。如果檔案系統有提供i_op->permission()時,那就以i_op->permission()的結果為准。如果沒有,就依照VFS的標准來做。mask的可以是MAY_READ,MAY_WRITE,和MAY_EXEC這三個值的OR組合。
· smap(inode,sector)
smap()的作用跟bmap()很像,但是,sector在這裡指到是disk上的sector number。而不是邏輯上的block number。大部分的檔案系統都沒有提供這個函式,除了在umsdos有提供之外。
· updatepage(file,page,offset)
關於這個函式我也不太清楚,不過,可以知道的是,這個函式目前只有NFS檔案系統有提供。有興趣的朋友可以參考NFS的原始碼。
· revalidate(dentry)
由於NFS有cache的問題,所以,這個函式主要也是在NFS中所使用的,為的是將dentry->i_node的內容做refresh。在裡有一個函式叫do_revalidate()就會呼叫這個函式,很多系統呼叫像stat等都會呼叫do_revalidate()對inode做refresh。
如果我們去看inode_operations結構的內容,就可以發現第一個字段是default_file_ops。其實這也是一組的函式,在Linux裡,每一個檔案都會有一個file結構來描述,而每一個file結構都會定義一組的函式來操作file結構,在inode裡,也同時記錄了用來操作inode所代表的檔案的函式。在開始講操作file結構的函式之前,讓我們來看看file結構的內容。
File結構
在Linux裡,每一個檔案都有一個file結構和inode結構,inode結構是用來讓Kernel做管理的,而file結構則是我們平常對檔案讀寫或開啟,關閉所使用的。當然,從user的觀點來看是看不出什麼的。在Linux裡,檔案的觀念應用的蠻廣泛的,甚至是寫一個driver你也只要提供一組的file operations就可以完成了。我們現在來看看File結構的內容。
struct file {
struct file *f_next,**f_pprev;
struct dentry *f_dentry;
struct file_operations *f_op;
mode_t f_mode;
loff_t f_pos;
unsigned int f_count,f_flags;
unsigned long f_reada,f_ramax,f_raend,f_ralen,f_rawin;
struct fown_struct f_owner;
unsigned long f_version;
void *private_data;
};
比起super_block和inode結構,file結構就顯得小多了,file結構也是用串行來管理,f_next會指到下一個file結構,而f_pprev則是會指到上一個file結構地址的地址,不過,這個字段的用法跟一般指到前一個file結構的用法不太一樣,有機會再跟各位討論,f_dentry會記錄其inode的dentry地址,f_mode為檔案存取的種類,f_pos則是目前檔案的offset,每次讀寫都從offset記錄的位置開始讀寫,f_count是此file結構的reference cout,f_flags則是開啟此檔案的模式,f_reada,f_ramax,f_raend,f_ralen,f_rawin則是控制read ahead的參數,f_owner記錄了要接收SIGIO和SIGURG的行程ID或行程群組ID,private_data則是tty driver所使用的字段。最後,我們來看看f_op這個字段。這個字段記錄了一組的函式是專門用來使用檔案的。
· llseek(file,offset,where)
我們寫程序會呼叫lseek()系統呼叫設定從檔案那個位置開始讀寫,這個函式你可以不用提供,因為系統已經有一個寫好的,但是系統提供的llseek()沒有辦法讓你將where設為SEEK_END,因為系統不知道你的檔案長度是多少,所以沒辦法提供這樣的服務。如果你不提供llseek()的話,那系統會直接使用它已經有的llseek()。llseek()必須要將file->offset的值做改新。
· read(file,buf,buflen,poffset)
當我們讀取一個檔案時,最終就是會呼叫read()這個函式來讀取檔案內容。這些參數VFS會替我們准備好,至於poffset則是offset的指針,這是要告訴read()從那裡開始讀,讀完之後必須更新poffset的內容。請注意,在這裡buf是一個地址,而且是一個位於user space的地址。
· write(file,buf,buflen,poffset)
write()的動作就跟read()是相反的,參數也都一樣,buf依然是位於user space的地址。
· readdir(file,dirent,filldir)
這是用來讀取目錄的下一個direntry的,其中file是file結構地址,dirent則是一個readdir_callback結構,這個結構裡包含了使用者呼叫readdir()系統呼叫時所傳過去的dirent結構地址,filldir則是一個函式指針,這個函式在VFS已經有提供了,這個函式其實是增加了kernel在讀取dirent方面的彈性。當檔案系統的readdir()被呼叫時,在它把下一個dirent取出來之後,應該要呼叫filldir(),讓它把所需的資料寫到user space的dirent結構裡,也許還會多做些處理。有興趣的朋友可以參考的filldir()函式。
· poll(file,poll_table)
之前的Kernel版本本來是在file_operations結構裡有select()函式而不是poll()函式的。但是,這並不代表Linux不提供select()系統呼叫,相反的,Linux仍然提供select()系統呼叫,只不過select()系統呼叫implement的方式是使用poll()函式來做的。
· ioctl(inode,file,cmd,arg)
ioctl()這個函式其實有很大的用途,尤其它可以做為user space的程序對Kernel的一個溝通管道。那ioctl()是什麼時候被呼叫呢? 還記得平常寫程序時偶而會用到ioctl()系統呼叫來直接控制檔案或device嗎? 是的,ioctl()系統呼叫最後就是把命令交給檔案的f_op->ioctl()來執行。f_op->ioctl()要做的事很簡單,只要根據cmd的值,做出適當的行為,並傳回值即可。但是,ioctl()系統呼叫其實是分幾個步驟的,第一,系統有幾個內定的command它自己可以處理,在這種情形下,它不會呼叫f_op->ioctl()來處理。如果user指定的command是以下的一種,那VFS會自己處理。
o FIONCLEX
清除檔案的close-on-exec位。
o FIOCLEX
設定檔案的close-on-exec位。
o FIONBIO
如果arg傳過來的值為0的話,就將檔案的O_NONBLOCK屬性去掉,但是如果不等於0的話,就將O_NONBLOCK屬性設起來。
o FIOASYNC
如果arg傳過來的值為0的話,就將檔案的O_SYNC屬性去掉,但是如果不等於0的話,就將O_SYNC屬性設起來。只是在Kernel 2.2.1時,這個屬性的功能還沒完成。
如果cmd的值不是以上數種,而且如果file所代表的不是普通的檔案的話,像是device之類的特殊檔案,VFS會直接呼叫f_op->ioctl()去處理。但是如果file代表普通檔案的話,那VFS會呼叫file_ioctl()做另外的處理。何謂另外的處理呢? file_ioctl()會再過澽一次cmd的值,如果是以下數種,它會先做些處理,然後再呼叫f_op->ioctl(),不管怎麼樣,file_ioctl()最後都會再呼叫f_op->ioctl()去處理。
o FIBMAP
先將arg指到的檔案block number取出來,並呼叫f_op->bmap()計算出其disk上的block number,最後再將計算出來的block number放到arg參數裡。
o FIGETBSZ
先取得檔案系統block的大小並放入arg的參數裡。
o FIONREAD
將檔案剩下尚未讀取的長度寫到arg裡。比方說檔案大小是1000,而f_op->offset的值是300,表示還有700個byte尚未讀取,所以,將700寫到arg參數裡。
· mmap(file,vmarea)
這個函式是用來將檔案的部分內容映像到內存中的,file是指要被映像的檔案,而vmarea則是用來描述到映像到內存的那裡。
· open(inode,file)
當我們呼叫open()系統呼叫來開啟檔案時,open()會把所有的事都做好,最後則會呼叫f_op->open()看檔案系統是否要做些什麼事,一般來講,VFS已經把事做好了,所以很多系統事實上根本不提供這個函式,當然,你要提供也可以,比方說,你可以在這個函式裡計算這個檔案系統的檔案被使用過多少次等。
· flush(file)
這個函式也是新增加的,這個函式是在我們呼叫close()系統呼叫來關閉檔案時所呼叫的。只要你呼叫close()系統呼叫,那close()就會呼叫flush(),不管那個時候f_count的值是否為0。這個函式我不是很確定在做什麼的,事實上,在Ext2裡也沒有提供這麼一個函式,也許是在關閉檔案之前,VFS允許檔案系統先做些backup的動作吧。
· release(inode,file)
這個函式也是在close()系統呼叫裡使用的,當然,不盡在close()中使用,在別的地方也是有使用到。基本上,這個函式的定位跟open()很像,不過,當我們對一個檔案呼叫close()時,只有當f_count的值歸0時,VFS才會呼叫這個函式做處理。一般來講,如果你在open()時配置了一些東西,那應該在release()時將配置的東西釋放掉。至於f_count的值則是不用在open()和release()中控制,VFS已經在fget()和fput()中增減f_count了。
· fsync(file,dentry)
fsync()這個函式主要是由buffer cache所使用,它是用來跟file這個檔案的資料寫到disk上。事實上,Linux裡有兩個系統呼叫,fsync()和fdatasync(),都是呼叫f_op->fsync()。它們幾乎是一模一樣的,差別在於fsync()呼叫f_op->fsync()之前會使用semaphore將f_op->fsync()設成critical section,而fdatasync()則是直接呼叫f_op->fsync()而不設semaphore。
· fasync(fd,file,on)
當我們呼叫fcntl()系統呼叫,並使用F_SETFL命令來設定檔案的參數時,VFS就會呼叫fasync()這個函式,而當讀寫檔案的動作完成時,行程會收到SIGIO的訊息。
· check_media_change(dev)
這個函式只對可以使用可移動的disk的block device有效而已,像是MO,CDROM,floopy disk等等。為什麼對這些可以把disk隨時抽取的需要提供這麼一個函式呢? 其實,從字面上我們大概可以知道,這是用來檢查disk是否換過了,以CDROM為例,每一個光盤片都代表一個檔案系統,如果今天我們把光盤片換掉了,那表示這個檔案系統不存在了,如果user此時去讀取這個檔案系統的資料,那會發生什麼事? 很有可能系統就這麼出事了。所以,對於這種的device,每當在mount時,我們就必須檢查其中的disk是否換過了,如何檢查呢? 當然只有檔案系統本身才知道,所以,檔案系統必須提供此函式。
· revalidate(dev)
這個函式跟上面的check_media_change()有著相當的關系。當user執行mount要掛上一個檔案系統時,mount會先呼叫裡的check_disk_change(),如果檔案系統所屬的device有提供這個函式的話,那check_disk_change()會先呼叫f_op->check_media_change()來檢查是否其中的disk有換過,如果有則呼叫invalidate_inodes()和invalidate_buffers()將跟原本disk有關的buffer或inode都設為無效,如果檔案系統所屬的device還有提供revalidate()的話,那就再呼叫revalidate()將此device的資料記錄好。
· lock(file,cmd,file_lock)
這個函式也是新增加的,在Linux裡,我們可對一個檔案呼叫fcntl()對它使用lock。如果呼叫fcntl()時,cmd的參數我們給F_GETLK,F_SETLK,或F_SETLKW時,那系統會間接呼叫f_op->lock來做事。當然,如果你的檔案系統不想提供lock的功能的話,你可以不用提供這個函式。
list_head結構的介紹
list_head結構定義在裡,它是一個double linked list的結構。底下是它的結構宣告:
struct list_head {
struct list_head *next, *prev;
};
有的人可能看到這樣的結構會覺得很奇怪這樣的結構可以存放資料嗎? 當然是不行的啰,因為這個結構根本是拿來讓人當資料存的。首先,我們先來看看兩個macro,
#define LIST_HEAD(name) \
struct list_head name = { &name, &name }
#define INIT_LIST_HEAD(ptr) do { \
(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)
這兩個macro在Kernel裡也算蠻常出現的,是用來將list_head做初始化的,它的初始化就是將next和prev這兩個字段設為跟結構的地址相同。所以,如果我們在程序裡看到這樣的程序,它的意思就是宣告一個list_head結構的變量hello,並將prev和next都設成hello的地址。
LIST_HEAD(hello)
因此,如果要檢查這個list是否是空的,只要檢查hello.next是否等於&hello就可以了。事實上,Linux也提供了一個叫list_empty()的函式來檢查list是否為空的。
static __inline__ int list_empty(struct list_head *head)
{
return head->next == head;
}
現在我們來介紹如何加入或刪除list_head到上面的hello串行裡。Linux提供二個函式來做這些事,分別是list_add()和lis_del()。這兩個函式的定義都放在裡,而且其程序代碼也都很簡單,只是單純double linked list的串接和刪除而已,因此我們不對它們做介紹。有關於這個結構,其實最重要的應該是它提供的這個macro。
#define list_entry(ptr, type, member) \
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
圖:http://linuxfab.cx/Columns/17/Image5.gif
我們現在來做個實驗,相信各位會更容易了解這個macro的。請看一下下面這段程序代碼。
struct HelloWorld {
int x, y;
struct list_head list;
} hello;
假設int是4個byte。那麼以下這一行會得到8,如圖5所示
(unsigned long) (&((struct HelloWorld *)0)->list)
有的人會對這一行程序感到奇怪,(struct HelloWorld*)0不就是一個NULL的指針嗎? 怎麼可以用0->list去參考list這個字段呢? 難道不怕造成segmentation fault嗎? 請注意一下,我們在0->list的前面還加上了一個&。如果沒有&,那上面這一行就會segmentation fault了。如果你加上了&,那就沒問題啰。Segmentation fault通常是去參考到不合法的內存地址內容所造成的,如果我們加上了&就表示我們沒有要去參考這個不合法地址的內容,我們只是要那個字段的地址而已,因此,不會造成segmentation fault。其實,結構的配置在內存裡是連續的。所以,如果我們去讀取某個字段時,像&hello->list。會先取得hello變量的地址,再然後再計算HelloWorld結構裡list字段所在的offset,再將hello的地址加上list字段的offset,求得list字段真正的地址。然後再去讀list字段的內容。這是compiler幫我們做的。那我們現在就來看看上面那一行究竟是什麼意思。首先,我們先把上面那一行想象成下面這個樣子。
ptr = 0;
(unsigned long) (&((struct HelloWorld *)ptr)->list)
這樣是不是容易懂了嗎,就是要取得&ptr->list的地址而已。所以,如果ptr是100的話,那會得到100+8=108。因為前面有二個int,每一個int是4個byte。經過轉型,就得到了(unsigned long)型態的108。如果ptr是0的話,那同理,我們會得到0+8=8。也就是這個字段在HelloWorld結構裡的offset。
現在,如果我們已經知道了list在HelloWorld結構中的offset,而且我們現在也知道hello這個變量裡list的地址的話,那有沒有辦法得到hello本身的地址呢? 可以的,就像圖6一樣,如果我們知道list的地址,只要將list的地址減8就可以知道了hello的地址了嘛。
struct list_head *plist = &hello.list;
printf( "&hello = %x\n", (char*)plist - (unsigned long) 8 ));
而這種方式就是list_head的用法,它是專門用來當作別的結構的字段,只要我們得到這個字段的位置和包含這個字段的結構是那一種,我們可以很輕易的算出包含此字段的結構地址,圖6就是super block在使用list_head所得到的結果。只要我們知道s_list的地址,只要呼叫
list_entry( &sb1.s_list, struct super_block, s_list)
就可以得到其sb1這個super_block結構的地址。
圖:http://linuxfab.cx/Columns/17/Image6.gif
結語
有關於VFS的大概就先跟各位介紹到這裡。其實,VFS裡還包含了很多東西,像是Super block的管理,inode管理,Quota的控制,Dcache與Buffer Cache的運作等等。如果有機會,我再跟各位做這方面的介紹。Linux的好處在於將原始碼公開,我們可以盡情在其中觀察別人程序的寫法。有時候,看習慣了,可能還會認為直接看原始碼比看別人寫出來的文章容易哩。
參考資料:
Linux Kernel Internals 2ed
The Linux Kernel Book
Kernel Hacker's Guide
摘自:http://linuxfab.cx