Ext2文件系统彻底分析 | 扩展属性


什么是文件的扩展属性

扩展属性(xattrs)提供了一个机制用来将键值对(Key/Value)永久地关联到文件,让现有的文件系统得以支持在原始设计中未提供的功能。扩展属性是文件系统不可知论者,应用程序可以通过一个标准的接口来操纵他们,此接口不因文件系统而异。每个扩展属性可以通过唯一的键来区分,键的内容必须是有效的UTF-8,格式为namespace.attribute,每个键采用完全限定的形式,也就是键必需有一个确定的前缀(例如user)。

Linux操作系统有如下集中扩展属性:

  • system:用于实现利用扩展属性的内核功能,例如访问控制表。eg:system.posix_acl_access便是位于此用户空间的扩展属性,用户是否可以读取或写入这些属性取决于所使用的安全模块。
  • security:用于实现安全模块。
  • trusted:把受限制的信息存入用户空间。
  • user:一般进程所使用的标准命名空间,经过一般文件权限位来控制此命名空间的访问。

Ext2扩展属性的数据布局

本文主要介绍一下Ext2文件系统中扩展属性的相关内容,包括磁盘数据布局和创建流程等。在Ext2文件系统中,扩展属性存储在一个单独的磁盘逻辑块中,其位置由inode中的i_file_acl成员指定。如图所示是键值对在该逻辑块中的布局示意图。其前32个字节是一个描述头(ext2_xattr_header),描述该逻辑块基本使用情况。而下面紧跟着的是扩展属性项(ext2_xattr_entry),扩展属性项描述了扩展属性的键名称等信息,同时包含值的偏移等内容。这里需要说明的是扩展属性项是从上往下生长的,而值则是从下往上生长

图1 扩展属性布局图

如下代码是描述头(ext2_xattr_header)的结构体定义,里面有魔数、引用计数和哈希值等内容。这里魔数的作用是确认该逻辑块的内容是扩展属性逻辑块,避免代码Bug或者磁盘损坏等情况下给用户返回错误的结果。引用计数和哈希值的作用是实现多文件的扩展属性共享。所谓扩展属性共享是指,如果多个文件的扩展属性完全一样的情况下,这些文件的扩展属性将采用相同的磁盘逻辑块存储,这样可以大大的节省存储空间。另外,Ext2借用的哈希缓存,将文件属性的哈希值存储在其中,用于快速判断文件是否存在相同的扩展属性逻辑块。

1
2
3
4
5
6
7
struct ext2_xattr_header {
__le32 h_magic; /* magic number for identification */
__le32 h_refcount; /* reference count */
__le32 h_blocks; /* number of disk blocks used */
__le32 h_hash; /* hash value of all attributes */
__u32 h_reserved[4]; /* zero right now */
};

扩展属性项在磁盘上是从上往下生长的,但需要注意的是由于每个扩展属性的键名称的长度不一定一样,因此该结构体的大小也是变化的。由于上述原因,我们无法直接找到某一个扩展属性项的位置,必需从头到位进行遍历。由于描述头的大小是确定的,这样第一个扩展属性项就可以找到,而下一个扩展属性项就可以根据本扩展属性项的位置及其中的e_name_len成员计算得到。

1
2
3
4
5
6
7
8
9
struct ext2_xattr_entry {
__u8 e_name_len; /* length of name */
__u8 e_name_index; /* attribute name index */
__le16 e_value_offs; /* offset in disk block of value */
__le32 e_value_block; /* disk block attribute is stored on (n/i) */
__le32 e_value_size; /* size of attribute value */
__le32 e_hash; /* hash value of name and value */
char e_name[0]; /* attribute name */
};

VFS设置扩展流程分析

操作系统提供了一些API来设置文件的扩展属性,分别是setxattr、fsetxattr和lsetxattr。这几个函数应用场景略有差异,但功能基本一致。本文以fsetxattr为例进行介绍。假设用户调用该接口为某个文件设置user前缀的扩展属性,此时的整个函数调用栈如图所示。本调用栈包含三部分内容,分别是用户态接口、VFS调研栈和Ext2文件系统调用栈。

图2 扩展属性设置流程

通过上图可以看出在VFS做了很多事情,最后通过函数指针的方式调用到Ext2文件系统的扩展属性设置接口。整个流程中除了Ext2文件系统实现的设置扩展属性的函数(ext2_xattr_set)逻辑相对复杂外,整个函数栈的代码逻辑非常简单,这里就不过多介绍了。但是,这里有几点需要说明的:

属性设置公共接口调用

