8.3.2  索引节点高速缓存

VFS也用了一个高速缓存来加快对索引节点的访问,和块高速缓存不同的一点是每个缓冲区不用再分为两个部分了,因为inode结构中已经有了类似于块高速缓存中缓冲区首部的域。索引节点高速缓存的实现代码全部在fs/inode.c,这部分代码并没有随着内核版本的变化做很多的修改。

1.索引节点链表

每个索引节点可能处于哈希表中,也可能同时处于下列“类型”链表的一种中:

·       "in_use" – 有效的索引节点,即 i_count > 0i_nlink > 0(参看前面的inode结构)

·      "dirty"  - 类似于 "in_use" ,但还“脏”

·      "unused" – 有效的索引节点但还没使用,即 i_count = 0

这几个链表定义如下:

static LIST_HEAD(inode_in_use);

static LIST_HEAD(inode_unused);

static struct list_head *inode_hashtable;

    static LIST_HEAD(anon_hash_chain); /* for inodes with NULL i_sb */

 

  因此,索引节点高速缓存的结构概述如下:

·      全局哈希表inode_hashtable,其中哈希值是根据每个超级块指针的值和32位索引节点号而得。对没有超级块的索引节点(inode->i_sb == NULL),则将其加入到anon_hash_chain链表的首部。例如,net/socket.csock_alloc()函数, 通过调用fs/inode.cget_empty_inode()创建的套接字是一个匿名索引节点,这个节点就加入到了anon_hash_chain链表。

·      正在使用的索引节点链表。全局变量inode_in_use指向该链表中的首元素和尾元素。函数get_empty_inode()获得一个空节点,get_new_inode()获得一个新节点,通过这两个函数新分配的索引节点就加入到这个链表中。

·      未用索引节点链表。全局变量inode_unusednext域 和prev域分别指向该链表中的首元素和尾元素

·      脏索引节点链表。由相应超级块的s_dirty域指向该链表中的首元素和尾元素

·      inode对象的缓存,定义如下:

static kmem_cache_t * inode_cachep

这是一个Slab缓存,用于分配和释放索引节点对象。

 

  索引节点的i_hash域指向哈希表,i_list指向in_useunused dirty某个链表。所有这些链表都受单个自旋锁inode_lock的保护,其定义如下:

/*

* A simple spinlock to protect the list manipulations.

*

* NOTE! You also have to own the lock if you change

* the i_state of an inode while it is in use..

*/

static spinlock_t inode_lock = SPIN_LOCK_UNLOCKED;

 

   索引节点高速缓存的初始化是由inode_init()实现的,而这个函数是在系统启动时由init/main.c中的start_kernel()函数调用的。inode_init()只有一个参数,表示索引节点高速缓存所使用的物理页面数。因此,索引节点高速缓存可以根据可用物理内存的大小来进行配置,例如,如果物理内存足够大的话,就可以创建一个大的哈希表。

   索引节点状态的信息存放在数据结构inodes_stat_t中,在fs/fs.h中定义如下:

   struct inodes_stat_t {

        int nr_inodes;

        int nr_unused;

        int dummy[5];

    };

   extern struct inodes_stat_t inodes_stat

    用户程序可以通过/proc/sys/fs/inode-nr /proc/sys/fs/inode-state获得索引节点高速缓存中索引节点总数及未用索引节点数。

 2.索引节点高速缓存的工作过程

    为了帮助大家理解索引节点高速缓存如何工作,我们来跟踪一下在打开Ext2文件系统的一个常规文件时,相应索引节点的作用。

 

fd = open("file", O_RDONLY);

close(fd);

 

   open()系统调用是由fs/open.c中的sys_open函数实现的,而真正的工作是由fs/open.c中的filp_open()函数完成的,filp_open()函数如下:

    struct file *filp_open(const char * filename, int flags, int mode)

{

        int namei_flags, error;

         struct nameidata nd;

 

        namei_flags = flags;

         if ((namei_flags+1) & O_ACCMODE)

                 namei_flags++;

         if (namei_flags & O_TRUNC)

                namei_flags |= 2;

 

        error = open_namei(filename, namei_flags, mode, &nd);

         if (!error)

                 return dentry_open(nd.dentry, nd.mnt, flags);

 

         return ERR_PTR(error);

}

其中nameidata结构在fs.h中定义如下:

struct nameidata {

         struct dentry *dentry;

         struct vfsmount *mnt;

         struct qstr last;

         unsigned int flags;

