第2章 文件系统

本章从文件系统开始探索整个内核应用层。在本书的整个体系中,文件系统担当着最重要的作用,可以认为,Linux内核的应用层就是以文件系统为核心而展开的。

本书以文件系统作为整个内核应用层的核心,主要基于下列理由。

  • 文件系统本身具有重大作用。国内一些技术公司,已经开始对文件系统的深度研究和应用。这其中,既有通信公司,也有传统IT公司和互联网公司。当前,分布式文件系统的广泛应用让文件系统成为当前内核应用的热门。
  • 文件系统在整个内核架构中具有基础架构性质。字符设备、块设备这些设备驱动的概念都要依靠文件系统来实现。设备管理的基础架构也要依赖文件系统(sysfs)。而设备和驱动是国内当前在内核层面应用最多的方面,也是国内程序员在底层开发中应用最多的方面。

前言讲到,书籍的作用只是带领读者入门,带领入门的关键是提供一条学习分析的快捷路径。根据笔者的经验,从架构层面分析内核,难点就是如何划分代码的层次。如何找到一个核心点,然后从核心点扩展,每一部分都自成单元,既具有普遍性,又能向外扩展,是本文最大一个挑战。如果没有建立层次分明的架构体系,内核代码就会显得庞大混乱,难以梳理和学习。从文件系统入手,掌握基本概念和实现架构后,可以从文件系统引出设备文件的概念,设备文件又可以引申到字符设备和块设备,这样就从文件系统过渡到设备管理。设备管理包含了设备驱动,设备驱动要用到中断,设备里面的块设备又控制了通用块层和I/O调度。而文件系统向外引申又和网络的socket联系。深入文件系统的代码,可以了解到内存的页面管理。从文件系统出发,层次推进基本囊括了内核应用层的重要概念和架构。这是作者总结的一种学习体系,希望通过这种体系串联起内核的知识点。

2.1 文件系统的基本概念

在深入分析文件系统之前,有必要介绍文件系统的几个基本概念,这将从架构层次理解文件系统设计的目的,从而从全局层面理解内核文件系统的代码,大大减低分析代码的难度和工作量。

2.1.1 什么是VFS

Linux内核通过虚拟文件系统(Virtual File System,VFS)管理文件系统。

VFS是Linux内核文件系统的一个极其重要的基础设施,VFS为所有的文件系统提供了统一的接口,对每个具体文件系统的访问要通过VFS定义的接口来实现。同时VFS也是一个极其重要的架构,所有Linux的文件系统必须按照VFS定义的方式来实现。

VFS本身只存在于内存中,它需要将硬盘上的文件系统抽象到内存中,这个工作是通过几个重要的结构实现的。VFS定义了几个重要的结构:dentryinodesuper_block,通过这些结构将一个真实的硬盘文件系统抽象到了内存,从而通过管理dentry、inode这几个对象就可以完成对文件系统的一些操作(当然,在合适的时候,仍然需要将内存的数据写入到硬盘)。

2.1.2 超级块super_block

超级块(super_block)代表了整个文件系统本身。通常,超级块是对应文件系统自身的控制块结构(可参考ext2文件系统的超级块结构)。超级块保存了文件系统设定的文件块大小,超级块的操作函数,而文件系统内所有的inode也都要链接到超级块的链表头。对于一个具体文件系统的控制块可能还含有另外的信息,而通过超级块对象,我们可以找到这些必要的信息。

超级块的内容需要读取具体文件系统在硬盘上的超级块结构获得,所以超级块是具体文件系统超级块的内存抽象。超级块对象整个结构很庞大复杂,作为学习的起步阶段,没必要探究每个成员的具体意义和使用目的(实际上,强记也很容易忘记)。代码清单2-1是超级块对象简化后的结构定义。

代码清单2-1 超级块结构简化定义

struct super_block {
        unsigned long        s_blocksize;
        unsigned char        s_blocksize_bits;
        ....../*省略超级块的链表、设备号等代码*/
        unsigned long long        s_maxbytes;        /* Max file size */
        struct file_system_type        *s_type;
        struct super_operations        *s_op;
        unsigned long        s_magic;
        struct dentry        *s_root;
        struct list_head        s_inodes;        /* all inodes */
        struct list_head         s_dirty;        /* dirty inodes */
        struct block_device        *s_bdev;
        void         *s_fs_info;        /* Filesystem private info */
};

从两个方面了解超级块结构的作用。

  1. 超级块结构给出了文件系统的全局信息。

    • s_blocksize 指定了文件系统的块大小。
    • s_maxbytes 指定文件系统中最大文件的尺寸。
    • s_type 是指向 file_system_type 结构的指针。
    • s_magic 是魔术数字,每个文件系统都有一个魔术数字。
    • s_root 是指向文件系统根dentry的指针。

    超级块对象还定义了一些链表头,用来链接文件系统内的重要成员。

    • s_inodes 指向文件系统内所有的inode,通过它可以遍历inode对象。
    • s_dirty 指向所有dirty的inode对象。
    • s_bdev 指向文件系统存在的块设备指针。

    在后面具体文件系统的例子中,可以看到这些成员是如何赋值和应用的。

  2. 超级块结构包含一些函数指针。 super_operations 提供了最重要的超级块操作。例如 super_operation 的成员函数 read_inode 提供了读取inode信息的功能。每个具体的文件系统一般都要提供这个函数来实现对inode信息的读取,例如ext2文件系统提供的具体函数是 ext2_read_inode。从这个例子,我们可以理解“VFS提供了架构,而具体文件系统必须按照VFS的架构去实现”的含义。

2.1.3 目录项dentry

对于一个通常的文件系统来说,文件和目录一般按树状结构保存。直观来看,目录里保存着文件,而所有目录一层层汇聚,最终到达根目录。从这个树状结构,我们可以想象VFS应该有数据结构反映这种树状的结构。实际上确实如此,目录项(dentry)就是反映了文件系统的这种树状关系。

在VFS里,目录本身也是一个文件,只是有点特殊。每个文件都有一个dentry(可能不止一个),这个dentry链接到上级目录的dentry。根目录有一个dentry结构,而根目录里的文件和目录都链接到这个根dentry,二级目录里的文件和目录,同样通过dentry链接到二级目录。这样一层层链接,就形成了一颗dentry树。从树顶可以遍历整个文件系统的所有目录和文件。

为了加快对dentry的查找,内核使用了hash表来缓存dentry,称为dentry cache。dentry cache在后面的分析中经常用到,因为dentry的查找一般都先在dentry cache里进行查找。

dentry的结构定义同样很庞杂,和超级块类似,我们当前只分析最重要的部分。dentry结构简化后的定义如代码清单2-2所示。

代码清单2-2 dentry结构的简化定义

struct dentry {
 ....../省略dentry锁、标志等代码/
        struct inode *d_inode;        /* Where the name belongs to */
        /*
         * The next three fields are touched by __d_lookup.  Please
         * so they all fit in a cache line.
         */
        struct hlist_node d_hash;        /* lookup hash list */
        struct dentry *d_parent;                /* parent directory */
        struct qstr d_name;
        /*
         * d_child and d_rcu can share memory
         */
        union {
                struct list_head d_child;        /* child of parent list */
                 struct rcu_head d_rcu;
        } d_u;
        struct list_head d_subdirs;        /* our children */
        struct dentry_operations *d_op;
        struct super_block *d_sb;        /* The root of the dentry tree */
        int d_mounted;
};

对dentry结构的成员解释如下。

  • d_inode 指向一个inode结构。这个inode和dentry共同描述了一个普通文件或者目录文件。
  • dentry结构里有d_subdirs成员和d_child成员。d_subdirs是子项(子项可能是目录,也可能是文件)的链表头,所有的子项都要链接到这个链表。d_child是dentry自身的链表头,需要链接到父dentry的d_subdirs成员。当移动文件的时候,需要把一个dentry结构从旧的父dentry的链表上脱离,然后链接到新的父dentry的d_subdirs成员。这样dentry结构之间就构成了一颗目录树。
  • d_parent 是指针,指向父dentry结构。
  • d_hash 是链接到dentry cache的hash链表。
  • d_name 成员保存的是文件或者目录的名字。打开一个文件的时候,根据这个成员和用户输入的名字比较来搜寻目标文件。
  • d_mounted 用来指示dentry是否是一个挂载点。如果是挂载点,该成员不为零。

2.1.4 索引节点inode

inode 代表一个文件。inode保存了文件的大小、创建时间、文件的块大小等参数,以及对文件的读写函数、文件的读写缓存等信息。一个真实的文件可以有多个dentry,因为指向文件的路径可以有多个(考虑文件的链接),而inode只有一个。

根据上面的描述是否可以得出结论,即dentry和inode代表一个文件?事实基本如此,inode和dentry分别代表了文件通用的两个部分,只不过对某些文件系统而言,inode提供的信息还不够,还需要其他信息。这个将在后面具体的文件系统里面看到。

inode的结构定义如代码清单2-3所示。因为inode的定义非常庞大,在我们初次认识inode结构的时候,将inode结构定义大大简化,以重点突出几个结构成员。

代码清单2-3 inode的结构定义

struct inode {
        struct list_head        i_list;
        struct list_head        i_sb_list;
        struct list_head        i_dentry;
        unsigned long        i_ino;
        atomic_t        i_count;
        loff_t        i_size;
        
        unsigned int        i_blkbits;
        struct inode_operations        *i_op;
        const struct file_operations        *i_fop;        /* former ->i_op->default_file_ops */
        struct address_space        *i_mapping;
        struct block_device        *i_bdev;
  ....../*省略锁等代码*/
};

inode结构非常复杂,同样,我们只分析其中最重要的几个成员,以简单理解inode最重要的作用。而其他的成员在后面的章节中继续分析,逐渐丰富对inode的理解。

  • 成员 i_listi_sb_listi_dentry 分别是三个链表头。成员 i_list 用于链接描述inode当前状态的链表。成员 i_sb_list 用于链接到超级块中的inode链表。当创建一个新的inode的时候,成员 i_list 要链接到 inode_in_use 这个链表,表示inode处于使用状态,同时成员 i_sb_list 也要链接到文件系统超级块的 s_inodes 链表头。由于一个文件可以对应多个dentry,这些dentry都要链接到成员 i_dentry 这个链表头。
  • 成员 i_ino 是inode的号,而 i_count 是inode的引用计数。成员 i_size 是以字节为单位的文件长度。
  • 成员 i_blkbits 是文件块的位数。
  • 成员 i_fop 是一个 struct file_operations 类型的指针。文件的读写函数和异步io函数都在这个结构中提供。每一个具体的文件系统,基本都要提供各自的文件操作函数。
  • i_mapping 是一个重要的成员。这个结构目的是缓存文件的内容,对文件的读写操作首先要在 i_mapping 包含的缓存里寻找文件的内容。如果有缓存,对文件的读就可以直接从缓存中获得,而不用再去物理硬盘读取,从而大大加速了文件的读操作。写操作也要首先访问缓存,写入到文件的缓存。然后等待合适的机会,再从缓存写入硬盘。后面我们将分析文件的具体读写,在此处只需要理解基本的作用即可。
  • 成员 i_bdev 是指向块设备的指针。这个块设备就是文件所在的文件系统所绑定的块设备。

2.1.5 文件

文件对象的作用是描述进程和文件交互的关系。这里需要指出的是,硬盘上并不存在一个文件结构。进程打开一个文件,内核就动态创建一个文件对象。同一个文件,在不同的进程中有不同的文件对象。