这个调用就是上图中i_op->setxattr的调用,其中i_op是inode中的一个成员,这个指针是分配inode节点的时候初始化的。这个函数屏蔽了不同的扩展属性(上文已经交代Linux文件系统有trusted和user等多种扩展属性)的处理方法集合。需要注意的是Ext2文件系统并没有实现自己的特有函数,而是调用了VFS提供的公共函数(generic_setxattr)。上述i_op是一个结构体指针,其中包含属性及扩展属性操作的所有接口,如下代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
const struct inode_operations ext2_file_inode_operations = { 
#ifdef CONFIG_EXT2_FS_XATTR
.setxattr = generic_setxattr,
.getxattr = generic_getxattr,
.listxattr = ext2_listxattr,
.removexattr = generic_removexattr,
#endif
.setattr = ext2_setattr,
.get_acl = ext2_get_acl,
.set_acl = ext2_set_acl,
.fiemap = ext2_fiemap,
};

关于设置扩展属性的公共实现函数比较简单,具体如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int generic_setxattr(struct dentry *dentry, 
const char *name,
const void *value,
size_t size,
int flags)
{
const struct xattr_handler *handler;

if (size == 0)
value = ""; /* empty EA, do not remove */
/* 根据扩展属性的键(Key)获取处理该动作的函数集指针handler。
* 例如设置user类型的扩展属性,则name格式为user.xxx */
handler = xattr_resolve_name(dentry->d_sb->s_xattr, &name);
if (!handler)
return -EOPNOTSUPP;
/* 调用具体类型扩展属性的处理函数, 例如对于user扩展属性,则
* 调用ext2_xattr_user_handler进行处理。 */
return handler->set(dentry, name, value, size, flags, handler->flags);
}

具体类型的扩展属性设置接口调用

这个调用就是上图中handler->set的调用,这个指针是在文件系统挂载的时候初始化的,其内容初始化了超级块的成员变量s_xattr。其中handler指针是根据用户传入的键名称确定的。具体获取是在函数xattr_resolve_name中实现的,其对超级块中的s_xattr变量进行遍历(匹配扩展属性的键前缀,流入user),从而找到可以处理该扩展属性的handler。如下代码是Ext2文件系统初始化超级快s_xattr用的数据,里面包含Ext2文件系统支持的所有扩展属性类型。

1
2
3
4
5
6
7
8
9
10
11
12
const struct xattr_handler *ext2_xattr_handlers[] = {
&ext2_xattr_user_handler,
&ext2_xattr_trusted_handler,
#ifdef CONFIG_EXT2_FS_POSIX_ACL
&posix_acl_access_xattr_handler,
&posix_acl_default_xattr_handler,
#endif
#ifdef CONFIG_EXT2_FS_SECURITY
&ext2_xattr_security_handler,
#endif
NULL
};

这样,经过几层调用之后就可以调用到Ext2文件系统中关于user扩展属性的设置接口。

Ext2扩展属性设置

对于user类型的扩展属性,其函数集为ext2_xattr_user_handler,如下是具体的定义。这里面实现了该类型扩展属性的查询和设置等接口。

1
2
3
4
5
6
const struct xattr_handler ext2_xattr_user_handler = { 
.prefix = XATTR_USER_PREFIX,
.list = ext2_xattr_user_list,
.get = ext2_xattr_user_get,
.set = ext2_xattr_user_set,
};

设置扩展属性的接口是ext2_xattr_user_set,如下是该函数的具体实现,从代码中可以看出该函数主要调用了ext2_xattr_set函数,这个函数实现了对添加扩展属性的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int 
ext2_xattr_user_set(struct dentry *dentry,
const char *name,
const void *value,
size_t size,
int flags,
int type)
{
if (strcmp(name, "") == 0)
return -EINVAL;
if (!test_opt(dentry->d_sb, XATTR_USER))
return -EOPNOTSUPP;

return ext2_xattr_set(d_inode(dentry), EXT2_XATTR_INDEX_USER,
name, value, size, flags);
}

函数ext2_xattr_set的实现非常长,大概300多行,因此这里就不贴代码了。本文具体介绍一下该函数的实现逻辑主要是查找键,然后根据查找的结果进行处理。如果能找到该键则更新值,如果找不到则新建一个键值对。这里有几个注意点,具体实现细节请自行阅读代码。

  • 设置扩展属性时可能是第一个,此时会分配新的磁盘空间
  • 设置扩展属性需要计算剩余空间,如果剩余空间不够,则创建失败
  • 设置扩展属性接口同时实现了更新扩展属性值的功能,但如果新值得长度大于旧值,则需要进行特殊处理
  • 在数据布局的次序上,键和值并没有严格的顺序

在实现逻辑中需要注意的一点是,用户在调用接口的时候可以传递附加标识,比如XATTR_REPLACE和XATTR_CREATE等。例如XATTR_REPLACE表示用户期望进行扩展属性值得替换操作,如果没有找到扩展属性的键,则返回失败。XATTR_CREATE则表示只进行创建操作,如果已经存在期望的键则失败。

持续更新… …

关注作者微信公众号,更及时的获取原创IT技术文章。
公众号二维码