随笔-118  评论-133  文章-4  trackbacks-0
注:
    写得实在太好了!
    原文出处:http://blog.csdn.net/rstevens/archive/2007/10/14/1824785.aspx

1.   概述

 


根据以前学习内核源码的经验,在学习文件系统实现之前,我大概定了个目标:

1、  建立一个清晰的全局概念。为将来需要研究代码细节打下坚实基础。

2、  只研究虚拟文件系统 VFS 的实现,不研究具体文件系统。

为什么选择 Linux 2.4.30?因为可以参考《Linux 源码情景分析》一书,减少学习难度。

 

1.1.              基本概念

1、  一块磁盘(块设备),首先要按照某种文件系统(如 NTFS)格式进行格式化,然后才能在其上进行创建目录、保存文件等操作。

Linux 中,有“安装”文件系统和“卸载”文件系统的概念。

一块经过格式化的“块设备”(不管是刚刚格式化完的,没有创建任何名录和文件;还是已经创建了目录和文件),只有先被“安装”,才能融入 Linux 的文件系统中,用户才可以在它上面进行正常的文件操作。

2、  Linux 把目录或普通文件,统一看成“目录节点”。通常一个“目录节点”具有两个重要属性:名称以及磁盘上实际对应的数据。本文中,“目录节点”有时简称为“节点”

“符号链接”是一种特殊的目录节点,它只有一个名称,没有实际数据。这个名称指向一个实际的目录节点。

3、  “接口结构”:在 内核代码中,经常可以看到一种结构,其成员全部是函数指针,例如:

struct file_operations {

  struct module *owner;

  loff_t (*llseek) (struct file *, loff_t, int);

  ssize_t (*read) (struct file *, char *, size_t, loff_t *);

  ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

  int (*readdir) (struct file *, void *, filldir_t);

  unsigned int (*poll) (struct file *, struct poll_table_struct *);

  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

  int (*mmap) (struct file *, struct vm_area_struct *);

  int (*open) (struct inode *, struct file *);

  int (*flush) (struct file *);

  int (*release) (struct inode *, struct file *);

  int (*fsync) (struct file *, struct dentry *, int datasync);

  int (*fasync) (int, struct file *, int);

  int (*lock) (struct file *, int, struct file_lock *);

  ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

  ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

  ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

  unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

};

这种结构的作用类似与 C++ 中的“接口类”,它是用 C 语言进行软件抽象设计时最重要的工具。通过它,将一组通用的操作抽象出来,核心的代码只针对这种“接口结构”进行操作,而这些函数的具体实现由不同的“子类”去完成。

以这个 file_operations“接口”为例,它是“目录节点”提供的操作接口。不同的文件系统需要提供这些函数的具体实现。

本文中,“接口结构”有时简称“接口”。

 

1.2.              虚拟文件系统

Linux 通过虚拟文件系统 VFS 来支持不同的具体的文件系统,那么 VFS 到底是什么?