文件的结构定义如代码清单2-4所示。

代码清单2-4 文件的数据结构

struct file {
        struct dentry        *f_dentry;
        struct vfsmount        *f_vfsmnt;
        const struct file_operations        *f_op;
        ....../*省略部分代码*/
        loff_t        f_pos;
        struct fown_struct        f_owner;
        unsigned int        f_uid, f_gid;
        struct file_ra_state        f_ra;
        struct address_space        *f_mapping;
};
  • f_dentryf_vfsmnt 分别指向文件对应的dentry结构和文件所属于的文件系统的 vfsmount对象。
  • 成员 f_pos 表示进程对文件操作的位置。例如对文件读取前10字节,f_pos 就指示第11字节位置。
  • f_uidf_gid 分别表示文件的用户ID和用户组ID。
  • 成员 f_ra 用于文件预读的设置。在第10章将继续详细分析预读的使用。
  • f_mapping 指向一个 address_space 结构。这个结构封装了文件的读写缓存页面。

2.2 文件系统的架构

VFS是具体文件系统的抽象,而VFS又是依靠超级块、inode、dentry以及文件这些结构来发挥作用。所以文件系统的架构就体现在这些结构的使用方式中。

2.2.1 超级块作用分析

每个文件系统都有一个超级块结构,每个超级块都要链接到一个超级块链表。而文件系统内的每个文件在打开时都需要在内存分配一个inode结构,这些inode结构都要链接到超级块。

图2-1展示了超级块之间的关系以及超级块和inode之间的链接关系。super_block1和super_block2是两个文件系统的超级块,它们被链接到super_blocks链表头,后者使用的就是内核基础层介绍的双向链表数据结构,顺着super_blocks链表可以遍历整个操作系统打开过的文件的inode结构。

图2-1 超级块的结构

此图描述了超级块链表(super_blocks)与两个超级块(super_block1、super_block2)的连接关系,以及每个超级块链接到各自文件系统内所有inode的链表(s_inodes)。超级块之间通过链表串联,每个超级块的s_inodes链表头指向该文件系统所有的inode结构。

2.2.2 dentry作用分析

2.1.3节分析了VFS中dentry的数据结构和作用,为了进一步理解dentry,我们用图2-2来解释dentry的链接关系。

图2-2 文件系统的目录结构

根目录下有usr和home两个目录,usr目录下有wj和nk两个文件,home目录下有个mnt目录,这是另外一个文件系统,挂载(mount)到当前文件系统。在mnt目录下有个cj文件。

文件系统的dentry链表图如图2-3所示。这只是个示意图,但是展示了几个重要的概念。

图2-3 dentry链表图

示意图展示了以下关键点:

  1. 每个文件的dentry链接到父目录的dentry,形成了文件系统的结构树。具体说,就是usr和home两个dentry的d_child成员链接到根目录dentry_hashtable是个数组,它的数组成员是第1章介绍过的hash链表数据结构。这里所说的dentry,指的是在内存中的dentry。如果某个文件已经被打开过,内存中就应该有该文件的dentry结构,并且该dentry被链接到dentry_hashtable数组的某个hash链表头。后续再访问该文件的时候,就可以直接从hash链表里面找到,避免了再次读硬盘。这是dentry的cache概念。
  1. home目录下的mnt目录指向一个挂载的文件系统。如何判断目录不是一个普通的目录,而是一个文件系统?这是dentry的d_mounted成员的功能。如果该成员不为0,代表该dentry是个挂载点,有文件系统挂载,需要特殊处理。

从图2-3可以看到,挂载过来的文件系统本身也有一个dentry树,也有自己的根目录。两个dentry树之间并没有链接关系。如何查找到挂载的文件系统?我们用图2-4来解释。

图2-4展示了一个新的数据结构vfsmount。对这个数据结构不做过多的探讨,只需要知道每个文件系统都有这样一个结构。当文件系统被挂载的时候,它的vfsmount结构就被链接到内核的一个全局链表—mount_hashtable数组链表。

图2-4 挂载的链表图

mount_hashtable是个数组,它的每个成员都是一个hash链表。上文的例子有两个vfsmount,cj文件所在文件系统的vfsmount被链接到mount_hashtable。这样当发现mnt目录是个特殊的目录时,从mount_hashtable数组找到hash链表头,再遍历整个hash链表,就能找到cj文件所在文件系统的vfsmount,然后mnt目录的dentry就被替换,置换为新文件系统的根目录。具体过程参考打开文件的代码分析。

2.2.3 inode作用分析

系统内核提供了一个hash链表数组inode_hashtable,所有的inode结构都要链接到数组里面的某个hash链表。这种用法和前文介绍的hash链表数组dentry_hashtable的用法很类似,这里就不再分析了。

在Linux系统里,字符设备和块设备、普通文件和目录、socket等都是一个文件,所以它们都有自己的inode结构。为了辨别这些不同的类型,inode结构的i_mode成员用不同的值代表不同的类型。如表2-1所示。

表2-1 i_mode代表的类型

类型描述
S_IFREG普通文件
S_IFDIR目录
S_IFCHR字符设备
S_IFBLK块设备
S_IFLNK符号链接
S_IFSOCK套接字
S_IFIFO命名管道

inode还有一个重要的作用是缓存文件的数据内容。这是通过成员i_mapping实现的。i_mapping使用了数据结构radix树来保存文件的数据。这个数据结构在1.2节已经介绍过。在后面文件的读写过程分析中,还将继续分析文件内容读写的代码。

2.2.4 文件作用分析

文件对象代表进程和具体文件交互的关系。内核为每个打开的文件申请一个文件对象,同时返回文件的号码。如图2-5所示,每个进程都指向一个文件描述符表。

图2-5 进程和文件描述符表

这个表里面用数组保存了进程中每个打开的文件。当应用进程打开一个硬盘上的文件时,内核要为这个文件分配一个文件对象并保存文件指针到文件描述符表里面的数组。

[!注意]
这里的“文件”指的是在硬盘上具体存在的文件,而“文件对象”是在内存里存在的文件结构。当读写文件的时候,通过文件号就可以获得文件的对象。这也就是读写文件必须先打开文件的原因,如果不执行打开的过程,是不能完成文件读写的。

2.3 从代码层次深入分析文件系统

文件系统千头万绪,但对用户来说,最重要的就是创建目录、创建文件、打开文件和文件读写。对于通常的硬盘文件系统来说,这要涉及硬盘的读写和硬盘空间管理,而读写从文件系统层一直到通用块设备层再到硬盘驱动,未免太过复杂。为了简化,我们给出一个最简单文件系统,通过这个例子导入文件系统的基本概念。然后通过代码分析,逐步深入内核,了解一下博大精深的内核架构。

[!提示]
本文假定读者已经了解模块概念,以及如何编译和安装模块。

2.3 从代码层次深入分析文件系统

文件系统千头万绪,但对用户来说,最重要的就是创建目录、创建文件、打开文件和文件读写。对于通常的硬盘文件系统来说,这要涉及硬盘的读写和硬盘空间管理,而读写从文件系统层一直到通用块设备层再到硬盘驱动,未免太过复杂。为了简化,我们给出一个最简单文件系统,通过这个例子导入文件系统的基本概念。然后通过代码分析,逐步深入内核,了解一下博大精深的内核架构。

TIP

本文假定读者已经了解模块概念,以及如何编译和安装模块。

2.3.1 一个最简单的文件系统aufs

我们先写一个最简单的文件系统,这个文件系统直接创建在内存中。它在内存中创建了两个目录和几个文件,用户可以通过ls命令显示目录和文件,但是无法创建目录和文件,也不能对文件进行读写操作。这样不涉及硬盘操作,大大简化了开始阶段需要考虑的问题,这个例子如代码清单2-5所示。

代码清单2-5 最简单文件系统aufs源代码

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/pagemap.h>
#include <linux/mount.h>
#include <linux/init.h>
#include <linux/namei.h>
 
#define AUFS_MAGIC        0x64668735
 
static struct vfsmount *aufs_mount;
static int aufs_mount_count;
 
static struct inode *aufs_get_inode(struct super_block *sb, int mode, dev_t dev)
{
  struct inode *inode = new_inode(sb);
  if (inode) {
       inode->i_mode = mode;
       inode->i_uid = current->fsuid;
       inode->i_gid = current->fsgid;
       inode->i_blksize = PAGE_CACHE_SIZE;
       inode->i_blocks = 0;
       inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
       switch (mode & S_IFMT) {
       default:
           init_special_inode(inode, mode, dev);
           break;
       case S_IFREG:
           printk("creat a  file \n");
           break;
       case S_IFDIR:
           inode->i_op = &simple_dir_inode_operations;
           inode->i_fop = &simple_dir_operations;
           printk("creat a dir file \n");
           inode->i_nlink++;
           break;
       }
  }
  return inode;
}
 
/* SMP-safe */
static int aufs_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev)
{
  struct inode *inode;
  int error = -EPERM;
  if (dentry->d_inode)
       return -EEXIST;
  inode = aufs_get_inode(dir->i_sb, mode, dev);
  if (inode) {
       d_instantiate(dentry, inode);
       dget(dentry);
       error = 0;
  }
  return error;
}
 
static int aufs_mkdir(struct inode *dir, struct dentry *dentry, int mode)
{
  int res;
  res = aufs_mknod(dir, dentry, mode | S_IFDIR, 0);
  if (!res)
       dir->i_nlink++;
  return res;
}
 
static int aufs_create(struct inode *dir, struct dentry *dentry, int mode)
{
  return aufs_mknod(dir, dentry, mode | S_IFREG, 0);
}
 
static int aufs_fill_super(struct super_block *sb, void *data, int silent)
{
  static struct tree_descr debug_files[] = {{""}};
  return simple_fill_super(sb, AUFS_MAGIC, debug_files);
}
 
static struct super_block *aufs_get_sb(struct file_system_type *fs_type,
                            int flags, const char *dev_name,
                            void *data)
{
  return get_sb_single(fs_type, flags, data, aufs_fill_super);
}
 
static struct file_system_type au_fs_type = {
  .owner =    THIS_MODULE,
  .name =     "aufs",
  .get_sb =   aufs_get_sb,
  .kill_sb =  kill_litter_super,
};
 
static int aufs_create_by_name(const char *name, mode_t mode,
                 struct dentry *parent,
                 struct dentry **dentry)
{
  int error = 0;
  /* If the parent is not specified, we create it in the root.
   * We need the root dentry to do this, which is in the super
   * block. A pointer to that is in the struct vfsmount that we
   * have around.
   */
  if (!parent) {
       if (aufs_mount && aufs_mount->mnt_sb) {
           parent = aufs_mount->mnt_sb->s_root;
       }
  }
  if (!parent) {
       printk("Ah! can not find a parent!\n");
       return -EFAULT;
  }
  *dentry = NULL;
  mutex_lock(&parent->d_inode->i_mutex);
  *dentry = lookup_one_len(name, parent, strlen(name));
  if (!IS_ERR(*dentry)) {
       if ((mode & S_IFMT) == S_IFDIR)
            error = aufs_mkdir(parent->d_inode, *dentry, mode);
       else
            error = aufs_create(parent->d_inode, *dentry, mode);
  } else
       error = PTR_ERR(*dentry);
  mutex_unlock(&parent->d_inode->i_mutex);
  return error;
}
 
