Linux VFS机制简析(一)

本文主要基于Linux内核文档,简单分析Linux VFS机制,以期对编写新的内核文件系统(通常是给分布式文件系统编写内核客户端)的场景有所帮助。

个人渊源

切入正文之前先扯点别的,舰队我在04年刚接触Linux时就深入分析了VFS,当时刚毕业入职一家做NAS存储的公司,需要对VFS、block device、MD等内核模块深入了解。时隔10几年之后的今天,因给一个分布式文件系统做内核客户端,重拾VFS发现一切还是熟悉的味道。这十几年过去了,内核版本从2.6到4.x,VFS的机制和整体架构变化不大,依然是各种底层文件系统和用户态接口之间不可或缺的转换层。

Overview

VFS(Virtual File System)是Linux内核里提供文件系统接口给用户态应用程序的一个虚拟文件系统层。同时VFS还提供了抽象化的操作接口以方便实现内核的底层文件系统。

Directory Entry Cache (dcache)

VFS实现open、stat、chmod等类似的文件系统调用,他们传递一个pathname参数给VFS。VFS根据文件路径pathname搜索directory entry cache(dentry cache或者dcache)获取对应的dentry。所以dcache是一个高速目录项缓存,用于映射文件路径和dentry。dentry结构用于优化查询性能,只存在于内存中,不实际存储到磁盘。

内存限制,并不是所有dentry都能在缓存命中,当根据pathname找不到对应dentry时,VFS调用lookup接口向底层文件系统查找获取inode信息,以此建立dentry和其对应的inode结构。

Inode

每个dentry通常对应一个inode结构用于描述文件、目录等的基本元数据信息。如果底层是磁盘存储,Inode结构会保存到磁盘。当需要时从磁盘读取到内存中进行缓存。一个inode结构可以被多个dentry指向,如硬链接。对于网络文件系统(分布式文件系统),Inode结构需要通过网络协议获取到缓存中。

VFS通过父目录的lookup方法来获取某个文件的inode信息,该方法由底层文件系统实现。一旦获取了inode信息,open,stat等无聊的操作直接从缓存里进行,变得很快。

File

Open一个文件还需要另外一个数据结构:File。File用于表示一个处于Open状态的文件,同一个文件被Open多次对应不同的File结构。应用程序打开文件后对应一个句柄(FD, file descriptor),每个FD都对应到内核的一个File结构,因此File结构直接存放在进程的FD表里,通过FD可以快速获取到File数据结构。

VFS实现用户态文件读写关闭操作时,通过用户态的FD来获取对应的File结构,然后调用对应的底层文件系统方法。只要有File结构正在使用,就增加dentry的引用计数,保证dentry和inode结构没有从缓存里删除。

Registering and Mounting a Filesystem

通过如下函数进行文件系统的注册和注销操作:

#include <linux/fs.h>
extern int register_filesystem(struct file_system_type *);
extern int unregister_filesystem(struct file_system_type *);

其中struct file_system_type用于描述文件系统基本信息和mount()等操作。当挂载文件系统到目录时,调用对应file_system_type里的mount()函数。原文件系统目录树上挂载点会附上新的vfsmount,当路径解析到挂载点时,会自动跳转到vfsmount的根目录。

通过/proc/filesystems可以查看到所有注册的文件系统类型。

struct file_system_type

结构体file_system_type的定义如下:

struct file_system_type {
115 const char *name;
116 int fs_flags;
117 struct dentry *(*mount) (struct file_system_type *, int,
118 const char *, void *);
119 void (*kill_sb) (struct super_block *);
120 struct module *owner;
121 struct file_system_type * next;
122 struct list_head fs_supers;
123 struct lock_class_key s_lock_key;
124 struct lock_class_key s_umount_key;
125 };

其中,name是文件系统名称,如ext4, xfs等等。fs_flags为各种标识,如FS_REQUIRES_DEV, FS_NO_DCACHE等。mount()函数指针用于挂载一个新的文件系统实例。kill_sb()函数指针用于关闭文件系统实例。owner是VFS内部使用,通常设置为THIS_MODULE。next也是VFS内部使用,初始化时设置为NULL即可。s_lock_keys_umount_key是lockdep相关的结构。

mount()函数有几个参数:fs_type为对应的file_sytem_type结构指针。flags为挂载的标识。dev_name为挂载的设备名,对于网络文件系统通常是一个网络路径。data为挂载的选项,通常为一组ASCII字符串。