从程序员的角度看,我认为 VFS 就是一套代码框架(framework,它将用户与具体的文件系统隔离开来,使得用户能够通过这套框架,以统一的接口在不同的具体的文件系统上进行操作。

这套框架包括:

1、  为用户提供统一的文件和目录的操作接口,如  open, read, write

2、  抽象出文件系统共有的一些结构,包括“目录节点”inode、“超级块”super_block 等。

3、  面向具体的文件系统,定义一系列统一的操作“接口”, file_operations, inode_operations, dentry_operation,具体的文件系统必须提供它们的实现。

4、  提供一套机制,让具体的文件系统融入 VFS 框架中,包括文件系统的“注册”和“安装”

5、  实现这套框架逻辑的核心代码 


我对文件系统的学习,实际上就是学习虚拟文件系统这套框架是如何实现的。

 

2.   核心数据结构

数据结构是代码的灵魂,要分析一个复杂的系统,关键是掌握那些核心的数据结构,这包括:

1、  弄清数据结构的核心功能。一个数据结构通常具有比较复杂的成员,此外,还有一些成员用于建立数据结构之间的关系。如果要一个个去理解,就会陷入细节。

2、  弄清数据结构之间的静态关系

3、  弄清数据结构之间是如何建立起动态的关系的

本文重点分析文件系统中的关键数据结构以及它们之间的关系。

 

2.1.              inode file_operations

1、  inode 用以描述“目录节点” ,它描述了一个目录节点物理上的属性,例如大小,创建时间,修改时间、uidgid

2、  file_operations 是“目录节点”提供的操作“接口”。它包括 open, read, wirte, ioctl, llseek, mmap 等操作。

3、  一个  inode 通过成员 i_fop 对应一个 file_operations

4、  打开文件的过程就是寻找 “目录节点”对应的 inode 的过程

5、  文件被打开后,inode file_operation 都已经在内存中建立,file_operations 的指针也已经指向了具体文件系统提供的函数,此后都文件的操作,都由这些函数来完成。

 
例如打开了一个普通文件 /root/file,其所在文件系统格式是 ext2,那么,内存中结构如下:

 

 

2.2.              目录节点入口dentry

本来,inode 中应该包括“目录节点”的名称,但由于符号链接的存在,导致一个物理文件可能有多个文件名,因此把和“目录节点”名称相关的部分从 inode 中分开,放在一个专门的 dentry 结构中。这样:

1、  一个dentry 通过成员 d_inode 对应到一个 inode上,寻找 inode 的过程变成了寻找 dentry 的过程。因此,dentry 变得更加关键,inode 常常被 dentry 所遮掩。可以说, dentry 是文件系统中最核心的数据结构,它的身影无处不在。

2、  由于符号链接的存在,导致多个 dentry 可能对应到同一个 inode

 
例如,有一个符号链接 /tmp/abc 指向一个普通文件 /root/file,那么 dentry inode 之间的关系大致如下:

 

 

2.3.                    super_block super_operations

 
一个存放在磁盘上的文件系统如 EXT2 等,在它的格式中通常包括一个“超级块”或者“控制块”的部分,用于从整体上描述文件系统,例如文件系统的大小、是否可读可写等等。

虚拟文件系统中也通过“超级块”这种概念来描述文件系统整体的信息,对应的结构是 struct super_block

super_block 除了要记录文件大小、访问权限等信息外,更重要的是提供一个操作“接口”super_operations

 

struct super_operations {

           
struct inode *(*alloc_inode)(struct super_block *sb);
           
void (*destroy_inode)(struct inode *);
           
void (*read_inode) (struct inode *);
           
void (*read_inode2) (struct inode *void *) ;
           
void (*dirty_inode) (struct inode *);
           
void (*write_inode) (struct inode *int);
           
void (*put_inode) (struct inode *);
           
void (*delete_inode) (struct inode *);
           
void (*put_super) (struct super_block *);
           
void (*write_super) (struct super_block *);
           
int (*sync_fs) (struct super_block *);
           
void (*write_super_lockfs) (struct super_block *);
           
void (*unlockfs) (struct super_block *);
           
int (*statfs) (struct super_block *struct statfs *);
           
int (*remount_fs) (struct super_block *int *char *);
           
void (*clear_inode) (struct inode *);
           
void (*umount_begin) (struct super_block *);
           
struct dentry * (*fh_to_dentry)(struct super_block *sb, __u32 *fh, int len, int fhtype, int parent);
           
int (*dentry_to_fh)(struct dentry *, __u32 *fh, int *lenp, int need_parent);
           
int (*show_options)(struct seq_file *struct vfsmount *);
};

我们通过分析“获取一个 inode ”的过程来只理解这个“接口”中两个成员  alloc_inode  read_inode 的作用。

在文件系统的操作中,经常需要获得一个“目录节点”对应的 inode,这个 inode 有可能已经存在于内存中了,也可能还没有,需要创建一个新的 inode,并从磁盘上读取相应的信息来填充。

对应的代码是 iget()   inlcude/linux/fs.h)过程如下:

1、  通过 iget4_locked() 获取 inode。如果 inode 在内存中已经存在,则直接返回;否则创建一个新的 inode

2、  如果是新创建的 inode,通过 super_block->s_op->read_inode() 来填充它。也就是说,如何填充一个新创建的 inode 是由具体文件系统提供的函数实现的。

 

 

 

iget4_locked()  首先在全局的 inode hash table 中寻找,如果找不到,则调用 get_new_inode() ,进而调用 alloc_inode() 来创建一个新的 inode

alloc_inode() 中可以看到,如果具体文件系统提供了创建 inode 的方法,则由具体文件系统来负责创建,否则采用系统默认的的创建方法。

 