struct dentry *aufs_create_file(const char *name, mode_t mode,
                  struct dentry *parent, void *data,
                  struct file_operations *fops)
{
  struct dentry *dentry = NULL;
  int error;
  printk("aufs: creating file '%s'\n",name);
  error = aufs_create_by_name(name, mode, parent, &dentry);
  if (error) {
       dentry = NULL;
       goto exit;
  }
  if (dentry->d_inode) {
       if (data)
            dentry->d_inode->u.generic_ip = data;
       if (fops)
            dentry->d_inode->i_fop = fops;
  }
exit:
  return dentry;
}
 
struct dentry *aufs_create_dir(const char *name, struct dentry *parent)
{
  return aufs_create_file(name,
                  S_IFDIR | S_IRWXU | S_IRUGO | S_IXUGO,
                  parent, NULL, NULL);
}
 
static int __init aufs_init(void)
{
  int retval;
  struct dentry *pslot;
 
  retval = register_filesystem(&au_fs_type);
  if (!retval) {
       aufs_mount = kern_mount(&au_fs_type);
       if (IS_ERR(aufs_mount)) {
           printk(KERN_ERR "aufs: could not mount!\n");
           unregister_filesystem(&au_fs_type);
           return retval;
       }
  }
 
  pslot = aufs_create_dir("woman star",NULL);
  aufs_create_file("lbb", S_IFREG | S_IRUGO, pslot, NULL, NULL);
  aufs_create_file("fbb", S_IFREG | S_IRUGO, pslot, NULL, NULL);
  aufs_create_file("ljl", S_IFREG | S_IRUGO, pslot, NULL, NULL);
  pslot = aufs_create_dir("man star",NULL);
  aufs_create_file("ldh", S_IFREG | S_IRUGO, pslot, NULL, NULL);
  aufs_create_file("lcw", S_IFREG | S_IRUGO, pslot, NULL, NULL);
  aufs_create_file("jw", S_IFREG | S_IRUGO, pslot, NULL, NULL);
 
  return retval;
}
 
static void __exit aufs_exit(void)
{
  simple_release_fs(&aufs_mount, &aufs_mount_count);
  unregister_filesystem(&au_fs_type);
}
 
module_init(aufs_init);
module_exit(aufs_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("This is a simple module");
MODULE_VERSION("Ver 0.1");

整个程序只有两百多行,即使对文件系统一点不懂也能了解大概意思。这个例子不包括文件读写等内容,目的只是说明文件系统内 dentryinodesuper_block 等几个重要概念。

程序编译后,我们通过 insmod 命令加载模块,然后执行如下操作。

  1. 在根目录下创建一个目录:
    mkdir au
  2. 挂载文件系统:
    mount -t aufs none /au
  3. 列出文件系统的内容:
    ls
    看到了什么?可以发现 “woman star” 和 “man star” 两个目录。然后到 woman star 目录再执行 ls,目录下果然有 lbbfbbljl 三个文件。这就是我们在代码中希望做到的事情。

2.3.2 文件系统如何管理目录和文件

现在我们先看看这段程序究竟执行了什么就创建了一个最简单文件系统,然后再深入内核分析究竟发生了什么。

整个程序代码可以分为三部分:

  1. 首先是 register_filesystem 函数,这个函数把 aufs 文件系统登记到系统;
  2. 然后调用 kern_mount 函数为文件系统申请必备的数据结构;
  3. 最后在 aufs 文件系统内创建两个目录,每个目录下面创建三个文件。

register_filesystem 函数顾名思义,就是把一个特定的文件系统登记到内核。这个函数的实现如代码清单2-6所示。

代码清单2-6 register_filesystem 函数

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
       *p = fs;
  write_unlock(&file_systems_lock);
  return res;
}

函数 register_filesystem 的参数是一个文件类型指针,函数执行部分要在内核寻找相同名字的文件系统,如果不存在相同名字的文件系统,就把 aufs 加入到系统的文件系统链表。如果这个文件系统已经存在,则返回忙。

内核定义一个全局变量 file_systems,用来保存所有登记的文件系统,而 find_filesystem 利用全局变量 file_systems 执行了具体的查找过程。这个函数非常简单,但是显然主要工作没在这里完成,我们继续往下看 kern_mount 函数。kern_mount 真正为文件系统分配了超级块对象和 vfsmount 对象。具体代码如代码清单2-7所示。

代码清单2-7 kern_mount 函数

struct vfsmount *kern_mount(struct file_system_type *type)
{
  return vfs_kern_mount(type, 0, type->name, NULL);
}

kern_mount 只是 vfs_kern_mount 的封装。vfs_kern_mount 的代码如代码清单2-8所示。

代码清单2-8 vfs_kern_mount 函数

struct vfsmount *
vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
  struct vfsmount *mnt;
  char *secdata = NULL;
  int error;
 
  if (!type)
       return ERR_PTR(-ENODEV);
 
  /* 分配一个vfsmount结构 */
  error = -ENOMEM;
  mnt = alloc_vfsmnt(name);
  if (!mnt)
       goto out;

vfs_kern_mount 函数的代码很长,为了方便阅读,我们将 vfs_kern_mount 分为三个部分。

第一部分:根据文件系统的名字为文件系统创建了一个 vfsmount 结构。这个结构在2.2.2节分析过,是用来为文件系统之间的挂载关系而设计的,后文还将继续分析具体的 mount 过程。

  /* 如果mount有数据参数,才执行下面的代码.对本章的例子来说,这部分跳过 */
  if (data) {
       secdata = alloc_secdata();
       if (!secdata)
           goto out_mnt;
       error = security_sb_copy_data(type, data, secdata);
       if (error)
           goto out_free_secdata;
  }
 
  /* 调用文件系统超级块提供的get_sb函数.对aufs文件系统来说,就是aufs_get_sb */
  error = type->get_sb(type, flags, name, data, mnt);
  if (error < 0)
       goto out_free_secdata;

第二部分:调用文件系统提供的 get_sb 创建一个超级块对象。创建超级块对象的同时,还要创建一个 dentry 结构作为文件系统的根 dentry(root dentry)和一个 inode 结构作为文件系统的根 inode。文件系统的根 dentry 等于文件系统的根目录,后续创建的一级目录都要以这个根目录作为父目录。

  /* 安全相关的代码,可以跳过 */
  error = security_sb_kern_mount(mnt->mnt_sb, secdata);
  if (error)
       goto out_sb;
  mnt->mnt_mountpoint = mnt->mnt_root;
  mnt->mnt_parent = mnt;
  up_write(&mnt->mnt_sb->s_umount);
  free_secdata(secdata);
  return mnt;
}

最后一部分设置vfsmount结构的父指针为自身,mnt_mountpoint为文件系统的根dentry。如果把文件系统mount到其他的文件系统,那么这两个参数就要设置为源文件系统的参数。

三部分综合起来,vfs_kern_mount函数实际已经执行了文件系统登记的大部分工作。

vfs_kern_mount函数的整体分析完毕,其中的get_sb函数比较重要,需要细节分析。对aufs文件系统而言,它的get_sb函数是aufs_get_sb。这个函数的代码如代码清单2-9所示。

代码清单2-9 aufs_get_sb函数

static struct super_block *aufs_get_sb(struct file_system_type *fs_type,
                      int flags, const char *dev_name, void *data)
{
  return get_sb_single(fs_type, flags, data, aufs_fill_super);
}

aufs_get_sb是个封装函数,实际上调用了系统提供的get_sb_single函数,这个函数的代码如代码清单2-10所示。

代码清单2-10 get_sb_single函数

       int get_sb_single(struct file_system_type *fs_type,
791    int flags, void *data,
792    int (*fill_super)(struct super_block *, void *, int),
793    struct vfsmount *mnt)
794    {
795    struct super_block *s;
796    int error;
797 
798    s = sget(fs_type, compare_single, set_anon_super, NULL);
799    if (IS_ERR(s))
800         return PTR_ERR(s);
801    if (!s->s_root) {
802         s->s_flags = flags;
            /*调用传递进来的函数指针.这里就是aufs_fill_super*/
803         error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
            ......
809         s->s_flags |= MS_ACTIVE;
810    }
       /*改变mount的选项*/
811    do_remount_sb(s, flags, data, 0);
812    return simple_set_mnt(mnt, s);
813 }

get_sb_single首先获得一个超级块对象,如果文件系统的超级块对象已经存在,返回对象指针,如果不存在,则创建一个新的超级块对象。

创建超级块对象后,第801行代码检查超级块对象是否有根dentry,如果尚未有根dentry,调用传递进来的函数指针fill_super为超级块对象填充根dentry和根inode。

最后的simple_set_mnt函数要把创建的超级块对象赋值给vfsmount结构所指的超级块,同时vfsmount所指的mnt_root点赋值为超级块所指的根dentry。于是从vfsmount结构就可以获得文件系统的超级块对象和根dentry。

函数fill_super函数作用是为超级块对象申请必备的成员。对于aufs文件系统而言,传递的函数指针是aufs_fill_super函数。这个函数的代码如代码清单2-11所示。

代码清单2-11 aufs_fill_super函数

static int aufs_fill_super(struct super_block *sb, void *data,
                           int silent)
{
  static struct tree_descr debug_files[] = {{""}};
  return simple_fill_super(sb, AUFS_MAGIC, debug_files);
}

aufs_fill_super定义了一个空的tree_descr结构,这个结构的作用是描述一些文件。如果不为空,填充超级块的同时,需要在根目录下创建一些文件;当前为空,说明不需要创建任何文件。simple_fill_super函数实现了填充根dentry的执行过程,具体代码如代码清单2-12所示。

代码清单2-12 填充超级块的simple_fill_super函数

        int simple_fill_super(struct super_block *s, int magic,
368        struct tree_descr *files)
369        {
370         static struct super_operations s_ops = {.statfs = simple_statfs};
371         struct inode *inode;
372         struct dentry *root;
373         struct dentry *dentry;
374         int i;
375 
376         s->s_blocksize = PAGE_CACHE_SIZE;
377         s->s_blocksize_bits = PAGE_CACHE_SHIFT;
378         s->s_magic = magic;
379         s->s_op = &s_ops;
380         s->s_time_gran = 1;
381 
382         inode = new_inode(s);
383         if (!inode)
384             return -ENOMEM;
385         inode->i_mode = S_IFDIR | 0755;
386         inode->i_uid = inode->i_gid = 0;
387         inode->i_blksize = PAGE_CACHE_SIZE;
388         inode->i_blocks = 0;
389         inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
390         inode->i_op = &simple_dir_inode_operations;
391         inode->i_fop = &simple_dir_operations;
392         inode->i_nlink = 2;
        /*创建一个dentry对象*/
393         root = d_alloc_root(inode);

simple_fill_super函数第一部分是为超级块对象赋初值。超级块对象指示了文件系统的块大小,第376行赋值aufs文件系统的块尺寸为一个页面,一个页面通常是4K大小,以比特位计算就是12位。第379行为超级块对象赋予操作函数,simple_fill_super实际上只为超级块对象提供了一个操作函数simple_statfs

从第382行开始创建一个inode结构。这个inode是文件系统的根inode,所以第383行设置它是一个目录,代表根目录,然后设置它的块尺寸和文件大小。inode结构包含它的操作函数,第389行和390行分别赋值它的inode操作函数和文件操作函数。

第393行d_alloc_root作用是创建一个根dentry。根dentry的父结构是自身,它指向的超级块对象就是文件系统的超级块对象。根dentry的名字被指定为斜杠符”/“,这就是从根目录开始查找文件使用斜杠符”/“作为开始的原因。

        for (i = 0; !files->name || files->name[0]; i++, files++)
398         if (!files->name)
399            continue;
400            dentry = d_alloc_name(root, files->name);
401         if (!dentry)
402             goto out;
403         inode = new_inode(s);
404         if (!inode)
405             goto out;
406         inode->i_mode = S_IFREG | files->mode;
407         inode->i_uid = inode->i_gid = 0;
408         inode->i_blksize = PAGE_CACHE_SIZE;
409         inode->i_blocks = 0;
410         inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
411         inode->i_fop = files->ops;
412         inode->i_ino = i;
413         d_add(dentry, inode);
414         }
        /*超级块内保存root dentry的指针.这样内核就可以通过root dentry找到根inode*/
