内核OverlayFS—注册与挂载

内核版本为4.4.68

这篇文章通过内核源码分析OverlayFS的注册与挂载过程

假设你对OverlayFS的层次关系已经有一个基本的了解。如果对OverlayFS的层以及联合挂载没有概念,建议先补充下相关知识,因为挂载OverlayFS时会使用到层

分析会尽可能避开公有部分(每种文件系统注册和挂载过程中都会涉及的内容),而更多的关注OverlayFS独有部分


注册

OverlayFS在内核中以内核模块的形式存在,假设你已经知道在内核模块加载时,会涉及到两个关键函数——入口和出口函数。他们通过如下代码指明:

1
2
module_init(ovl_init);//入口函数——ovl_init
module_exit(ovl_exit);//出口函数——ovl_exit

当使用命令

overlay```加载overlayfs时,会调用入口函数ovl_init:
1
2
3
4
5
6

```c
static int __init ovl_init(void)
{
return register_filesystem(&ovl_fs_type);
}

因此,当加载overlay模块时,模块入口函数调用register_filesystem函数注册overlayfs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int register_filesystem(struct file_system_type * fs)
{
int res = 0;
struct file_system_type ** p;

BUG_ON(strchr(fs->name, '.'));
if (fs->next)//说明已在文件系统类型链表
return -EBUSY;
write_lock(&file_systems_lock);
//根据文件系统名查找文件系统类型链表
p = find_filesystem(fs->name, strlen(fs->name));
if (*p)
res = -EBUSY;
else
*p = fs;
write_unlock(&file_systems_lock);
return res;
}

EXPORT_SYMBOL(register_filesystem);

注册一个文件系统需要用到这个文件系统的类型结构——file_system_type。这个结构中定义了文件系统的名称,挂载函数等信息

注册函数主要完成以下工作:

  • 首先通过next字段检查文件系统是否已经在文件系统类型链表中,如果存在,则直接返回。
  • 否则调用find_filesystem()遍历由变量file_systems指向的全局文件系统类型链表,查看是否存在已经注册过名为overlay的文件系统,有则返回,否则返回链表结尾的地址。
  • 最后,如果没找到,则将这个文件系统链接到链表结尾

下图展示了文件系统类型链表:

这里提一下,上图中fs_supers字段的类型在内核4.4.68中换成了struct hlist_head,super_block的s_instance字段的类型换成了hlist_node

每种文件系统的fs_supers字段指向对应的已挂载文件系统实例。假设系统还未注册OverlayFS,因此会将OverlayFS的file_system_type添加到文件系统类型链表结尾

不直接遍历全局链表查找,而是先查看fs的next指针是否为NULL的原因在于遍历链表十分耗时

总结起来,OverlayFS的注册是在overlay模块的插入中完成,而注册即是将代表OverlayFS的file_system_type添加到全局文件系统类型链表中,因此实际上是一个通用文件系统注册的步骤,和其它文件系统的注册没有什么区别


挂载

每当挂载一个OverlayFS时,比如下面的命令将lower和upper目录,使用overlayfs挂载到merged目录:

1
sudo mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work merged

我的磁盘划了3个分区,3个分区的文件系统都是ext4,分别挂载在/、/home、/boot目录下:

整个dentry树大概是这个样子:

OverlayFS的文件系统类型变量ovl_fs_type中指明了挂载函数:

1
2
3
4
5
6
static struct file_system_type ovl_fs_type = {
.owner = THIS_MODULE,
.name = "overlay",
.mount = ovl_mount,//挂载函数
.kill_sb = kill_anon_super,
};

mount字段指明了挂载函数为ovl_mount,它是函数mount_nodev的封装,在“注册”一节文件系统类型链表的图中可以看到,每个文件系统的fs_supers字段指向已挂载文件系统实例的链表。因此,挂载一个OverlayFS实例,就是要分配并初始化一个超级块,链接到这个链表中,也就是注册函数所要完成的工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static struct dentry *ovl_mount(struct file_system_type *fs_type, int flags,
const char *dev_name, void *raw_data)
{
return mount_nodev(fs_type, flags, raw_data, ovl_fill_super);
}

/* fs/super.c */
struct dentry *mount_nodev(struct file_system_type *fs_type,
int flags, void *data,
int (*fill_super)(struct super_block *, void *, int))
{
int error;
//1.创建一个superblock,新创建的这个superblock已经链接到全局superblock链表中
struct super_block *s = sget(fs_type, NULL, set_anon_super, flags, NULL);
...
//2.填充superblock
error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
...
}
EXPORT_SYMBOL(mount_nodev);

