内核OverlayFS—创建与删除文件

内核版本4.4.68

在OverlayFS中,创建和删除一个文件实际上并不是在OverlayFS的联合挂载目录下进行,而是在upper目录进行

举个例子,如果OverlayFS中存在/dir目录,而且这个目录是一个upper目录,那么执行命令touch /dir/file实际上会在upper目录下的dir目录中创建一个file文件。如果/dir目录下存在file文件,想要删除这个文件,也是到upper目录下的dir目录中删除这个file文件

由于upper目录并不属于OverlayFS,假设upper目录是ext4下的一个目录,因此OverlayFS无法直接在upper目录下创建或删除文件。那么OverlayFS是如何实现upper目录下的文件创建与删除的?实际上,在OverlayFS定义的文件创建与删除的函数中,只是负责找到对应的upper层中的目录,然后将操作的控制权转交给VFS,然后VFS进一步调用upper目录实际文件系统定义的文件创建与删除函数。这篇文章就是通过两个实际例子来分析这些步骤是如何完成的


创建文件

通过对VFS的学习,我们知道,在目录dir下创建一个文件,会调用目录dir对应inode操作文件创建相关的函数。在OverlayFS中,变量ovl_dir_inode_operations指明了目录的inode操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const struct inode_operations ovl_dir_inode_operations = {
.lookup = ovl_lookup,
.mkdir = ovl_mkdir,
.symlink = ovl_symlink,
.unlink = ovl_unlink,
.rmdir = ovl_rmdir,
.rename2 = ovl_rename2,
.link = ovl_link,
.setattr = ovl_setattr,
.create = ovl_create,
.mknod = ovl_mknod,
.permission = ovl_permission,
.getattr = ovl_dir_getattr,
.setxattr = ovl_setxattr,
.getxattr = ovl_getxattr,
.listxattr = ovl_listxattr,
.removexattr = ovl_removexattr,
};

下面以在OverlayFS根目录下创建一个一般文件file3为例,分析OverlayFS中文件创建过程

file3创建于OverlayFS根目录下,因此创建file3时,首先会获取OverlayFS根目录对应的inode,然后索引到inode操作,进一步根据create()函数指针找到并调用函数ovl_create():

1
2
3
4
5
static int ovl_create(struct inode *dir, struct dentry *dentry, umode_t mode,
bool excl)
{
return ovl_create_object(dentry, (mode & 07777) | S_IFREG, 0, NULL);
}

参数dentry是即将创建的文件对应的目录项,在这里就是文件file3对应的目录项,函数ovl_create()是函数ovl_create_object()的封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
static int ovl_create_object(struct dentry *dentry, int mode, dev_t rdev,
const char *link)
{
int err;

err = ovl_want_write(dentry);//获取对upper挂载点的写访问
if (!err) {
err = ovl_create_or_link(dentry, mode, rdev, link, NULL);
ovl_drop_write(dentry);
}

return err;
}

主要操作由函数ovl_create_or_link()完成,省略一些细节,该函数代码如下:

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
static int ovl_create_or_link(struct dentry *dentry, int mode, dev_t rdev,
const char *link, struct dentry *hardlink)
{
...
struct inode *inode;
struct kstat stat = {
.mode = mode,
.rdev = rdev,
};
...
/*在OverlayFS下为新创建的文件分配一个inode*/
inode = ovl_new_inode(dentry->d_sb, mode, dentry->d_fsdata);
...
/*父目录有可能是lower层的目录,因此要进行cow-on-write操作*/
err = ovl_copy_up(dentry->d_parent);
...

/*根据dentry的opaque值决定调用哪个函数*/
if (!ovl_dentry_is_opaque(dentry)) {
err = ovl_create_upper(dentry, inode, &stat, link, hardlink);
} else {
...
err = ovl_create_over_whiteout(dentry, inode, &stat, link,
hardlink);
...
}
...
}