static struct inode *alloc_inode(struct super_block *sb)
{

            
static struct address_space_operations empty_aops;
           
static struct inode_operations empty_iops;
           
static struct file_operations empty_fops;
           
struct inode *inode;

            
if (sb->s_op->alloc_inode)
                        inode 
= sb->s_op->alloc_inode(sb);
           
else {
                        inode 
= (struct inode *) kmem_cache_alloc(inode_cachep, SLAB_KERNEL);
 
                        if (inode)
                                    memset(
&inode->u, 0sizeof(inode->u));
            }

            
if (inode) {
                       
struct address_space * const mapping = &inode->i_data;
                        inode
->i_sb = sb;
                        inode
->i_dev = sb->s_dev;
                        inode
->i_blkbits = sb->s_blocksize_bits;
                        inode
->i_flags = 0;
                        atomic_set(
&inode->i_count, 1);
                        inode
->i_sock = 0;
                        inode
->i_op = &empty_iops;
                        inode
->i_fop = &empty_fops;
                        inode
->i_nlink = 1;
                        atomic_set(
&inode->i_writecount, 0);
                        inode
->i_size = 0;
                        inode
->i_blocks = 0;
                        inode
->i_bytes = 0;
                        inode
->i_generation = 0;
                        memset(
&inode->i_dquot, 0sizeof(inode->i_dquot));
                        inode
->i_pipe = NULL;
                        inode
->i_bdev = NULL;
                        inode
->i_cdev = NULL;

                        mapping
->a_ops = &empty_aops;
                        mapping
->host = inode;
                        mapping
->gfp_mask = GFP_HIGHUSER;
                        inode
->i_mapping = mapping;
            }
           
return inode;
}

super_block 是在安装文件系统的时候创建的,后面会看到它和其它结构之间的关系。

 

3.   安装文件系统

1、  一个经过格式化的块设备,只有安装后,才能融入 Linux VFS 之中。

2、  安装一个文件系统,必须指定一个目录作为安装点。

3、  一个设备可以同时被安装到多个目录上。

4、  如果某个目录下原来有一些文件和子目录,一旦将一个设备安装到目录下后,则原有的文件和子目录消失。因为这个目录已经变成了一个安装点。

5、  一个目录节点下可以同时安装多个设备。

 

3.1.              “根安装点”、“根设备”和“根文件系统”

安装一个文件系统,除了需要“被安装设备”外,还要指定一个“安装点”。“安装点”是已经存在的一个目录节点。例如把 /dev/sda1 安装到 /mnt/win 下,那么 /mnt/win 就是“安装点”。

可是文件系统要先安装后使用。因此,要使用 /mnt/win 这个“安装点”,必然要求它所在文件系统已也经被安装。

也就是说,安装一个文件系统,需要另外一个文件系统已经被安装。

这是一个鸡生蛋,蛋生鸡的问题:最顶层的文件系统是如何被安装的?

答案是,最顶层文件系统的时候是被安装在“根安装点”上的,而根安装点不属于任何文件系统,它对应的 dentry inode 是由内核在初始化阶段凭空构造出来的。

最顶层的文件系统叫做“根文件系统”。Linux 在启动的时候,要求用户必须指定一个“根设备”,内核在初始化阶段,将“根设备”安装到“根安装点”上,从而有了根文件系统。这样,文件系统才算准备就绪。此后,用户就可以通过 mount 命令来安装新的设备。

 

3.2.              安装连接件 vfsmount

“安装”一个文件系统涉及“被安装设备”和“安装点”两个部分,安装的过程就是把“安装点”和“被安装设备”关联起来,这是通过一个“安装连接件”结构 vfsmount 来完成的。

vfsmount  将“安装点”dentry 和“被安装设备”的根目录节点 dentry 关联起来。

每安装一次文件系统,会导致:

1、  创建一个 vfsmount

2、  为“被安装设备”创建一个 super_block,并由具体的文件系统来设置这个 super_block。(我们在“注册文件系统”一节将再来分析这一步)

3、  为被安装设备的根目录节点创建 dentry

4、  为被安装设备的根目录节点创建 inode 并由 super_operations->read_inode() 来设置此 inode

5、  super_block 与“被安装设备“根目录节点 dentry 关联起来

6、  vfsmount 与“被安装设备”的根目录节点 dentry 关联起来

在内核将根设备安装到“根安装点”上后,内存中有如下结构关系:



 现在假设我们在 /mnt/win 下安装了 /dev/sda1 /dev/sda1 下有 dir1,然后又在 dir1 下安装了 /dev/sda2,那么内存中就有了如下的结构关系

 

 




4.   注册文件系统

前面说了,在安装一个文件系统的时候,需要为“被安装设备”创建一个 super_block,并设置它。

如果从源码追寻这个创建和设置 super_block 的过程,就引出了“注册文件系统”的概念。

实际上,在安装一个文件系统之前,还需要有一个注册文件系统的步骤,否则内核就因为不认识该文件系统而无法完成安装。