415         s->s_root = root;
416         return 0;

simple_fill_super函数的第二部分是根据传递进来的参数,在根目录下创建一系列的文件。因为当前场景传进来的是空参数,所以这里可以跳过。

通过代码分析,到目前为止,创建了一个超级块对象,创建了一个根dentry和一个根inode。后面创建的文件和目录都应该链接到这个根dentry。代码里面的new_inoded_alloc_root都很简单,留给读者自己分析。

再回到aufs文件系统。创建目录和创建文件调用了不同的函数,aufs_create_dir函数的作用是创建一个目录,它的代码如代码清单2-13所示。

代码清单2-13 aufs_create_dir

struct dentry *aufs_create_dir(const char *name, struct dentry *parent)
{
  return aufs_create_file(name, 
                  S_IFDIR | S_IRWXU | S_IRUGO | S_IXUGO,
                  parent, NULL, NULL);
}

aufs_create_diraufs_create_file函数的封装函数,aufs_create_file函数执行创建文件的过程。调用aufs_create_file的时候,传递的参数S_IFDIR指明创建的是一个目录。

aufs_create_file的代码如代码清单2-14所示。

代码清单2-14 创建文件的aufs_create_file函数

struct dentry *aufs_create_file(const char *name, mode_t mode,
                  struct dentry *parent, void *data,
                  struct file_operations *fops)
{
  ......
  error = aufs_create_by_name(name, mode, parent, &dentry);
  ......
  if (dentry->d_inode) {
        if (data)
            dentry->d_inode->u.generic_ip = data;
        if (fops)
            dentry->d_inode->i_fop = fops;
  }
exit:
  return dentry;
}

回忆前面的知识,文件是由dentry和inode代表的。而执行函数aufs_create_by_name后,空指针dentry已经被赋值了,而且有d_inode成员,说明它的的作用是创建文件的dentry和inode结构,如代码清单2-15所示。

代码清单2-15 根据名字创建文件的aufs_create_by_name函数

static int aufs_create_by_name(const char *name, mode_t mode,
                 struct dentry *parent,
                 struct dentry **dentry)
{
  int error = 0;
  /*如果没有父目录,就赋予一个.赋予的是哪一个?就是前面创建的root dentry*/
  if (!parent ) {
      if (aufs_mount && aufs_mount->mnt_sb) {
          parent = aufs_mount->mnt_sb->s_root;
      }
  }
  if (!parent) {
      printk("Ah! can not find a parent!\n");
      return -EFAULT;
}        

函数aufs_create_by_name起始部分要判断是否有父目录,如果没有父目录就赋予一个。赋予的是哪一个?就是文件系统的根dentry。

*dentry = NULL;
  mutex_lock(&parent->d_inode->i_mutex);
  /*检查要创建的这个目录存在不存在?是不是重复了*/
  *dentry = lookup_one_len(name, parent, strlen(name));
  if (!IS_ERR(dentry)) {
      /*分成两个分支.如果是目录则调用aufs_mkdir,如果是文件,则调用aufs_create*/
      if ((mode & S_IFMT) == S_IFDIR)
           error = aufs_mkdir(parent->d_inode, *dentry, mode);
      else 
           error = aufs_create(parent->d_inode, *dentry, mode);
  } else
      error = PTR_ERR(dentry);
  mutex_unlock(&parent->d_inode->i_mutex);
  return error;
}

然后调用lookup_one_len获得一个dentry结构。lookup_one_len函数首先在父目录下根据名字查找dentry结构,如果存在同名的dentry结构就返回指针,如果不存在就创建一个dentry。lookup_one_len的代码如代码清单2-16所示。

代码清单2-16 查找同名dentry的lookup_one_len函数

        struct dentry * lookup_one_len(const char * name, struct dentry *base, int len)