        int last_type;

};

   这个数据结构是临时性的,其中,我们主要关注dentrymnt域。Dentry结构我们已经在前面介绍过,而vfsmount结构记录着所属文件系统的安装信息,例如文件系统的安装点、文件系统的根节点等。

   filp_open()主要调用以下两个函数

(1)     open_namei():填充目标文件所在目录的dentry结构 和 所在文件系统的vfsmount结构。在dentry结构中dentry->d_inode就指向目标文件的索引节点。这个函数比较复杂和庞大,在此为了突出主题,后面我们只介绍与主题相关的内容。

(2)     dentry_open():建立目标文件的一个“上下文”,即file数据结构,并让它与当前进程的task_strrct结构挂上钩。同时,在这个函数中,调用了具体文件系统的打开函数,即f_op->open()。该函数返回指向新建立的file结构的指针。

 

     open_namei()函数通过path_walk()与目录项高速缓存(即目录项哈希表)打交道,而path_walk()又调用具体文件系统的inode_operations->lookup()方法;该方法从磁盘找到并读入当前节点的目录项,然后通过iget(sb, ino),根据索引节点号从磁盘读入相应索引节点并在内存建立起相应的inode结构,这就到了我们讨论的索引节点高速缓存。

当索引节点读入内存后,通过调用d_add(dentry, inode),就将dentry结构和inode结构之间的链接关系建立起来。两个数据结构之间的联系是双向的。一方面,dentry结构中的指针d_inode指向inode结构,这是一对一的关系,因为一个目录项只对应着一个文件。反之则不然,同一个文件可以有多个不同的文件名或路径(通过系统调用link()建立,注意与符号连接的区别,那是由symlink()建立的),所以从inode结构到dentry结构的方向是一对多的关系。因此, inode结构的i_ dentry是个队列,dentry结构通过其队列头部d_alias挂入相应inode结构的队列中。

  

    为了进一步说明索引节点高速缓存,我们来进一步考察iget()。当我们打开一个文件时,就调用了iget()函数,而iget真正调用的是iget4(sb, ino, NULL, NULL)函数,该函数代码如下:

     struct inode *iget4(struct super_block *sb, unsigned long ino, find_inode_t find_actor, void *opaque)

{

        struct list_head * head = inode_hashtable + hash(sb,ino);

         struct inode * inode;

 

         spin_lock(&inode_lock);

         inode = find_inode(sb, ino, head, find_actor, opaque);

         if (inode) {

                __iget(inode);

                 spin_unlock(&inode_lock);

                wait_on_inode(inode);

                return inode;

         }

         spin_unlock(&inode_lock);

 

         /*

          * get_new_inode() will do the right thing, re-trying the search

          * in case it had to block at any point.

         */

         return get_new_inode(sb, ino, head, find_actor, opaque);

  }

 下面对以上代码给出进一步的解释:

·      inode结构中有个哈希表inode_hashtable,首先在inode_lock锁的保护下,通过find_ inode函数在哈希表中查找目标节点的inode结构,由于索引节点号只有在同一设备上时才是唯一的,因此,在哈希计算时要把索引节点所在设备的super_block结构的地址也结合进去。如果在哈希表中找到该节点,则其引用计数(i_count)加1;如果i_count在增加之前为0,说明该节点不“脏”,则该节点当前肯定处于inode_unused list队列中,于是,就把该节点从这个队列删除而插入inode_in_use队列;最后,把inodes_stat.nr_unused1

·       如果该节点当前被加锁,则必须等待,直到解锁,以便确保iget4()返回一个未加锁的节点。

·      如果在哈希表中没有找到该节点,说明目标节点的inode结构还不在内存,因此,调用get_new_inode()从磁盘上读入相应的索引节点并建立起一个inode结构,并把该结构插入到哈希表中。

·      get_new_inode()给出进一步的说明,该函数从Slab缓存区中分配一个新的inode结构,但是这个分配操作有可能出现阻塞,于是,就应当解除保护哈希表的inode_lock自旋锁,以便在哈希表中再次进行搜索。如果这次在哈希表中找到这个索引节点,就通过__iget把该节点的引用计数加1,并撤销新分配的节点。如果在哈希表中还没有找到,就使用新分配的索引节点;因此,把该索引节点的一些域先初始化为必须的值,然后调用具体文件系统的 sb->s_op->read_inode()域填充该节点的其他域。这就把我们从索引节点高速缓存带到了某个具体文件系统的代码中。当s_op->read_inode()方法正在从磁盘读索引节点时,该节点被加锁(i_state = I_LOCK);当read_inode()返回时,该节点的锁被解除,并且唤醒所有等待者