通过register_filesystem() ,将一个“文件系统类型”结构 file_system_type注册到内核中一个全局的链表file_systems 上。

 

struct file_system_type {

            
const char *name;
           
int fs_flags;
           
struct super_block *(*read_super) (struct super_block *void *int);
           
struct module *owner;
           
struct file_system_type * next;
           
struct list_head fs_supers;
};
 
int register_filesystem(struct file_system_type * fs)
{
           
int res = 0;
           
struct file_system_type ** p;
 
           
if (!fs)
                       
return -EINVAL;

            
if (fs->next)
                       
return -EBUSY;

            INIT_LIST_HEAD(
&fs->fs_supers);
            write_lock(
&file_systems_lock);
            p 
= find_filesystem(fs->name);

            
if (*p)
                        res 
= -EBUSY;
           
else
                       
*= fs;

            write_unlock(
&file_systems_lock);
           
return res;
}


这个结构中最关键的就是 read_super() 这个函数指针,它就是用于创建并设置 super_block 的目的的。

因为安装一个文件系统的关键一步就是要为“被安装设备”创建和设置一个 super_block,而不同的具体的文件系统的 super_block 有自己特定的信息,因此要求具体的文件系统首先向内核注册,并提供 read_super() 的实现。

5.   根据路径名寻找目标节点的 dentry

下面来研究文件系统中的一个非常关键的操作:根据路径名寻找目标节点的 dentry

例如要打开 /mnt/win/dir1/abc 这个文件,就是根据这个路径,找到目标节点 ‘abc’ 对应的 dentry ,进而得到 inode 的过程。

5.1.              寻找过程

寻找过程大致如下:

1、  首先找到根文件系统的根目录节点 dentry inode

2、  由这个 inode 提供的操作接口 i_op->lookup(),找到下一层节点 ‘mnt’ dentry inode

3、  ‘mnt’ inode 找到 ‘win’ dentry inode

4、  由于 ‘win’ 是个“安装点”,因此需要找到“被安装设备”/dev/sda1 根目录节点的 dentry inode,只要找到 vfsmount B,就可以完成这个任务。

5、  然后由 /dev/sda1 根目录节点的 inode 负责找到下一层节点 ‘dir1’ dentry inode

6、  由于 dir1 是个“安装点”,因此需要借助 vfsmount C 找到 /dev/sda2 的根目录节点 dentry inode

7、  最后由这个 inode 负责找到 ‘abc’ dentry inode

可以看到,整个寻找过程是一个递归的过程。

完成寻找后,内存中结构如下,其中红色线条是寻找目标节点的路径


 

现在有两个问题:

1、在寻找过程的第一步,如何得到“根文件系统”的根目录节点的 dentry

答案是这个 dentry 是被保存在进程的 task_struct 中的。后面分析进程与文件系统关系的时候再说这个。

2、如何寻找 vfsmount B C

这是接下来要分析的。

 

5.2.              vfsmount 之间的关系

我们知道, vfsmount ABC 之间形成了一种父子关系,为什么不根据 A 来找到 B ,根据 B 找到 C 了?

这是因为一个文件系统可能同时被安装到不同的“安装点”上。

假设把 /dev/sda1 同时安装到 /mnt/win /mnt/linux

现在 /mnt/win/dir1 /mnt/linux/dir1 对应的是同一个 dentry!!!

然后,又把 /dev/sda2 分别安装到 /mnt/win/dir1 /mnt/linux/dir1

 

现在, vfsmount dentry 之间的关系大致如下。可以看到:

1、  现在有四个 vfsmount A, B, C, D

2、  A B对应着不同的安装点 ‘win’ ‘linux’,但是都指向 /dev/sda1 根目录的 dentry

3、  C D 对应着这相同的安装点 ‘dir1’,也都指向 /dev/sda2 根目录的 dentry

4、  C A child, A C parent

5、  D B child, B D parent

 

5.3.              搜索辅助结构 nameidata

在递归寻找目标节点的过程中,需要借助一个搜索辅助结构 nameidata,这是一个临时结构,仅仅用在寻找目标节点的过程中。

 

 

在搜索初始化时,创建 nameidata,其中 mnt 指向 current->fs->rootmntdentry 指向 current->fs->root

dentry 随着目录节点的深入而不断变化;

mnt 则在每进入一个新的文件系统后发生变化

以寻找 /mnt/win/dir1/abc 为例

开始的时候, mnt 指向 vfsmount Adentry 指向根设备的根目录

随后,dentry  先后指向 ‘mnt’ ‘win’ 对应的 dentry