从上面的代码可以看出,函数主要完成以下工作:

  • 调用ovl_new_inode()在OverlayFS中为新创建的文件分配一个inode
  • 由于父目录可能是一个lower层的目录,因此要对父目录调用ovl_copy_up()执行copy_up操作,将父目录拷贝至upper层
  • 根据新创建文件的opaque属性在upper层中创建实际文件
    • 如果没有设置opaque属性,则调用ovl_create_upper()函数。在这个例子中,会调用此函数:
      • dentry->d_parent找到新创建文件的父目录dentry。在此例中,父目录就是OverlayFS的根目录
      • 调用函数ovl_dentry_upper()根据父目录的层次信息(其它博文有介绍),找到父目录对应upper层目录的dentry及inode,在这个例子中父目录对应的upper层目录就是upper目录
      • 在upper目录对应的dentry下查找是否存在和新文件同名的dentry,不存在则创建
      • 调用ovl_create_real()函数,传入上一步得到的dentry,以及upper目录对应的inode。函数根据文件的类型调用不同的函数创建文件:
        • 如果是一般文件,调用ovl_do_create(),该函数进一步调用vfs_create(),将控制权转交给VFS,vfs_create()函数中会根据传入的inode索引到inode操作,然后调用inode操作中的create()函数完成文件的创建。在本例中,会按该路径执行
        • 如果是目录,则调用ovl_do_mkdir(),该函数进一步调用vfs_mkdir(),将控制权转交给VFS,vfs_mkdir()函数中同样会根据传入的inode索引到inode操作,然后调用inode操作中的mkdir()函数完成目录的创建
    • 如果设置了opaque属性,则调用ovl_create_over_whiteout()函。根ovl_create_upper()函数不同的是,ovl_create_over_whiteout()会用到workdir,将上述步骤在workdir下完成,然后调用一个重命名操作,将workdir下创建的文件重命名到upper层

ovl_create_upper()函数如下:

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
29
30
static int ovl_create_upper(struct dentry *dentry, struct inode *inode,
struct kstat *stat, const char *link,
struct dentry *hardlink)
{
struct dentry *upperdir = ovl_dentry_upper(dentry->d_parent);//dentry父目录的upper目录项
struct inode *udir = upperdir->d_inode;//父目录upper目录项对应的inode
struct dentry *newdentry;
int err;

mutex_lock_nested(&udir->i_mutex, I_MUTEX_PARENT);
newdentry = lookup_one_len(dentry->d_name.name, upperdir,
dentry->d_name.len);//在父目录的upper目录项下查找dentry
err = PTR_ERR(newdentry);
if (IS_ERR(newdentry))
goto out_unlock;
err = ovl_create_real(udir, newdentry, stat, link, hardlink, false);
if (err)
goto out_dput;

ovl_dentry_version_inc(dentry->d_parent);
ovl_dentry_update(dentry, newdentry);//设置dentry的upper目录项为newdentry
ovl_copyattr(newdentry->d_inode, inode);
d_instantiate(dentry, inode);
newdentry = NULL;
out_dput:
dput(newdentry);
out_unlock:
mutex_unlock(&udir->i_mutex);
return err;
}

总结一下,在OverlayFS中创建一个文件时,首先根据这个文件所在目录的inode索引到inode操作,然后根据创建文件的类型调用inode操作中不同的函数,这些在VFS中完成。在本例中,创建一般文件file3,所以调用create(),OverlayFS定义了自己的create()函数—ovl_create()。因此,文件的创建操作开始由OverlayFS来完成。OverlayFS创建文件的大部分工作由ovl_create_or_link()完成,该函数会解析出upper层相应目录的inode和dentry,创建新文件的dentry。接着,OverlayFS调用vfs_create()函数,将控制权又返回到VFS层,该函数最终根据OverlayFS解析到的upper层目录的inode索引到inode操作,然后调用实际函数完成最终文件的创建。在本里中,由于upper是ext4文件系统下的一个目录,所以具体文件的创建函数定义在ext4中。由此可以得出一个结论,OverlayFS下文件的创建,利用了VFS通用文件系统模型,实现操作转移,将文件的创建操作转移到upper层


创建文件的函数调用栈

上图展示了文件创建的函数调用栈,红色加粗部分是前面介绍例子的调用栈,它展示了一个一般文件的创建过程。除了一般文件,还有目录、链接以及特殊文件的创建。从图中可以看出,在OverlayFS中创建文件时,最初都在VFS层调用了inode操作的相应函数,然后OverlayFS进行一系列解析,得到相应的upper层文件的inode,最终又回到VFS层,根据upper层文件的inode索引到对应的操作,将文件的创建操作转化到upper层


删除文件

删除文件时,则会调用inode操作的unlink()函数或rmdir()函数。通过ovl_dir_inode_operations变量可以找到OverlayFS定义的unlink()函数为ovl_unlink(),rmdir()对应的函数为ovl_rmdir():

1
2
3
4
5
6
7
8
9
static int ovl_unlink(struct inode *dir, struct dentry *dentry)
{
return ovl_do_remove(dentry, false);
}