主要分为2步:

  • 调用sget()函数,该函数搜索fs_supers字段指向的由超级块组成的已挂载文件系统实例链表,如果找到一个与块设备相关的超级块,则返回它的地址,否则分配并初始化一个新的超级块,并插入到OverlayFS已挂载文件系统实例链表和全局已挂载文件系统实例链表中
  • 调用函数fill_super()填充这个super_block,使其包含OverlayFS的特有信息

sget()函数完成了super_block的分配和插入链表,这是一个文件系统挂载的通用步骤。已分配的super_block还需要填充更多OverlayFS独有的信息,这也是本文主要关注的部分

OverlayFS挂载信息

在进一步介绍如何填充新分配的super_block之前,要介绍一个结构体ovl_fs。这个结构体是OverlayFS的特有信息,它记录了OverlayFS upper层和lower层的文件系统、文件名以及work目录的dentry这些挂载信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ovl_fs {
struct vfsmount *upper_mnt;
unsigned numlower;
struct vfsmount **lower_mnt;
struct dentry *workdir;
long lower_namelen;
/* pathnames of lower and upper dirs, for show_options */
struct ovl_config config;
};

struct ovl_config {
char *lowerdir;
char *upperdir;
char *workdir;
};

upper_mnt字段描述了upper层文件系统,lower_mnt字段描述了lower层文件系统,workdir为work目录的dentry,结构体ovl_config记录了upper、lower、以及work目录的字符串信息

填充super_block