1272        {
        ....../*省略参数定义*/
1277         this.name = name;
1278         this.len = len;
1279         if (!len)
1280             goto access;
1281   /*这里根据名字计算hash值,看看是怎么计算的*/
1282         hash = init_name_hash();
1283         while (len--) {
1284             c = *(const unsigned char *)name++;
1285             if (c == '/' || c == '\0')
1286                 goto access;
1287             hash = partial_name_hash(c, hash);
1288         }
1289         this.hash = end_name_hash(hash);
1290 
1291         return __lookup_hash(&this, base, NULL);

第1282~1289行的作用是计算名字的hash值。因为初始hash函数init_name_hash和最终hash函数end_name_hash都不执行任何的计算,所以最终得到的hash值是将名字中的每个字符做固定的数学运算得到的。

__lookup_hash函数是通过hash值查找同名字的dentry结构,它的代码如代码清单2-17所示。

代码清单2-17 在hash链表中查找的__lookup_hash函数

static struct dentry * __lookup_hash(struct qstr *name, 
struct dentry * base, struct nameidata *nd)
{
  struct dentry * dentry;
  struct inode *inode;
  int err;
  inode = base->d_inode;
  err = permission(inode, MAY_EXEC, nd);
  dentry = ERR_PTR(err);
  if (err)
      goto out;
  /*
   * See if the low-level filesystem might want
   * to use its own hash..
   */
  if (base->d_op && base->d_op->d_hash) {
      err = base->d_op->d_hash(base, name);
      dentry = ERR_PTR(err);
      if (err < 0)
          goto out;
  }

__lookup_hash函数第一部分是检查inode的权限,然后检查文件系统是否提供了特有的hash函数,如果有hash函数,则调用重新计算hash值。

dentry = cached_lookup(base, name, nd);
if (!dentry) {
      /*如果没有找到.说明这个目录不存在,则创建一个dentry*/
      struct dentry *new = d_alloc(base, name);
      dentry = ERR_PTR(-ENOMEM);
      if (!new)
          goto out;
      dentry = inode->i_op->lookup(inode, new, nd);
      if (!dentry)
          dentry = new;
      else
          dput(new);
  }
out:
  return dentry;
}

cached_lookup在dentry cache里面查找同名的dentry结构,如果返回为空,说明不存在```c dentry = cached_lookup(base, name, nd); if (!dentry) { /如果没有找到.说明这个目录不存在,则创建一个dentry/ struct dentry *new = d_alloc(base, name); dentry = ERR_PTR(-ENOMEM); if (!new) goto out; dentry = inodei_oplookup(inode, new, nd); if (!dentry) dentry = new; else dput(new); } out: return dentry; }


`cached_lookup`在dentry cache里面查找同名的dentry结构,如果返回为空,说明不存在同名的dentry结构,那么调用`d_alloc`创建一个新的dentry结构。

创建dentry结构完成后,需要再次调用文件系统的`lookup`查找是否有同名的dentry存在。这样做为了防止同名的dentry已经被其他用户提前创建了。

`cached_lookup`函数的代码如代码清单2-18所示。

**代码清单2-18 cached_lookup函数**

```c
static struct dentry * cached_lookup(struct dentry * parent, 
struct qstr * name, struct nameidata *nd)
{
  struct dentry * dentry = __d_lookup(parent, name);
  /* lockess __d_lookup may fail due to concurrent d_move() 
   * in some unrelated directory, so try with d_lookup
   */
  /*注意原来的解释,为何再次查找?因为要防止一个并发的move操作*/
  if (!dentry)
       dentry = d_lookup(parent, name);
       ....../*省略校验的代码*/
}

cached_lookup函数使用两次lookup。第一次调用__d_lookup,而第二次调用d_lookup。这是因为要防止并发的d_move操作。__d_lookup执行中没有获得重命名锁,有可能因为重命名操作而失败。

这种类似的做法在内核里很常见。因为内核是为所有用户进程提供服务,必须考虑并发性。

__d_lookup的代码如代码清单2-19所示。

代码清单2-19 __d_lookup函数

struct dentry * __d_lookup(struct dentry * parent, struct qstr *name)
{
  unsigned int len = name->len;
  unsigned int hash = name->hash;
  const unsigned char *str = name->name;
  struct hlist_head *head = d_hash(parent,hash);
  struct dentry *found = NULL;
  struct hlist_node *node;
  struct dentry *dentry;

函数__d_lookup起始部分要找到hash链表。内核中的dentry结构都根据hash值链接到众多hash链表中,这些hash链表的头结构保存在数组dentry_hashtable中。利用parent指针和hash值计算最终的hash值,从数组dentry_hashtable中获得hash链表的链表头。

  rcu_read_lock();
  /*遍历hash list */
  hlist_for_each_entry_rcu(dentry, node, head, d_hash) {
      struct qstr *qstr;
      if (dentry->d_name.hash != hash)
          continue;
      if (dentry->d_parent != parent)
          continue;
      spin_lock(&dentry->d_lock);
      /*
       * Recheck the dentry after taking the lock - d_move may
       * changed things.  Don't bother checking the hash because
       * about to compare the whole name anyway.
       */
      if (dentry->d_parent != parent)
          goto next;
      /*
       * It is safe to compare names since d_move() cannot
       * change the qstr (protected by d_lock).
       */
      qstr = &dentry->d_name;
      /*如果文件系统定义了d_compare函数,则调用*/
      if (parent->d_op && parent->d_op->d_compare) {
          if (parent->d_op->d_compare(parent, qstr, name))
              goto next;
      } else {
          /*比较长度是否相同*/
          if (qstr->len != len)
              goto next;
          /*比较目录名字是否相同*/
          if (memcmp(qstr->name, str, len))
              goto next;
      }
      if (!d_unhashed(dentry)) {
          atomic_inc(&dentry->d_count);
          found = dentry;
      }
      spin_unlock(&dentry->d_lock);
      break;
  next:
      spin_unlock(&dentry->d_lock);
  }
  rcu_read_unlock();
  return found;
}

获得hash链表头之后,__d_lookup遍历整个hash链表,寻找匹配的dentry结构。匹配的过程参考代码中的注释部分。找到或者创建dentry结构后,还需要创建inode结构。接续前面的分析,返回aufs_create_by_name函数。创建inode结构是其中aufs_mkdir函数的功能,它的代码如代码清单2-20所示。

代码清单2-20 创建目录文件的aufs_mkdir函数

static int aufs_mkdir(struct inode *dir, struct dentry *dentry,
                     int mode)
{
  int res;
  /*参数S_IFDIR指示是创建一个目录文件的inode*/
  res = aufs_mknod(dir, dentry, mode | S_IFDIR, 0);
  if (!res)
      dir->i_nlink++;
  return res;
}

aufs_mkdir封装了aufs_mknod函数,aufs_mknod代码如代码清单2-21所示。

代码清单2-21 aufs_mknod函数

static int aufs_mknod(struct inode *dir, struct dentry *dentry,
           int mode, dev_t dev)
{
  struct inode *inode;
  int error = -EPERM;
  /*如果dentry已经有d_inode结构,说明inode已经存在了*/
  if (dentry->d_inode)
      return -EEXIST;
  inode = aufs_get_inode(dir->i_sb, mode, dev);
if (inode) {
      d_instantiate(dentry, inode);
      dget(dentry);
      error = 0;
  }
  return error;
}

aufs_mknod 函数通过 aufs_get_inode 来创建目录文件的 inode,然后调用 d_instantiate 函数把 dentry 加入到 inode 的 dentry 链表头。

aufs_get_inode 创建了一个 inode 结构,同时要把 inode 结构加入超级块对象的 inode 链表头,这样从超级块对象的 inode 链表,可以遍历文件系统内所有的 inode 结构。除了超级块对象的链表,inode 还要加入一个全局变量链表 inode_in_use,这个链表指示 inode 结构是活跃的,处于使用中。aufs_get_inode 的代码如代码清单2-22所示。

代码清单2-22 aufs_get_inode函数

static struct inode *aufs_get_inode(struct super_block *sb, in
{
  /*申请一个inode结构*/
  struct inode *inode = new_inode(sb);
  if (inode) {
      inode->i_mode = mode;
      inode->i_uid = current->fsuid;
      inode->i_gid = current->fsgid;
      inode->i_blksize = PAGE_CACHE_SIZE;
      inode->i_blocks = 0;
      inode->i_atime = inode->i_mtime = inode->i_ctime = CURRE
      switch (mode & S_IFMT) {
      default:
          init_special_inode(inode, mode, dev);
          break;
      case S_IFREG:
          printk("creat a  file \n");
          break;
      case S_IFDIR:
          inode->i_op = &simple_dir_inode_operations;
          inode->i_fop = &simple_dir_operations;
          printk("creat a dir file \n");
      }

注意:代码清单2-22中 aufs_get_inode 函数的参数列表在原文中被截断,实际应为 struct super_block *sb, int mode, dev_t dev,此处保持原始文本样貌。

aufs_get_inode 函数后续:inode 属性与操作函数设置

inode->i_nlink++;
          break;
      }
  }
  return inode; 
}

可以看到,inode 结构的用户 ID 和组 ID 分别赋值为当前进程的文件系统用户 ID 和组 ID。代码中的 current 是内核定义的一个宏,它的作用是获得当前进程的结构指针。

aufs_get_inode 要根据 inode 的类型,设置不同的操作函数。对于特殊的 inode,比如块设备文件或者字符设备文件,调用 init_special_inode 赋值。对于目录文件,分别赋值 i_opi_fop

关键赋值细节

  • 用户 ID 和组 ID 来自当前进程的文件系统上下文。
  • 类型判断(S_IFBLKS_IFCHRS_IFDIR 等)决定了 inode 的操作函数表。

dentry 树与挂载依赖

目前为止,整个 aufs 文件系统的代码,已经为每个文件和目录都创建了 dentry 结构,同时为每个文件和目录创建了 inode 结构。这些文件和目录的 dentry 结构层层链接,已经在内核形成了一颗 dentry 树。但是这颗树还不能被访问,要真正使用起来,还需要挂载到根文件系统。

挂载前提

dentry 树虽然已形成,但尚未挂载到 VFS 根目录,因此当前无法通过路径访问。后续步骤需完成挂载操作。

2.3.3 文件系统的挂载过程

挂载(mount)做了什么事情?从理论上推断,系统本身也有一个文件目录树,如果把 aufs 创建的 dentry树 绑定到系统本来的 dentry 树上并建立链接,就可以从原先的系统树遍历到 aufs 的 dentry 树了。这就是 mount 过程。

执行文件系统的 mount 命令,要指定一个源文件系统和一个目的文件系统。同时要为目的文件系统指定一个目录,源文件系统就挂载到目的文件系统的这个目录下,这个目录称为 挂载点。一般源文件系统要指定设备名,这个设备就是源文件系统所存在的设备。因为 aufs 文件系统只存在于内存,并不存在于硬盘设备,所以 aufs 不用指定设备名。

文件系统挂载通过系统调用 sys_mount 来执行,sys_mount 又调用 do_mountdo_mount 首先获得挂载点目录的 dentry 结构以及目的文件系统的 vfsmount 结构,这些信息保存在一个 nameidata 结构中。然后根据 mount 选项调用不同的函数执行 mount。因为 aufs 文件系统第一次执行 mount,所以调用的是 do_new_mount 函数,该函数执行了 mount 的大部分事情。

代码清单 2-23 do_new_mount 函数

static int do_new_mount(struct nameidata *nd, char *type, int 
           int mnt_flags, char *name, void *data)
{
  struct vfsmount *mnt;
  if (!type || !memchr(type, 0, PAGE_SIZE))
      return -EINVAL;
  /* we need capabilities... */
  if (!capable(CAP_SYS_ADMIN))
      return -EPERM;
  mnt = do_kern_mount(type, flags, name, data);
  if (IS_ERR(mnt))
      return PTR_ERR(mnt);
  return do_add_mount(mnt, nd, mnt_flags, NULL);
}

do_kern_mount 前文已经分析了,目的是创建超级块对象和 root dentry 和 inode。因为 aufs 文件系统初始化时已经创建了这些对象,所以得到的是已经存在的对象。

do_add_mount 把源文件系统挂载到目的文件系统。

代码清单 2-24 do_add_mount 函数

int do_add_mount(struct vfsmount *newmnt, struct nameidata *nd,
                int mnt_flags, struct list_head *fslist)
{
  int err;
 
  down_write(&namespace_sem);
  /* Something was mounted here while we slept */
  while (d_mountpoint(nd->dentry) && follow_down(&nd))
      ;
  err = -EINVAL;
  /*检查namespace是否当前进程的namespace*/ 
  if (!check_mnt(nd->mnt))
      goto unlock;
 
  /* Refuse the same filesystem on the same mount point */
  err = -EBUSY;
  if (nd->mnt->mnt_sb == newmnt->mnt_sb &&
     nd->mnt->mnt_root == nd->dentry)
      goto unlock;
 
  err = -EINVAL;
  if (S_ISLNK(newmnt->mnt_root->d_inode->i_mode))
      goto unlock;

do_add_mount 函数第一部分是检查参数。

  • 第 1099 行(原文行号):检查挂载点目录本身是否为挂载点。如果挂载点目录本身已经被挂载了(这通过检查挂载点目录的 dentry 结构的 d_mounted 成员是否不为 0 实现的),说明挂载点目录已经被挂载,那么要调用 follow_down 函数来找到真正的 dentry 结构和 vfsmount 对象。follow_down 的查找过程后文还会分析到,此处略过。
  • 第 1107 行:检查同一个文件系统是否已经在挂载点目录挂载了,如果已经挂载,要返回错误。
  • 第 1112 行:检查源文件系统根 inode(根 dentry 的 inode)是否符号链接,如果是符号链接,则返回错误。
  newmnt->mnt_flags = mnt_flags;
  if ((err = graft_tree(newmnt, nd)))
      goto unlock;
  /*当前场景的fslist为NULL,跳过*/
  if (fslist) {
      /* add to the specified expiration list */
      spin_lock(&vfsmount_lock);
      list_add_tail(&newmnt->mnt_expire, fslist);
      spin_unlock(&vfsmount_lock);
  }
  up_write(&namespace_sem);
  return 0;

graft_tree 函数把 aufs 的 dentry 树和目的文件系统的 dentry 树嫁接到一起。

代码清单 2-25 graft_tree 函数

static int graft_tree(struct vfsmount *mnt, struct nameidata *nd)
{
  int err;
  if (mnt->mnt_sb->s_flags & MS_NOUSER)
      return -EINVAL;
 
  if (S_ISDIR(nd->dentry->d_inode->i_mode) !=
       S_ISDIR(mnt->mnt_root->d_inode->i_mode))
       return -ENOTDIR;
 
  err = -ENOENT;
  mutex_lock(&nd->dentry->d_inode->i_mutex);
  /*检查挂载点目录是否是废弃的目录*/
  if (IS_DEADDIR(nd->dentry->d_inode))
      goto out_unlock;
  ....../*省略无关代码*/
  err = -ENOENT;
  if (IS_ROOT(nd->dentry) || !d_unhashed(nd->dentry))
      err = attach_recursive_mnt(mnt, nd, NULL);
  • 第 858 行:判断源文件系统是否可以被挂载。Linux 系统的一些特殊的文件系统是不能被挂载的,比如 pipefs 文件系统、块设备文件系统(参见第 9 章)等。
  • 第 861 行:检查挂载点目录是否是一个目录文件,以及源文件系统的根 inode 是否目录文件。这两者都应该是目录才能执行挂载操作,如果不是目录则返回错误。
  • 第 875 行:检查挂载点是否有效。有效的条件是挂载点目录是一个根目录,或者挂载点目录被缓存在 dentry cache 中。这是因为文件系统根目录没有被缓存在 dentry cache 中,所以做这一步检查。检查通过,调用 attach_recursive_mnt 函数执行挂载操作。注意,此时 mnt 参数是 aufs 文件系统的 vfs_mount 对象,作为源文件系统;而 nd 参数保存了目的文件系统的 dentry 和 vfsmount 对象,作为目的点。

代码清单 2-26 attach_recursive_mnt 函数

static int attach_recursive_mnt(struct vfsmount *source_mnt,
           struct nameidata *nd, struct nameidata *parent_nd)
{
  LIST_HEAD(tree_list);
  struct vfsmount *dest_mnt = nd->mnt;
  struct dentry *dest_dentry = nd->dentry;
  struct vfsmount *child, *p;
  if (propagate_mnt(dest_mnt, dest_dentry, source_mnt, &tree_list))
      return -EINVAL;
  /*处理shared*/
  if (IS_MNT_SHARED(dest_mnt)) {
      for (p = source_mnt; p; p = next_mnt(p, source_mnt))
           set_mnt_shared(p);
  }
  spin_lock(&vfsmount_lock);
  /*当前场景parent_nd为空,跳过*/
  if (parent_nd) {
      detach_mnt(source_mnt, parent_nd);
      attach_mnt(source_mnt, nd);
      touch_namespace(current->namespace);
  } else {
      mnt_set_mountpoint(dest_mnt, dest_dentry, source_mnt);
      commit_tree(source_mnt);
  }
  list_for_each_entry_safe(child, p, &tree_list, mnt_hash) {
      list_del_init(&child->mnt_hash);
      commit_tree(child);
  }
  spin_unlock(&vfsmount_lock);
  return 0;
}

mnt_set_mountpoint 函数里面,目的 dentry 的 d_mounted 要加 1,这是将来判断该 dentry 是否是为挂载点的依据。同时源文件系统 vfsmount 对象的 mnt_mountpoint 指向目的 dentry。

commit_tree 用来把源 vfsmount 提交到全局 hash 链表。

代码清单 2-27 commit_tree 函数

static void commit_tree(struct vfsmount *mnt)
{
  struct vfsmount *parent = mnt->mnt_parent;
  struct vfsmount *m;
  LIST_HEAD(head);
  struct namespace *n = parent->mnt_namespace;
 
  BUG_ON(parent == mnt);
 
  list_add_tail(&head, &mnt->mnt_list);
  list_for_each_entry(m, &head, mnt_list)
       m->mnt_namespace = n;
  list_splice(&head, n->list.prev);
  /*源vfsmount对象链接到mount hash表*/
  list_add_tail(&mnt->mnt_hash, mount_hashtable +
                hash(parent, mnt->mnt_mountpoint));
  /*源vfsmount对象链接到父vfsmount对象的链表*/
  list_add_tail(&mnt->mnt_child, &parent->mnt_mounts);
  touch_namespace(n);
}

commit_tree 从 202 行到 205 行目的是把源文件系统的 vfsmount 对象链接到 namespace 的链表尾部。链接之前,第 204 行设置 vfsmount 对象的 namespace 为父对象的 namespace。

执行到最后,mount 实际上把 aufs 的 vfsmount 对象链接到一个全局 hash 表,同时也链接到父 vfsmount 对象的链表头。而目的 dentry 指向的就是目的文件系统的 / 目录。这个 dentry 结构的 d_mounted 成员要加 1,这是判断这个目录是否是挂载点的依据。在文件打开的过程需要判断目录是否挂载点,检查的依据就是这个参数。

代码分析到现在,已经大量使用了双向链表 list 和 hash list。理解这些基础的数据结构是阅读代码的基础条件。


2.3.4 文件打开的代码分析

通过前面代码的工作,源文件系统的 vfsmount 对象已经链接到目的文件系统的 vfsmount 对象,同时也链接到全局的 hash 表。

1. sys_open 函数

打开一个文件,是通过内核提供的系统调用 sys_open 实现的。

代码清单 2-28 sys_open 函数

asmlinkage long sys_open(const char __user *filename, int flags, int mode)
{
  long ret;
  if (force_o_largefile())
      flags |= O_LARGEFILE;
  /*> [!example] **代码清单 2-29 `do_sys_open` 函数**
```c
long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{   
  char *tmp = getname(filename);
  int fd = PTR_ERR(tmp);
  if (!IS_ERR(tmp)) {
      fd = get_unused_fd();
      if (fd >= 0) {
          struct file *f = do_filp_open(dfd, tmp, flags, mode);
          if (IS_ERR(f)) {
              put_unused_fd(fd);
              fd = PTR_ERR(f);
          } else {
              fsnotify_open(f->f_dentry);
              /*安装文件指针到fd数组*/
              fd_install(fd, f);
          }
      }
  }
}

