当前位置:Linux教程 - Linux - 虚拟文件系统 (VFS) 简介

虚拟文件系统 (VFS) 简介

虚拟文件系统 (VFS) 简介

前言

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 "", (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