mount()必须返回文件系统目录树的root dentry。文件系统的super block增加一个引用计数并处于locked状态。mount失败时返回ERR_PTR(err)。mount()函数可以选择返回一个已经存在的文件系统的一个子树,而不是创建一个新的文件系统实例,这种情况返回的是子树的root dentry。

底层文件系统实现mount,可以直接调用通用的mount实现:mount_bdev(在块设备上挂载文件系统)、mount_nodev(挂载没有设备的文件系统)和mount_single(挂载在不同的mounts间共享实例的文件系统),并提供一个fill_super()的回调函数用于创建root dentry和inode。比如FUSE就通过调用mount_nodev来实现mount操作。

其中file_super()回调函数的参数包括:struct super_block sb(文件系统sb,需要在fill_super()里进行初始化)、void data(文件系统挂载的选项字符串)、int silent(是否忽略error)。

当然也可以参考通用的mount实现自己的mount操作,比如Ceph就直接调用了sget()函数创建sb并通过set()回调函数初始化sb。

Mount Options

mount函数会传递一个options的字符串,以逗号隔开。它是mount命令输入的选项(通过-o设置)。options的格式可以是如下两种:

  • option
  • option=value

Linux内核头文件linux/parser.h里定义了帮助解析options的API。可以从现有的文件系统代码里找到使用方法。

如果一个文件系统使用了mount options,则必须实现s_op->show_options()函数将选项进行显示。显示的规则如下:

  • 如果option不是默认值,则必须显示。
  • 如果option等于默认值,则可选择是否显示。

Superblock and struct super_operations

Superblock超级块(简称sb,莫名哈哈一笑)代表一个挂载的文件系统,其数据结构保存了文件系统基本的元数据信息。其中s_op指向了struct super_operations,为sb这一级的函数操作合集。

super_operations的定义如下:

struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *); void (*dirty_inode) (struct inode *, int flags);
int (*write_inode) (struct inode *, int);
void (*drop_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_fs) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *); int (*show_options)(struct seq_file *, struct dentry *); ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
int (*nr_cached_objects)(struct super_block *);
void (*free_cached_objects)(struct super_block *, int);
};

所有的函数,如果没有特别说明,都在没有持有锁的情况下被调用,因此大部分这些函数都可以安全地进行阻塞操作。所有的函数都只在进程上下文中被调用(区别于中断处理或者中断处理下半部分)。

alloc_inode:被inode_alloc()函数调用用于分配inode内存并进行inode结构初始化。如果函数未定义,则简单的分配一个'struct inode'。通常alloc_inode用于底层文件系统分配一个包含inode结构体的更大的结构体(特定的inode结构,如:fuse_inode)。

destroy_inode:被destroy_inode()函数调用用于释放inode相关申请的资源。只有alloc_inode定义了才需要定义destroy_inode,并且释放的也是alloc_inode里申请的相关资源。

dirty_inode:由VFS调用标记inode dirty(元数据信息被修改过并且没有同步到磁盘或服务器)。

write_inode:由VFS调用用于将inode同步到磁盘。第二个参数用于标识是否同步写盘。

drop_inode:VFS在当inode的引用计数减为0时,调用该函数。调用者已经持有了inode->i_lock。该函数返回0,则inode将可能被丢到LRU链表里,返回1则会由调用者继续调用evict_inodedestroy_inode。如果文件系统不需要缓存inode,则该函数可以设置为NULL或者generic_delete_inode(函数里直接return 1)。

delete_inode:VFS删除inode时直接调用该函数。由于查看的Linux文档版本是2.6.39,所以有该函数指针,在3.10版本已经没有了detele_inode

put_super:VFS想要释放sb时调用(如umount操作)。调用者已经持有sb的lock。

sync_fs:VFS想要把该文件系统所有的脏数据刷盘时调用。

freeze_fs:目前只有LVM使用。用于冻结文件系统,不能进行写入操作。

unfreeze_fs:解冻文件系统,使其可以写入。

statfs:用于获取文件系统的统计信息。

remount_fs:用于重新挂载文件系统,调用者持有kernel lock。

clear_inode:同样在3.10版本没有了。

umount_begin:用于umount文件系统。

show_options:用于/proc/mounts里显示文件系统的mount选项。

quota_readquota_write:用于读写文件系统的quota文件。

nr_cached_objectsfree_cache_objects:用于返回可以释放的cache对象个数,以及进行实际的释放对象操作。

可以看到super_operations包含了inode的分配、初始化和释放。inode里的i_op字段指向了底层文件系统inode相关操作合集:struct inode_operations。