函数 do_sys_open 首先把文件名从用户态复制到内核,然后获得一个未使用的文件号,最后调用 do_filp_open 执行文件打开的过程.

2. do_filp_open 函数

代码清单 2-30 do_filp_open 函数

static struct file *do_filp_open(int dfd, 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++;
  error = open_namei(dfd, filename, namei_flags, mode, &nd);
  if (!error)
      return nameidata_to_filp(&nd, flags);
  return ERR_PTR(error);
}

do_filp_open 主要执行了两步:

  • 第一步open_namei,它的作用是沿着要打开文件名的整个路径,一层层解析路径,最后得到文件的 dentry 和 vfsmount 对象,保存到一个 nameidata 结构中.这个 nameidata 结构就是 open_namei 的输入参数 nd.
  • 第二步nameidata_to_filp 函数,它的作用是根据第一步获得的 nameidata 结构,初始化一个 file 对象.

3. open_namei 函数

我们首先从 open_namei 函数开始分析.

代码清单 2-31 open_namei 函数

int open_namei(int dfd, const char *pathname, int flag,
      int mode, struct nameidata *nd)
{
  int acc_mode, error;
  struct path path;
  struct dentry *dir;
  int count = 0;
 
  acc_mode = ACC_MODE(flag);
  /*检查写权限*/
  /* O_TRUNC implies we need access checks for write permission */
  if (flag & O_TRUNC)
      acc_mode |= MAY_WRITE;
  /* Allow the LSM permission hook to distinguish append 
     access from general write access. */
  if (flag & O_APPEND)
      acc_mode |= MAY_APPEND;

open_namei 函数第一部分是设置权限参数.如果打开文件时带有 O_TRUNC 标志,说明要修改文件的长度,对文件的操作模式要加上可写权限;如果打开文件时带有 O_APPEND 标志,对文件的操作模式要加上 MAY_APPEND 权限.MAY_APPEND 也可当做可写权限(MAY_WRITE)的一种,但是把它专门选出来,作为一个特殊的标识.

/*
 * The simplest case - just a plain lookup.
 */
if (!(flag & O_CREAT)) {
      error = path_lookup_open(dfd, pathname, lookup_flags(flag),
                     nd, flag);
      if (error)
          return error;
      goto ok;
}
 
/*
 * Create - we need to know the parent.
 */
error = path_lookup_create(dfd, pathname, LOOKUP_PARENT, nd, flag, mode);
if (error)
      return error;

open_namei 函数第二部分是两种打开文件的模式.打开文件的时候,如果文件不存在,可以为用户创建一个文件,这是通过文件的 O_CREAT 标志来控制的.如果不带有 O_CREAT 标志,不需要创建文件,那么调用 path_lookup_open 函数.如果带有 O_CREAT 标志,说明需要创建文件,则调用 path_lookup_create 函数(调用函数时带有 LOOKUP_PARENT 标志).

path_lookup_create 函数不处理最终目标文件,它只查找到文件所在目录就结束查找过程了,等函数返回后,再检查最终目标文件是否存在.

 /*
  * We have the parent and last component. First of all, check
  * that we are not asked to creat(2) an obvious directory - that
  * will not do.
  */
 error = -EISDIR;
    /*检查last_type和文件名*/  
 if (nd->last_type != LAST_NORM || nd->last.name[nd->last.len] != '\0')
      goto exit;
  /*在父dentry里面查找是否有nd->last名字的文件,没有则创建dentry对象
   *里面调用了cached_lookup,这个函数前面分析过了*/
  dir = nd->dentry;
  nd->flags &= ~LOOKUP_PARENT;
  mutex_lock(&dir->d_inode->i_mutex);
  path.dentry = lookup_hash(nd);
  path.mnt = nd->mnt;

open_namei 函数第三部分首先检查 path_lookup_create 函数的返回值.第一种情况是检查返回类型.返回类型有很多种,可以是 LAST_NORMLAST_DOTDOT 等.如果返回类型不等于 LAST_NORM,说明文件名字是点 . 或者点点 ..,则直接返回.

另外一种情况是文件名是一个目录,也不处理,直接返回.如果文件名是目录,那么文件名的最后一个字符是斜杠符 /,而普通文件的最后一个字符不可能是斜杠符,因此目录文件的长度比 nd 指示的文件名长度多出来一个字符,通过检查最后一个字符是否为空判断文件是否是目录.文件名和长度的处理在本节的 __link_path_walk 函数继续分析.

如果检查通过,返回的 nd 变量的 dentry 成员是文件所在目录的 dentry,调用 lookup_hash 查找目标文件的 dentry,结果分两种情况.一种是文件存在,可以找到,一种是文件不存在,这种情况要为文件创建一个 dentry 结构.lookup_hash 函数调用了 __lookup_hash 在 dentry cache 执行查找过程.__lookup_hash 函数在 aufs 文件系统一节已经分析过,此处略过.

do_last:
……/*省略无关代码*/
   /*d_inode为空,说明文件不存在,需要创建inode*/
  /* Negative dentry, just create the file */
  if (!path.dentry->d_inode) {
      if (!IS_POSIXACL(dir->d_inode))
          mode &= ~current->fs->umask;
      error = vfs_create(dir->d_inode, path.dentry, mode, nd);
      mutex_unlock(&dir->d_inode->i_mutex);
      dput(nd->dentry);
      nd->dentry = path.dentry;
      if (error)
          goto exit;
      /* Don't check for write permission, don't truncate */
      acc_mode = 0;
      flag &= ~O_TRUNC;
      goto ok;
  }
  /*
   * It already exists.
   */
  mutex_unlock(&dir->d_inode->i_mutex);
  audit_inode_update(path.dentry->d_inode);
  error = -EEXIST;
  if (flag & O_EXCL)
      goto exit_dput;
    /*检查是否一个mount点,如果是mount点需要切换到源mount点*/
  if (__follow_mount(&path)) {
      error = -ELOOP;
      if (flag & O_NOFOLLOW)
          goto exit_dput;
  }
  error = -ENOENT;
  if (!path.dentry->d_inode)
      goto exit_dput;
    /*是否一个符号链接,是则跳到do_link分支处理*/ 
  if (path.dentry->d_inode->i_op && path.dentry->d_inode->i_op->follow_link)
      goto do_link;
  path_to_nameidata(&path, nd);
  error = -EISDIR;
      /*如果是目录,出错退出*/
  if (path.dentry->d_inode && S_ISDIR(path.dentry->d_inode->i_mode))
      goto exit;
ok:
      /*最后统一的open处理*/
  error = may_open(nd, acc_mode, flag);

open_namei 函数(续)

第四部分:检查 dentry 并创建或处理文件

open_namei 函数第四部分首先检查 dentry 结构的 d_inode 成员.如果成员为空,说明文件不存在,dentry 是函数第三部分刚刚创建的,因此 d_inode 尚未赋值为空.这种情况下,调用 vfs_create 创建文件,然后进入 ok 分支返回.

如果成员不为空,说明文件已经存在,随后要检查文件为挂载点或者符号链接的情况.这两种情况在本节后面的 __link_path_walk 函数也要处理,在后面代码中一并分析.

do_link:
  /*如果是符号链接,继续处理,找到符号链接的目的文件*/
  error = __do_follow_link(&path, nd);
  ......
  if (nd->last.name[nd->last.len]) {
      __putname(nd->last.name);
      goto exit;
  }
  error = -ELOOP;
  if (count++==32) {
      __putname(nd->last.name);
      goto exit;
  }
  dir = nd->dentry;
  mutex_lock(&dir->d_inode->i_mutex);
  path.dentry = lookup_hash(nd);
  path.mnt = nd->mnt;
  __putname(nd->last.name);
  goto do_last;
}

第五部分:符号链接处理

open_namei 函数第五部分是进行符号链接的处理.调用 __do_follow_link 为符号链接文件找到它的真实文件,然后再执行真实文件的查找过程.因为符号链接可以一层层链接,造成无限的循环,所以需要设置一个变量 count 计算符号链接的递归次数.超过设定值 32 以上的,就不再解析,而是返回错误 -ELOOP.


4. path_lookup_create_path_lookup_intent_open 函数

path_lookup_create 函数顾名思义,它是沿着文件的整个路径寻找.文件的路径是包含斜杠符 "/" 的一串字符,path_lookup_create 要对字符进行分割,分离出每层路径的目录名和最终的文件名,然后对目录和最终的文件进行查找.

path_lookup_openpath_lookup_create 都封装了 __path_lookup_intent_open 函数,不同之处只是 path_lookup_create 的参数带有 LOOKUP_CREATE 标志,所以只分析 __path_lookup_intent_open 函数就可以了,如代码清单 2-32 所示.

代码清单 2-32 __path_lookup_intent_open 函数

static int __path_lookup_intent_open(int dfd, const char *name,
      unsigned int lookup_flags, struct nameidata *nd,
      int open_flags, int create_mode)
{
  /*创建一个新的 filp 对象*/
  struct file *filp = get_empty_filp();
  int err;
  if (filp == NULL)
      return -ENFILE;
  nd->intent.open.file = filp;
  nd->intent.open.flags = open_flags;
  nd->intent.open.create_mode = create_mode;
  err = do_path_lookup(dfd, name, lookup_flags|LOOKUP_OPEN, nd);
  // ... 后续处理
}

5. do_path_lookup 函数

__path_lookup_intent_open 函数给 nd 参数赋值之后,调用 do_path_lookup 执行路径查找工作,它的代码如代码清单 2-33 所示.