然后当寻找到 vfsmount B 后,mnt 指向了它,而 dentry 则指向了 /dev/sda1 根目录的 dentry

有了这个结构,上一节的问题就可以得到解决了:

在寻找 /mnt/win/dir1/abc 的过程中,首先找到 A,接下来在要决定选 C 还是 D,因为是从 A 搜索下来的, C A child,因此选择 C 而不是 D;同样,如果是寻找 /mnt/linux/dir1/abc,则会依次选择 B D。这就是为什么 nameidata 中要带着一个 vfsmount 的原因。

 

6.   打开文件

 

6.1.              “打开文件”结构 file

 一个文件每被打开一次,就对应着一个 file 结构。

我们知道,每个文件对应着一个 dentry inode,每打开一个文件,只要找到对应的 dentry inode 不就可以了么?为什么还要引入这个 file 结构?

 这是因为一个文件可以被同时打开多次,每次打开的方式也可以不一样。

dentry inode 只能描述一个物理的文件,无法描述“打开”这个概念。

因此有必要引入 file 结构,来描述一个“被打开的文件”。每打开一个文件,就创建一个 file 结构。

 

file 结构中包含以下信息:

打开这个文件的进程的 uid,pid

打开的方式

读写的方式

当前在文件中的位置

实际上,打开文件的过程正是建立file, dentry, inode 之间的关联的过程。

 


7.   文件的读写

文件一旦被打开,数据结构之间的关系已经建立,后面对文件的读写以及其它操作都变得很简单。就是根据 fd 找到 file 结构,然后找到 dentry inode,最后通过 inode->i_fop 中对应的函数进行具体的读写等操作即可。

 

8.   进程与文件系统的关联

8.1.              “打开文件”表和 files_struct结构

一个进程可以打开多个文件,每打开一个文件,创建一个 file 结构。所有的 file 结构的指针保存在一个数组中。而文件描述符正是这个数组的下标。

我记得以前刚开始学习编程的时候,怎么都无法理解这个“文件描述符”的概念。现在从内核的角度去看,就很容易明白“文件描述符”是怎么回事了。用户仅仅看到一个“整数”,实际底层对应着的是 file, dentry, inode 等复杂的数据结构。

files_struct 用于管理这个“打开文件”表。

 

struct files_struct {
    atomic_t count;
    rwlock_t file_lock;    
/* Protects all the below members.  Nests inside tsk->alloc_lock */
    
int max_fds;
    
int max_fdset;
    
int next_fd;
    
struct file ** fd;    /* current fd array */
    fd_set 
*close_on_exec;
    fd_set 
*open_fds;
    fd_set close_on_exec_init;
    fd_set open_fds_init;
    
struct file * fd_array[NR_OPEN_DEFAULT];
};

 

其中的 fd_arrar[] 就是“打开文件”表。

task_struct 中通过成员 files 与 files_struct 关联起来。

8.2.              struct  fs_struct

task_struct 中与文件系统相关的还有另外一个成员 fs,它指向一个 fs_struct 。

struct fs_struct {

       atomic_t count;
       rwlock_t 
lock;
       
int umask;
       
struct dentry * root, * pwd, * altroot;
       
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};


其中:

root 指向此进程的“根目录”,通常就是“根文件系统”的根目录 dentry

pwd 指向此进程当前所在目录的 dentry

因此,通过 task_struct->fs->root,就可以找到“根文件系统”的根目录 dentry,这就回答了 5.1 小节的第一个问题。

rootmnt :指向“安装”根文件系统时创建的那个 vfsmount

pwdmnt:指向“安装”当前工作目录所在文件系统时创建的那个 vfsmount

这两个域用于初始化 nameidata 结构。


8.3.              进程与文件系统的结构关系图

 下图描述了进程与文件系统之间的结构关系图:

 


 

9.   参考资料

1、《Linux 源码情景分析》上册

2Linux 2.4.30 源码

 
struct nameidata {

       
struct dentry *dentry;
       
struct vfsmount *mnt;
       
struct qstr last;
       unsigned 
int flags;
       
int last_type;
};
static inline struct inode *iget(struct super_block *sb, unsigned long ino)
{
            
struct inode *inode = iget4_locked(sb, ino, NULL, NULL);


            
if (inode && (inode->i_state & I_NEW)) {
                        sb
->s_op->read_inode(inode);
                        unlock_new_inode(inode);
            }
            
return inode;
}
posted on 2008-01-09 15:49 lfc 阅读(1774) 评论(0)  编辑 收藏 引用
只有注册用户登录后才能发表评论。