struct inode_operations

struct inode_operations定义如下,它描述了VFS如何管理inode对象。

struct inode_operations {
int (*create) (struct inode *,struct dentry *, umode_t, bool);
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,umode_t);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
int (*readlink) (struct dentry *, char __user *,int);
void * (*follow_link) (struct dentry *, struct nameidata *);
void (*put_link) (struct dentry *, struct nameidata *, void *);
int (*permission) (struct inode *, int);
int (*get_acl)(struct inode *, int);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
int (*removexattr) (struct dentry *, const char *);
void (*update_time)(struct inode *, struct timespec *, int);
int (*atomic_open)(struct inode *, struct dentry *,
struct file *, unsigned open_flag,
umode_t create_mode, int *opened);
};

同样,如果没有特别注明,所有函数都在没有锁持有的情况下调用。

create:由open和create系统调用使用。入参inode为父目录的inode,入参dentry为新创建的,没有对应的inode(negative dentry)。底层文件系统需要调用d_instantiate()将dentry和新创建的inode进行关联。只有目录类型的inode才会调用该函数指针。

lookup:VFS需要查找目录下面某个inode信息是调用该函数。入参dentry里携带了要查找的文件name。该函数里需要调用d_add()将找到的inode插入到dentry。并且inode的i_count字段需要递增。如果inode没有找到,则dentry插入一个NULL inode(这种dentry称为一个negative dentry)。只有在底层真实错误时才能返回error,此时open、create、mknode等涉及创建inode的操作都会失败。同样也只有目录类型的inode才会调用该函数指针。

lookup函数里,可以将dentry的d_op字段初始化为自己的dentry_operations,来定制对dentry和dcache的一些管理函数操作合集。

link:link系统调用使用,用于创建硬链接。同样需要调用d_instantiate()来关联dentry和inode。

unlink:unlink系统调用使用,用于删除一个inode关联的文件或目录。

symlink:symlink系统调用使用,用于创建一个软链接。

mkdir:mkdir系统调用使用,用于创建一个子目录。

rmdir:rmdir系统调用使用,用于删除一个子目录。

mknod:mknod系统调用使用,用于创建一个设备inode(char,block)或者一个named pipe (FIFO)或者一个socket。

rename:rename系统调用使用,用于改名。

readlink:readlink系统调用使用,用于读取软链接文件指向的实际路径。

follow_link:VFS调用,用于跟踪获取一个软链接指向的inode。该函数返回一个指针cookie,该cookie会传递给put_link

put_link:用于释放follow_link里申请的资源,cookie作为最后一个参数传入。它在NFS等文件系统上,page cache不是很稳定的情况下使用。

permission:VFS调用,用于检测访问权限。有可能在rcu-walk mode下被调用,那么该函数必须不能阻塞或者存储数据到inode。如果在rcu-walk mode下遇到问题,则返回-ECHILD,它将在ref-walk mode重新被调用。

setattr:VFS调用,用于设置文件的attr属性。它将被chmod等相关系统调用使用。

getattr:VFS调用,用于获取文件的attr属性。它将被stat等相关系统调用使用。

setxattr:VFS调用,用于设置文件的一个扩展attr属性。它将被setxattr系统调用使用。

getxattr:VFS调用,用于根据属性名称获取文件的一个扩展attr属性。它将被getxattr系统调用使用。

listxattr:VFS调用,用于列出给定文件的所有扩展属性。它将被listxattr系统调用使用。

removexattr:VFS调用,用于删除一个扩展attr属性。它将被removexattr系统调用使用。

update_time:VFS调用,用于更新inode的时间(如atime)或者i_version字段。如果该函数没有指定,则VFS将自己更新inode并调用mark_inode_dirty_sync。

atomic_open:该可选的函数,用于性能优化。它将lookup、可能的create操作以及open操作在一个接口里完成。只有negative dentry才会调用该函数。在dentry cache里的positive dentry直接通过f_op->open()函数来打开文件即可。

参考

Linux Documentation: VFS

后记

本篇主要介绍了VFS架构机制和作用,以及如何实现一个底层文件系统的注册和mount、super block和sb operations、inode和inode operations。

下一篇将继续介绍有关Address space和address operations、file和file operations、dentry和dentry operations和dentry cache API:Linux VFS机制简析(二)