代码清单 2-33 do_path_lookup 函数

/* Returns 0 and nd will be valid on success; Returns error, otherwise */
static int fastcall do_path_lookup(int dfd, const char *name,
               unsigned int flags, struct nameidata *nd)
{
  // ... 省略参数定义代码
  nd->last_type = LAST_ROOT; /* if there are only slashes... */
  nd->flags = flags;
  nd->depth = 0;
  if (*name=='/') {
      read_lock(&current->fs->lock);
      if (current->fs->altroot && !(nd->flags & LOOKUP_NOALT))
      {
          /**/
           nd->mnt = mntget(current->fs->altrootmnt);
           nd->dentry = dget(current->fs->altroot);
           read_unlock(&current->fs->lock);
           if (__emul_lookup_dentry(name,nd))
               goto out; /* found in altroot */
           read_lock(&current->fs->lock);
      }
      /*查找的 dentry 对象和 vfsmount 对象是文件系统的 root dentry 和 root vfsmount */
      nd->mnt = mntget(current->fs->rootmnt);
      nd->dentry = dget(current->fs->root);
      read_unlock(&current->fs->lock);

第一部分:检查文件名是否以斜杠开头

do_path_lookup 函数的第一部分是检查文件名字是否用斜杆符 "/" 开始.以斜杠符开始,说明文件的查找是从根目录开始,那么起始的 dentry(也就是 nd 变量的 dentry)要设置为当前进程文件系统的根 dentry,nd 变量的 vfsmount 对象要设置为当前进程文件系统的根 vfsmount 对象.当前进程文件系统是一个和进程有关的概念,每个进程初始化的时候,都要为它设置当前文件系统.当前文件系统包含了三个 dentry,它们分别指向根 dentry、当前 dentry(即 pwd 命令显示的当前目录)和替换根 dentry.

如果当前进程文件系统存在替换根 dentry 且打开文件的时候不设置 LOOKUP_NOALT 标志,那么 nd 变量的 dentry 要设置为当前进程文件系统的替换根 dentry,nd 变量的 vfsmount 对象要设置为当前进程文件系统的替换根 vfsmount 对象.

} else if (dfd == AT_FDCWD) {
       /*如果是 AT_FDCWD,说明要在进程的当前路径查找文件,那么 dentry 就是当前目录 */
       read_lock(&current->fs->lock);
       nd->mnt = mntget(current->fs->pwdmnt);
       nd->dentry = dget(current->fs->pwd);
       read_unlock(&current->fs->lock);

第二部分:检查 AT_FDCWD 标志

do_path_lookup 第二部分检查 AT_FDCWD 标志.如果文件名不是用斜杠符开始而且设置了 AT_FDCWD 标志,意味着文件的搜索路径不是从进程文件系统的根路径开始,而是从进程当前路径开始.所以设置 nd 变量的 dentry 为进程文件系统的当前 dentry,nd 变量的 vfsmount 对象要设置为进程文件系统的当前 vfsmount 对象.

} else {
      struct dentry *dentry;
      file = fget_light(dfd, &fput_needed);
      retval = -EBADF;
      if (!file)
          goto out_fail;
      dentry = file->f_dentry;
      ……
      nd->mnt = mntget(file->f_vfsmnt);
      nd->dentry = dget(dentry);
      fput_light(file, fput_needed);
  }
  current->total_link_count = 0;
  retval = link_path_walk(name, nd);
  ……
}

第三部分:通过已打开文件描述符查找

do_path_lookup 第三部分的前提是前两个部分的条件都不成立,这个文件已经打开过,输入参数是文件的 ID 号.这种情况是在进程的已打开文件结构里面根据用户态的 ID 号查找文件.这种场景不是我们研究的情况,此处略过.


nd 参数设置了正确的 dentry 和 vfsmount 对象后,调用 link_path_walk 执行路径的查找.link_path_walk 执行了两次查找的过程,如代码清单 2-34 所示.

代码清单 2-34 link_path_walk 函数

int fastcall link_path_walk(const char *name, struct nameidata *nd)
{
  struct nameidata save = *nd;
  int result;
  /* make sure the stuff we saved doesn't go away */
  /*增加 mnt 和 dentry 的引用计数*/
  dget(save.dentry);
  mntget(save.mnt);
  result = __link_path_walk(name, nd);
  if (result == -ESTALE) {
      *nd = save;
      dget(nd->dentry);
      mntget(nd->mnt);
      nd->flags |= LOOKUP_REVAL;
      result = __link_path_walk(name, nd);
  }
  dput(save.dentry);
  mntput(save.mnt);
  return result;
}

link_path_walk 两次执行了 __link_path_walk 函数.原因是第一次查找有可能失败,文件系统返回了 -ESTALE 失败标识码.这种情况下 ndflag 成员要加上 LOOKUP_REVAL 标志,作用是不再依赖 dentry cache,而是强迫文件系统执行自己的查找功能.对于硬盘文件系统,文件系统自己的查找功能通常要读硬盘上文件系统的元数据来获取文件信息.


__link_path_walk 真正对文件的整个路径名做循环查找,需要一层层解析文件的路径,对每层路径进行查询,如代码清单 2-35 所示.

代码清单 2-35 __link_path_walk 函数