在填充super_block时,会分配一个ovl_fs类型的变量,并设置super_block的s_fs_info字段指向这个变量,从而可以访问到挂载信息。填充super_block由函数ovl_fill_super完成,主要包括下列工作:

  1. 分配一个ovl_fs结构ufs,调用函数ovl_parse_opt()对挂载命令进行解析,得到upper、lower、work目录的字符串,存入ufs的config字段

    1
    2
    3
    4
    5
    6
    7
    ...
    struct ovl_fs *ufs;//存储overlayfs挂载信息
    ...
    ufs = kzalloc(sizeof(struct ovl_fs), GFP_KERNEL);
    ...
    err = ovl_parse_opt((char *) data, &ufs->config);
    ...
  2. 调用ovl_mount_dir()函数根据upper目录和work目录的字符串对upper层和work目录进行解析。该函数最终会调用kern_path()执行路径解析,得到upper层和work目录的路径,保存在传入ovl_mount_dir()函数的参数upperpath以及workpath中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct path upperpath = { NULL, NULL };
    struct path workpath = { NULL, NULL };
    ...
    if (ufs->config.upperdir) {
    ...
    //对upper层进行路径解析,得到路径upperpath
    err = ovl_mount_dir(ufs->config.upperdir, &upperpath);
    ...
    //对work目录进行路径解析,得到路径workpath
    err = ovl_mount_dir(ufs->config.workdir, &workpath);
    //有效性检查
    ...
    }
    ...

    这个if条件中,在解析完两个路径后,会进行有效性检查,要求upper目录与work目录必须位于相同文件系统下,同时要求两个目录不能再同一子树中。

  3. 对记录lower层信息的字符串进行解析,分析出lower层数,循环调用函数ovl_lower_dir()对lower层进行解析,该函数最终也会调用kern_path()执行路径解析,得到每个lower目录的路径,保存在lower路径栈stack中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    ...
    struct path *stack = NULL;
    char *lowertmp;
    char *lower;
    unsigned int numlower;
    unsigned int stacklen = 0;
    ...
    //复制lower层信息字符串
    lowertmp = kstrdup(ufs->config.lowerdir, GFP_KERNEL);
    ...
    //解析字符串,得到lower层数目
    stacklen = ovl_split_lowerdirs(lowertmp);
    ...
    //为lower路径栈分配空间
    stack = kcalloc(stacklen, sizeof(struct path), GFP_KERNEL);
    ...
    lower = lowertmp;//此时的lowertmp的形式为:lowerdir1\0lowerdir2\0lowerdir3\0...
    //循环获取每个lower层的路径
    for (numlower = 0; numlower < stacklen; numlower++) {
    err = ovl_lower_dir(lower, &stack[numlower],
    &ufs->lower_namelen, &sb->s_stack_depth,
    &remote);
    if (err)
    goto out_put_lowerpath;

    lower = strchr(lower, '\0') + 1;//接着处理下一个lowerdir
    }
    //循环结束后,numlower等于stacklen,记录了lower层数
  1. 调用函数clone_private_mount()根据到的upper层路径,lower层路径克隆得到一个私有的挂载点,调用函数ovl_workdir_create()在work目录下创建一个名为“work”的目录,填充ufs的upper_mnt、workdir、lower_mnt和numlower字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    ...
    //处理upper层和work目录
    if (ufs->config.upperdir) {
    ufs->upper_mnt = clone_private_mount(&upperpath);
    ...
    ufs->workdir = ovl_workdir_create(ufs->upper_mnt, workpath.dentry);
    ...
    }
    ...
    //处理lower层
    ufs->lower_mnt = kcalloc(numlower, sizeof(struct vfsmount *), GFP_KERNEL);
    ...
    for (i = 0; i < numlower; i++) {
    struct vfsmount *mnt = clone_private_mount(&stack[i]);
    ...
    mnt->mnt_flags |= MNT_READONLY;//设置lower层为只读
    ufs->lower_mnt[ufs->numlower] = mnt;
    ufs->numlower++;//lower层数
    }
    ...

    私有挂载点不会附着到命名空间中的任何位置,对原挂载点的改变不会传播到私有挂载点

  2. 填充super_block的s_d_op字段,这个字段是一个struct dentry_operations类型的指针,它描述了OverlayFS的dentry操作,每当在OverlayFS中分配一个dentry时,会使用s_d_op字段赋值dentry的d_op字段

    1
    2
    3
    4
    5
    6
    ...
    if (remote)
    sb->s_d_op = &ovl_reval_dentry_operations;
    else
    sb->s_d_op = &ovl_dentry_operations;
    ...
  1. 为struct ovl_entry类型的变量oe分配空间(这个变量记录OverlayFS根目录的层次信息,由根目录dentry的d_fsdata字段指向)。为OverlayFS根目录分配inode,创建根目录的dentry。通过前面得到的upper层和lower层的路径填充oe,然后设置根目录dentry的d_fsdata字段指向oe。最后调用函数ovl_copyattr()将upper目录inode的i_uid、i_gid、i_mode字段的值拷贝到OverlayFS根目录inode相应字段中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ...
    oe = ovl_alloc_entry(numlower);
    ...
    //为根目录分配inode,创建dentry
    root_dentry = d_make_root(ovl_new_inode(sb, S_IFDIR, oe));
    ...
    //通过前面得到的upper层和lower层的路径填充oe
    oe->__upperdentry = upperpath.dentry;
    for (i = 0; i < numlower; i++) {
    oe->lowerstack[i].dentry = stack[i].dentry;
    oe->lowerstack[i].mnt = ufs->lower_mnt[i];
    }
    ...
    //设置根目录dentry的d_fsdata字段指向oe
    root_dentry->d_fsdata = oe;

    //inode属性拷贝
    ovl_copyattr(ovl_dentry_real(root_dentry)->d_inode,
    root_dentry->d_inode);
  1. 填充super_block其余字段,设置super_block的根dentry、设置OverlayFS的字段s_fs_info指向代表挂载信息的变量ufs

    1
    2
    3
    4
    5
    6
    ...
    sb->s_magic = OVERLAYFS_SUPER_MAGIC;
    sb->s_op = &ovl_super_operations;//设置superblock操作对象
    sb->s_root = root_dentry;//设置superblock的根目录项
    sb->s_fs_info = ufs;//设置superblock的文件系统私有信息
    ...

总结一下,可以归纳为两个部分:

  • 1-4步:主要是在填充ufs,也就是设置OverlayFS挂载信息,最终第8步会设置super_block的s_fs_info字段指向ufs
  • 第6步:主要是创建根目录,然后设置根目录的层次信息oe(关于OverlayFS文件的层次信息在我的另一篇博文中有介绍)

最后设置super_block相应的字段保存这些信息

图示分析

这里给出图示,帮助对填充部分代码的理解

1-4步如下图:

第6步如下图:

文章目录
  1. 1. 注册
  2. 2. 挂载
    1. 2.1. OverlayFS挂载信息
    2. 2.2. 填充super_block
    3. 2.3. 图示分析
|