Linux VFS机制简析(一)的更多相关文章

  1. Linux VFS机制简析(二)

    Linux VFS机制简析(二) 接上一篇Linux VFS机制简析(一),本篇继续介绍有关Address space和address operations.file和file operations. ...

  2. Linux内存管理机制简析

    Linux内存管理机制简析 本文对Linux内存管理机制做一个简单的分析,试图让你快速理解Linux一些内存管理的概念并有效的利用一些管理方法. NUMA Linux 2.6开始支持NUMA( Non ...

  3. Linux目录结构简析

    Linux目录结构简析 Linux继承了unix操作系统结构清晰的特点.在linux下的文件结构非常有条理.但是,上述的优点只有在对linux相当熟悉时,才能体会到.现在,虫虫就把linux下的目录结 ...

  4. Binder机制简析(三)

    注册Service Service组件运行在Server进程中,首先要将Service注册到Service Manager中,再启动一个Binder线程池来等待和处理Client的通信请求. 注册过程 ...

  5. Linux内核poll/select机制简析

    0 I/O多路复用机制 I/O多路复用 (I/O multiplexing),提供了同时监测若干个文件描述符是否可以执行IO操作的能力. select/poll/epoll 函数都提供了这样的机制,能 ...

  6. linux装载可执行程序简析

    朱宇轲 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 linux中主要 ...

  7. linux共享内存简析

    共享内存是IPC的一种机制,允许两个不相关的进程共享同一块内存 //共享内存可以双向通信,但其本身没有相应机制,需要程序编写者设计,本例为单向通信(分为读端和写端). 共享内存读端: #include ...

  8. Magento 缓存机制简析

    在知道缓存机制前,首先需要知道,Magento的路由机制,这边就不做赘述了,百度一大堆. 下面一个简单的缓存生效流程: A:首先在页面开始时,Magento在app\code\core\Mage\Co ...

  9. Unity - 存读档机制简析

    本文旨在于简要分析Unity中的两种存档机制,即:PlayerPrefs数据持久化方法及Serialization数据序列化方法 较比与源项目,我另加了JSON方法.XML方法等及一些Unity设置, ...

随机推荐

  1. centos7 .net core 使用supervisor守护进程,可以后台运行

    1.安装supervisor yum install supervisor 2.配置supervisor vi /etc/supervisord.conf 拉到最后,这里的意思是 /etc/super ...

  2. javascript webstorm用法

    javascript  webstorm用法 一.什么是webstorm?       WebStorm 是jetbrains公司旗下一款JavaScript 开发工具.被广大中国JS开发者誉为“We ...

  3. Windows10电脑系统时间校准

    有时候新安装电脑系统,系统时间不对,需要主动去校准系统时间. 1.点击时间 2.日期和时间设置 3.其他日期.时间和区域设置 4.设置时间和日期 5.Internet 时间 6.点击立即更新,如果更新 ...

  4. Launch VINS example (Euroc dataset) in RTAB-MAP

    $ roslaunch rtabmap_ros euroc_datasets.launch args:="-d RGBD/CreateOccupancyGrid false Odom/Str ...

  5. luoguP4213 [模板]杜教筛

    https://www.luogu.org/problemnew/show/P4213 同 bzoj3944 考虑用杜教筛求出莫比乌斯函数前缀和,第二问随便过,第一问用莫比乌斯反演来做,中间的整除分块 ...

  6. 八大排序算法的python实现(四)快速排序

    代码: #coding:utf-8 #author:徐卜灵 #交换排序.快速排序 # 虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤.因此我的对快速排序作了进一步的说明: ...

  7. [译文]Casperjs1.1.0参考文档-安装

    安装 Casperjs能被安装在mac osx,windows 和大多数linux版本 依赖项 PhantomJS1.82及以上 Python2.6及以上(很多人忘了安装python导致安装失败) 1 ...

  8. 推荐分享一个牛X的自定义PHP加密解密类

    通俗点说,用它来进行加密,同一个字符串,每次进行加密,得出的结果都是不一样的,大大加强了数据安全性.同时还可设定加密后数据的有效期,简直牛掰了 #食用方法 将下面的第二份模块代码保存为 Mcrypt. ...

  9. 3 hql语法及自定义函数(含array、map讲解) + hive的java api

    本博文的主要内容如下: .hive的详细官方手册    .hive支持的数据类型   .Hive Shell .Hive工程所需依赖的jar包  .hive自定义函数 .分桶4   .附PPT hiv ...

  10. TextInput

    TextInput /** TextInput 是一个允许用户在应用中通过键盘输入文本的基本组件* 本组件的属性提供了多种特性的配置,如自动完成,自动大小写,占位文字,键盘类型等* 常用:* plac ...