static fastcall int __link_path_walk(const char * name, struct nameidata *nd)
{
  struct path next;
  struct inode *inode;
  int err;
  unsigned int lookup_flags = nd->flags;
  
  while (*name=='/')
      name++;
  if (!*name)
      goto return_reval;
  inode = nd->dentry->d_inode;
  if (nd->depth)
      lookup_flags = LOOKUP_FOLLOW | (nd->flags & LOOKUP_CONTINUE);

__link_path_walk 函数非常复杂,我们把它分为多个部分,逐一讲解.

第一部分:去除前导斜杠和检查符号链接深度

第一部分是将文件名字符串的最前面的斜杠符去掉.因为斜杠符可能是多个,所以有一个 while 循环.然后检查文件符号链接的深度.因为文件可以是一个符号链接,符号链接又可以指向一个符号链接,如此递归可能造成死循环.每次处理符号链接的时候,结构 nddepth 成员加一,如果超过一个限值,就不再处理了,避免无限的符号链接.

/* At this point we know we have a real path component. */
/*这个循环遍历名字字符的每一轮。就是以 "/" 字符分隔的每一层字符*/
for(;;) {
    unsigned long hash;
    struct qstr this;
    unsigned int c;
    nd->flags |= LOOKUP_CONTINUE;
    /*权限检查*/
    err = exec_permission_lite(inode, nd);
    if (err == -EAGAIN)
        err = vfs_permission(nd, MAY_EXEC);
    if (err)
        break;
        
    /*这里计算 name 的 hash 值,如果碰到 "/" 字符,代表这一轮的名字到了结束位 */
    this.name = name;
    c = *(const unsigned char *)name;
    hash = init_name_hash();
    do {
        name++;
        hash = partial_name_hash(c, hash);
        c = *(const unsigned char *)name;
    } while (c && (c != '/'));
    this.len = name - (const char *) this.name;
    this.hash = end_name_hash(hash);
    /* remove trailing slashes? */
    /*已经是最后一轮的名字字符了,转到 last_component 处理*/
    if (!c)
        goto last_component;
    /*最后的字符是个 "/",转到 last_with_slashes 处理*/
    while (*++name == '/');
    if (!*name)
        goto last_with_slashes;

第二部分:分离路径组件

__link_path_walk 函数第二部分是一个循环,作用是将文件名字符串分离.文件名字符串是一个长的路径,每一层目录之间用斜杠符分隔.这部分代码逐个比较字符,如果碰到了斜杠符,意味着斜杠符之前的字符串是一个目录名.对目录名计算 hash 值,之后进行目录名的查找工作.计算 hash 值的过程在 aufs 文件系统的例子中已经分析过,不再赘述.如果目录名查找成功,则进入下一轮循环.

循环最终有两种情况,一种情况是得到了最终的目标文件名,转入 last_component 分支处理,另一种是整个文件名字符用的是一个斜杠符结尾的,则转入 last_with_slashes 分支处理.这两个分支的名字其实就说明了它们各自的功能.

/*
 * "." and ".." are special - ".." especially so because it has
 * to be able to know about the current root directory and
 * parent relationships.
 */
/*如果名字是 "." 或 "..",要特殊处理*/
if (this.name[0] == '.') switch (this.len) {
    default:
        break;
    case 2:        
        if (this.name[1] != '.')
            break;
        follow_dotdot(nd);
        inode = nd->dentry->d_inode;
        /* fallthrough */
    case 1:
        continue;
}
/*如果文件系统提供了自己的 hash 函数,则使用它计算 hash 值*/
if (nd->dentry->d_op && nd->dentry->d_op->d_hash) {
    err = nd->dentry->d_op->d_hash(nd->dentry, &this);
    if (err < 0)
        break;
}

第三部分:处理 ”.” 和 ”..”

__link_path_walk 函数第三部分是处理文件名中特殊的点(".")和点点("..")字符.文件名是一个点代表自身,文件名是两个点代表上一级目录.所以文件名是一个点的时候什么也不做,直接进入下一级循环,如果文件名是两个点,则调用 follow_dotdot 寻找当前文件的上一级目录.

/* This does the actual lookups.. */
err = do_lookup(nd, &this, &next);
if (err)
    break;
err = -ENOENT;
/*d_inode 是所查找到文件的 inode,如果为空,说明查找失败*/
inode = next.dentry->d_inode;
if (!inode)
    goto out_dput;
err = -ENOTDIR; 
if (!inode->i_op)
    goto out_dput;
/*inode 有 follow_link,说明是个符号链接,要特殊处理,否则执行 path_to_nameidata */
if (inode->i_op->follow_link) {
    err = do_follow_link(&next, nd);
    if (err)
        goto return_err;
    err = -ENOENT;
    inode = nd->dentry->d_inode;
    if (!inode)
        break;
    err = -ENOTDIR; 
    if (!inode->i_op)
        break;
} else
    path_to_nameidata(&next, nd);
err = -ENOTDIR; 
if (!inode->i_op->lookup)
    break;
continue;
/* here ends the main loop */

第四部分:实际查找与符号链接处理

__link_path_walk 函数第四部分调用 do_lookup 函数执行真正的查找,查找的结果通过一个 path 结构变量 next 返回.

do_lookup 执行的是最终目标文件的目录的查找,最终的目标文件不是它查找,而是在 last_component 分支中执行的.所以如果 inode 不具备 i_op 成员(目录必须有该成员),说明该 inode 不是一个目录,则返回 -ENOTDIR 错误.这个错误的英文名字就说明了错误原因.

如果 i_op 成员具备 follow_link 成员,说明 inode 是一个符号链接.符号链接必须调用 do_follow_link 找到符号链接真正指向的路径.

last_with_slashes:
      /*最后是个 "/",说明是个目录,设置标志*/
      lookup_flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
last_component:
      /*已经沿
 

第五部分:last_with_slasheslast_component 分支

/*最后的文件名字是 “.”或者 “..”,要特殊处理*/ 
      if (this.name[0] == '.') switch (this.len) {
          default:
              break;
          case 2:        
              if (this.name[1] != '.')
                  break;
              follow_dotdot(nd);
              inode = nd->dentry->d_inode;
                  /* fallthrough */
          case 1:
              goto return_reval;
      }
      if (nd->dentry->d_op && nd->dentry->d_op->d_hash) {
          err = nd->dentry->d_op->d_hash(nd->dentry, &this);
          if (err < 0)
              break;
      }
      /*查找最终的文件*/ 
      err = do_lookup(nd, &this, &next);
      if (err)
          break;
      inode = next.dentry->d_inode;
      /*最终文件是个符号链接,要特殊处理*/
      if ((lookup_flags & LOOKUP_FOLLOW)
          && inode && inode->i_op && inode->i_op->follow_link)
           err = do_follow_link(&next, nd);
           if (err)
               goto return_err;
           inode = nd->dentry->d_inode;
      } else
          path_to_nameidata(&next, nd);
      err = -ENOENT;
      if (!inode)
          break;
      /*带有LOOKUP_DIRECTORY的标志,说明打开的是目录。非我们研究的情况
      if (lookup_flags & LOOKUP_DIRECTORY) {
          err = -ENOTDIR; 
          if (!inode->i_op || !inode->i_op->lookup)
              break;
      }
      goto return_base;

__link_path_walk 函数的第五部分是 last_with_slashes 分支和 last_component 分支.对于 last_with_slashes 分支,要加上一个 LOOKUP_DIRECTORY 标志,意味着最终查找的是一个目录.

如果参数带有 LOOKUP_PARENT 标志,进入 lookup_parent 分支处理,而不在 last_component 分支处理.这个标志用于目标文件有可能不存在需要创建的时候.这说明 last_component 只处理最终目标文件存在的情况.如果有可能不存在,则需要 lookup_parent 分支处理.

last_component 分支同样要处理文件名是“点”和“点点”的情况,这种情况在前面已经分析过.

对最终的目标文件,last_component 分支同样调用 do_lookup 执行最终目标文件的查找,对查找的结果,也要处理符号链接的情况.这种情况和第四部分重合.

第六部分:lookup_parent 分支

lookup_parent:
       /*last成员返回最终目标文件的名字和长度*/
       nd->last = this;
       nd->last_type = LAST_NORM;
       if (this.name[0] != '.')
           goto return_base;
       if (this.len == 1)
           nd->last_type = LAST_DOT;
       else if (this.len == 2 && this.name[1] == '.')
           nd->last_type = LAST_DOTDOT;
       else
           goto return_base;

__link_path_walk 函数的第六部分是 lookup_parent 分支,这个分支专门处理最终目标文件有可能不存在的场景.这个分支设置 nd 的返回类型为 LAST_NORM 就直接返回了,并没有真正去执行查找.这种情况最终目标文件的查找是在 open_namei 函数执行的,前面已经分析过.如果最终的目标文件名是点(“.”)或者点点(“..”),返回的 last_type 要表明是 LAST_DOT 或者 LAST_DOTDOT.


__link_path_walk 函数很复杂,为了更清晰的理解,我们借助一个例子来分析.假设把 aufs 文件系统挂载到了 /home/mnt/au,我们要打开的文件是 home/mnt/au/woman star/lbb.具体步骤如下:

  • 步骤 1:首先是根据分隔号得到 home 目录,然后计算 home 的 hash 值,调用 do_lookup.
  • 步骤 2:这样就获得了 home 文件(目录文件)的 inode,因为 homeinodefollow_link 调用,最终调用 path_to_nameidata.
  • 步骤 3:对 mnt 目录用步骤 1 和步骤 2 查找.
  • 步骤 4:查找 au.因为 au 是个挂载点,在 do_lookup 函数需要根据两个文件系统的挂载点解析,解析后,父目录就换成了 aufs 的根目录.这部分下一节再分析.
  • 步骤 5:对 woman star 目录用步骤 1 和步骤 2 查找.
  • 步骤 6:最后的目标文件 lbblast_component 分支处理.在 woman star 目录可以找到.

通过例子,应该可以清楚理解 __link_path_walk 函数的处理流程.对于路径名中间出现的点“.”或者点点“..”,以及符号链接的处理,读者可以自行分析一下.


do_lookup 函数

do_lookup 函数不仅执行最终目标文件的查找,还要处理挂载点,它的代码如代码清单 2-36 所示.

代码清单 2-36 do_lookup 函数

static int do_lookup(struct nameidata *nd, struct qstr *name,
           struct path *path)
{
  struct vfsmount *mnt = nd->mnt;
  struct dentry *dentry = __d_lookup(nd->dentry, name);
  if (!dentry)
      goto need_lookup;
  if (dentry->d_op && dentry->d_op->d_revalidate)
      goto need_revalidate;

do_lookup 函数第一部分以 nddentry 为父目录,调用 __d_lookup 函数查找 name 所代表文件的 dentry.name 代表的文件既可能是一个普通文件,也可能是一个目录文件.

do_lookup 函数第二部分是两个分支:一个是 done 分支,另一个是 need_lookup 分支.

done:
  path->mnt = mnt;
  path->dentry = dentry;
  __follow_mount(path);
  return 0;
 
need_lookup:
  /*cache找不到,需要真正查找。这是文件系统提供的lookup调用*/
  dentry = real_lookup(nd->dentry, name, nd);
  if (IS_ERR(dentry))
      goto fail;
  goto done;

如果第一部分的查找成功了,则进入 done 分支设置 vfsmount 对象和 dentry,然后检查 dentry 是否是一个挂载点.如果查找未成功,则进入 need_lookup 分支.第一部分的查找是在 dentry cache 里面进行,如果进入 need_lookup 分支,说明在 dentry cache 中找不到指定名字文件的 dentry.对于建立在硬盘之上的文件系统,这时候要调用文件系统提供的 lookup 函数在硬盘上搜索文件.这部分代码分析深入的话,就涉及块设备读写和文件的读写,后文再分析.对于我们的情况,不需要调用文件系统的 lookup 函数.完成文件系统的 lookup 之后,仍然需要进入 done 分支,处理挂载点.


__follow_mount 函数

挂载点的处理需要调用 __follow_mount 函数,找到挂载点真正的 dentry 结构,如代码清单 2-37 所示.

代码清单 2-37 __follow_mount 函数

static int __follow_mount(struct path *path)
{
  int res = 0;
  while (d_mountpoint(path->dentry)) {
      /*查找挂载的对象*/
      struct vfsmount *mounted = lookup_mnt(path->mnt, path->dentry);
      if (!mounted)
          break;
      dput(path->dentry);
      if (res)
          mntput(path->mnt);
      /*找到挂载点,更换dentry为挂载点的root dentry*/
      path->mnt = mounted;
      path->dentry = dget(mounted->mnt_root);
      res = 1;
  }
  return res;
}

d_mountpoint 函数用于判断该 dentry 是否是挂载点,也就是判断 d_mounted 参数是否为 0.回顾 mount 系统调用:当一个文件系统挂载的时候,这个参数要加 1,所以如果该文件的 dentry 被一个文件系统挂载了,这个参数不为 0.

lookup_mnt 是遍历系统的 mount 链表,找到挂载点,然后更换 dentry.还是用例子来解释:因为 au 是个挂载点,所以 lookup_mnt 的参数就是 auvfsmount 对象和 dentry.aufs 文件系统挂载时,已经把文件系统的 vfsmount 对象链接到 mount 链表,lookup_mnt 找到的结果就是 aufs 文件系统的 vfsmount 对象.那么 pathdentry 就换成了 aufs 文件系统的 root dentry.当 do_lookup 函数返回后,下一轮要寻找 woman star 目录,实际是在 aufs 的根目录里面查找.

现在总结 open_namei 的整个处理过程:经过层层解析,open_namei 函数最终结果得到了文件的 dentryinode 结构,以及 vfsmount 对象.


nameidata_to_filp 函数

从当前代码返回 do_filp_open 函数.open_namei 之后,要调用 nameidata_to_filp 函数,实现打开文件的最后一步,获得文件结构,它的代码如代码清单 2-38 所示.

代码清单 2-38 nameidata_to_filp 函数

struct file *nameidata_to_filp(struct nameidata *nd, int flags)
{
  struct file *filp;
  /* Pick up the filp from the open intent */
  filp = nd->intent.open.file;
  /* Has the filesystem initialised the file for us? */
     /*如果文件系统没有初始化f_dentry*/
  if (filp->f_dentry == NULL)
      filp = __dentry_open(nd->dentry, nd->mnt, flags, filp, NULL);
  else
      path_release(nd);
  return filp;
}

__dentry_open 函数

文件结构 filp 已经在 open_namei 的过程中创建了,只不过它还没完成初始化.对它初始化通过 __dentry_open 函数执行,初始化过程要对文件打开时设置的选项进行处理,如代码清单 2-39 所示.

代码清单 2-39 __dentry_open 函数

static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt,
                  int flags, struct file *f,
                  int (*open)(struct inode *, struct file *))
{
  struct inode *inode;
  int error;
  f->f_flags = flags;
  f->f_mode = ((flags+1) & O_ACCMODE) | FMODE_LSEEK |
                FMODE_PREAD | FMODE_PWRITE;
  inode = dentry->d_inode;
  /*如果允许写文件,检查写权限*/
  if (f->f_mode & FMODE_WRITE) {
      error = get_write_access(inode);
      if (error)
          goto cleanup_file;
  }
  /*给文件的参数赋值*/
  f->f_mapping = inode->i_mapping;
  f->f_dentry = dentry;
  f->f_vfsmnt = mnt;
  f->f_pos = 0;
  f->f_op = fops_get(inode->i_fop);
  file_move(f, &inode->i_sb->s_files);

__dentry_open 函数第一部分主要是初始化文件的参数.文件的操作函数 f_opinode 获得.f_mapping 是文件的读写 cache 的管理结构,同样从 inode 获得.函数 file_move 把文件加入超级块对象的文件链表,这样从超级块可以遍历文件系统内所有的文件结构.

if (!open && f->f_op)
    open = f->f_op->open;
if (open) {
    error = open(inode, f);
    if (error)
        goto cleanup_all;
}
f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
/*初始化文件的预读参数*/
file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
/* NB: we're sure to have correct a_ops only after f_op->open */
/*如果文件带有O_DIRECT标志,检查direct I/O的函数调用*/ 
if (f->f_flags & O_DIRECT) {
    if (!f->f_mapping->a_ops ||
        ((!f->f_mapping->a_ops->direct_IO) &&
        (!f->f_mapping->a_ops->get_xip_page))) {
        fput(f);
        f = ERR_PTR(-EINVAL);
    }
}

__dentry_open 函数第二部分首先检查文件系统是否为文件定义了 open 函数,如果已经定义,那么随后执行文件的 open 函数.然后函数 file_ra_state_init 初始化文件预读的参数.文件预读相关的内容在第 10 章.最后是处理 O_DIRECT 模式,这是通过 direct I/O 方式访问文件,不经过文件的 page cache,在第 10 章读写文件的流程将看到它的作用.


2.4 本章小结

本章通过一个简单的文件系统,分析了文件系统挂载、文件和目录的创建,以及文件打开的过程.通过这些分析,读者对文件系统的概念、超级块、inodedentry 的概念,以及架构应该有比较深入的理解.借助这些知识,完全可以分析文件关闭的过程,或者 chmodustatutimetruncate 等文件系统调用的实现.


[图片上下文]:此部分包含图片引用:Image 198 (Page 87), Image 201 (Page 88), Image 208 (Page 91), Image 211 (Page 92), Image 216 (Page 94), Image 221 (Page 96), Image 88 (Page 97), Image 227 (Page 98).这些图片在原书中对应位置展示了相关数据结构或流程,此处无法直接呈现.