static int ovl_rmdir(struct inode *dir, struct dentry *dentry)
{
return ovl_do_remove(dentry, true);
}

两者都是ovl_do_remove()的封装,忽略一些细节,函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int ovl_do_remove(struct dentry *dentry, bool is_dir)
{
enum ovl_path_type type;
int err;

...
/*删除的文件可能是一个lower层的文件,所以要先将父目录copy_up*/
err = ovl_copy_up(dentry->d_parent);
...
type = ovl_path_type(dentry);//获取删除文件的路径类型
if (OVL_TYPE_PURE_UPPER(type)) {//(type) & __OVL_PATH_PURE
err = ovl_remove_upper(dentry, is_dir);//1.如果文件只存在upper层
} else {
...

err = ovl_remove_and_whiteout(dentry, is_dir);

...
}

...
}

if-else结构表明,OverlayFS删除一个文件时,也分为2种情况:

  • 若删除的文件只存在于upper层,则调用函数ovl_remove_upper()实现删除操作:
    • 和文件创建一样,首先会找到要删除文件所在目录对应的upper层目录的inode和dentry
    • 在上一步得到的upper层目录的dentry下查找删除文件的dentry。如果没找到则会返回错误
    • 根据删除的文件是否是一个目录调用不同函数将控制权转给VFS:
      • 如果删除的文件是一个目录,则调用vfs_rmdir()
      • 如果删除的文件不是一个目录,则调用vfs_unlink()
    • vfs_rmdir()或vfs_unlink()会根据前面得到的upper层目录的inode,索引到inode操作,然后调用upper层文件系统实现的rmdir()或unlink()将控制权交给upper层文件系统,完成文件的删除
  • 若删除的文件存在lower层,则调用函数ovl_remove_and_whiteout()实现删除操作。该函数同样会查找删除文件所在目录upper层对应目录的inode和dentry。和文件只存在upper层不同,若文件存在lower层,则需要在work目录下建立起whiteout文件最后重命名到upper层来隐藏lower层的文件。对upper层文件的清理分2种情况:
    • 文件类型是目录:会调用ovl_clean()删除upper层对应目录
    • 文件类型不是目录:将whiteout文件重命名到upper层后会清理掉原来upper层的文件
      需要注意的是,对于目录,OverlayFS会使用自身的缓存来加速目录的搜索操作。因此如果删除的是一个目录,在清理upper层对应目录前,会清理相应的缓存。这部分逻辑比较复杂,由于这里的重点在于文件删除的通用步骤,因此不做分析,往后可能在其它博文中介绍,感兴趣的话可以具体去看看源码

假设要删除刚刚创建的file3,则会调用ovl_remove_upper():

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
29
30
31
32
33
34
35
36
37
38
39
40
static int ovl_remove_upper(struct dentry *dentry, bool is_dir)
{
/*获取即将创建文件所在目录对应upper层目录的dentry,inode信息*/
struct dentry *upperdir = ovl_dentry_upper(dentry->d_parent);
struct inode *dir = upperdir->d_inode;
struct dentry *upper;
int err;

mutex_lock_nested(&dir->i_mutex, I_MUTEX_PARENT);
/*在upper层对应目录下查找创建同名目录项*/
upper = lookup_one_len(dentry->d_name.name, upperdir,
dentry->d_name.len);
err = PTR_ERR(upper);
if (IS_ERR(upper))
goto out_unlock;

err = -ESTALE;
/*判断查找到的同名目录项是否就是dentry对应upper层的目录项*/
if (upper == ovl_dentry_upper(dentry)) {
if (is_dir)
err = vfs_rmdir(dir, upper);//是目录则调用vfs_rmdir
else
err = vfs_unlink(dir, upper, NULL);//非目录则调用vfs_unlink
ovl_dentry_version_inc(dentry->d_parent);
}
dput(upper);

/*
* Keeping this dentry hashed would mean having to release
* upperpath/lowerpath, which could only be done if we are the
* sole user of this dentry. Too tricky... Just unhash for
* now.
*/
if (!err)
d_drop(dentry);
out_unlock:
mutex_unlock(&dir->i_mutex);

return err;
}


删除文件的函数调用栈

红色加粗部分为删除刚刚创建的file3的调用栈

文章目录
  1. 1. 创建文件
  2. 2. 创建文件的函数调用栈
  3. 3. 删除文件
  4. 4. 删除文件的函数调用栈
|