在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結構的地址。