一、虚拟文件系统VFS(抽象层)
1、通用文件模型
通常一个完整的Linux系统由数千到数百万个文件组成,文件中存储了程序、数据和各种信息。层次化的目录结构用于对文件进行编排和分组。
文件系统类型一般可以分为 3 种:
基于磁盘的文件系统( Disk-based Filesystem)
虚拟文件系统( Virtual Filesystem)
网络文件系统( Network Filesystem)
a、inode
Linux 内核文件系统最重要的数据结构为inode,则一个inode 就代表一个文件,inode 结构体保存文件的大小,文件的 块大小、创建时间等各种参数。一个文件的inode只有唯一一个。
Linux 内核 inode 结构体如下:
b、链接
在 linux 系统中有种文件是链接文件,可以为解决文件的共享使用。链接分为两种:一种是硬链接(Hard Link),另一种是软链接或者也称为符号链接(Symbolic Link)。
【硬连接】相当于给一个文件取了多个名称,多个文件名称对应同一个索引节点,索引节点的成员i_nlink 是硬链接计数。
【软链接】这种文件的数据是另一个文件的路径,软链接可对文件或目录创建。
2、VFS 结构
在 VFS 接口实现当中,涉及大量的数据结构。VFS 结构由两个部分组成:文件和文件系统,这些都需要管理和抽象。
a、文件表示
inode 是 Linux 内核选择用于表示文件内容和相关元数据的方法。在抽象对底层文件系统的访问时,并未使用固定的函数,而是使用了函数指针。这函数指针保存在两个结构中,包括了所有相关的函数。
(1) inode 操作:创建链接、文件重命名、在目录中生成新文件、删除文件。
(2) 文件操作:作用于文件的数据内容。它们包含一些显然的操作(如读和写),还包括如设置文件位置指针和创建内存映射之类的操作。
b、文件系统和超级块
VFS 支持的文件系统类型通过一种特殊的内核对象连接进来,该对象提供了一种读取超级块的方法。除了文件系统的关键信息(块长度、最大文件长度,等等)之外,超级块还包含了读、写、操作 inode 的函数指针。
内核还建立了一个链表,包含所有活动文件系统的超级块实例。之所以使用活动( active)这个术语替代已装载(mounted),是因为在某些环境中,有可能使用一个超级块对应几个装载点。
c、VFS 的 inode 结构
inode分为内存中的inode和文件系统中的inode。VFSinode属于前者,后者以Ext2/Ext3为代表的inode。
// 常用
/*
* Keep mostly read-only and often accessed (especially for
* the RCU path lookup and 'stat' data) fields at the beginning
* of the 'struct inode'
*/
struct inode {
umode_t i_mode; // inode的权限
unsigned short i_opflags;
kuid_t i_uid; // inode拥有着的id
kgid_t i_gid; // inode所属的群组id
unsigned int i_flags;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
......
dev_t i_rdev; // 如果inode代表的是device的话,哪此字段将记录device的代码
loff_t i_size; // inode所代表的档案大小
struct timespec64 i_atime; // inode最近一次的存取时间
struct timespec64 i_mtime; // inode最近一次的修改时间
struct timespec64 i_ctime; // inode产生的时间
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
u8 i_blkbits; // inode在做io操作时区块大小
u8 i_write_hint;
blkcnt_t i_blocks;// i弄得所使用block数,一个block为512byte
d、inode 操作
内核提供了大量函数,对 inode 进行操作。为此定义了一个函数指针的集合,以抽象这些操作,因为实际数据是通过具体文件系统的实现操作的。调用接口总是保持不变,但实际工作是由特定于实现的函数完成的。
e、目录项缓存
由于块设备速度较慢,可能需要很长时间才能找到与一个文件名关联的 inode。即使设备数据已经在页缓存。Linux使用目录项缓存(简称 dentry 缓存)来快速访问此前的查找操作的结果。该缓存围绕着 struct dentry 建立,此前已经提到几次这个结构。
在VFS 连同文件系统实现读取的一个目录项(目录或文件)的数据之后,则创建一个 dentry 实例,以缓存找到的数据。
A1、dentry 结构,该结构定义如下:
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock目录项标记 */
seqcount_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list 散列表表项的指针*/
struct dentry *d_parent; /* parent directory 父目录的目录项对象*/
struct qstr d_name; // 目录项的名称
struct inode *d_inode; /* Where the name belongs to - NULL is negative 与文件名称相关联的索引节点*/
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */
/* Ref lookup also touches following */
struct lockref d_lockref; /* per-dentry lock and refcount */
const struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree 文件的超级块对象*/
unsigned long d_time; /* used by d_revalidate */
void *d_fsdata; /* fs-specific data */
union {
struct list_head d_lru; /* LRU list */
wait_queue_head_t *d_wait; /* in-lookup ones only */
};
struct list_head d_child; /* child of parent list 子目录中目录项对象的链表的指针*/
struct list_head d_subdirs; /* our children 对目录而言,表示子目录目录项对象的链表*/
/*
* d_alias and d_rcu can share memory
*/
union {
struct hlist_node d_alias; /* inode alias list 相关索引节点(别名)的链表*/
struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */
struct rcu_head d_rcu;
} d_u;
} __randomize_layout;
A2、缓存的组织
dentry 结构不仅使得易于处理文件系统,对提高系统性能也很关键。dentry 对象在内存中的组织,涉及下面两个部分。
(1) 一个散列表( dentry_hashtable)包含了所有的dentry对象。
(2) 一个 LRU(最近最少使用,least recently used)链表,其中不再使用的对象将授予一个最后宽限期,宽限期过后才从内存移除。
A3、dentry 操作
dentry_operations 结构保存了一些指向各种特定于文件系统可以对 dentry 对象执行的操作的函数指针。该结构定义如下:
struct dentry_operations {
int (*d_revalidate)(struct dentry *, unsigned int);
int (*d_weak_revalidate)(struct dentry *, unsigned int);
int (*d_hash)(const struct dentry *, struct qstr *);
int (*d_compare)(const struct dentry *,
unsigned int, const char *, const struct qstr *);
int (*d_delete)(const struct dentry *);
int (*d_init)(struct dentry *);
void (*d_release)(struct dentry *);
void (*d_prune)(struct dentry *);
void (*d_iput)(struct dentry *, struct inode *);
char *(*d_dname)(struct dentry *, char *, int);
struct vfsmount *(*d_automount)(struct path *);
int (*d_manage)(const struct path *, bool);
struct dentry *(*d_real)(struct dentry *, const struct inode *);
} ____cacheline_aligned;
二、Proc 文件系统
1、proc 文件系统
proc 文件系统是一种虚拟文件系统,其信息不能从块设备读取。只有在读取文件内容时才动态生成相应的信息。使用proc文件系统,可以获得有关内核各子系统的信息(如内存利用率、附接的外设等等),也可以在不重新编译内核源代码的情况下修改内核的行为,或重启系统。
proc 文件系统提供一种接口,可用于该机制导出的所有选项,直接地修改参数无需开发专门程序,只需要一个shell和标准的 cat、 echo 程序即可。
/proc 信息
尽管 proc 文件系统的容量依系统而不同,其中仍然包含了许多深层嵌套的目录、文件、链接。信息可以分为以下几大类:
内存管理;系统进程的特征数据;
文件系统;设备驱动程序;
系统总线;电源管理;
终端;系统控制参数。
Linux 系统上的/proc 目录是一种文件系统,即proc文件系统。/proc 是一种伪文件系统(也即虚拟文件系统),具体目录如下:
2、proc 常见文件
buddyinfo:存储记录系统的内存资源,通过它可以进行参考内存碎片情况分析。
cmdline:在启动时传递至内核的相关参数信息,这些信息通常由 lilo 或 grub 等启动管理工具进行传递;
cpuinfo:处理器的相关信息的文件;
crypto:系统上已安装的内核使用的密码算法及每个算法的详细信息列表;
devices:系统已经加载的所有块设备和字符设备的信息;
diskstats:每块磁盘设备的磁盘I/O 统计信息列表;
filesystems:当前被内核支持的文件系统类型列表文件,被标示为 nodev 的文件系统表示不需要块设备的支持;
interrupts:X86 或 X86_64 体系架构系统上每个IRQ相关的中断号列表;
iomem:每个物理设备上的记忆体(RAM 或者ROM)在系统内存中的映射信息;
ioports:当前正在使用且已经注册过的与物理设备进行通讯的输入-输出端口范围信息列表;
kallsyms:模块管理工具用来动态链接或绑定可装载模块的符号定义,由内核输出;
locks:保存当前由内核锁定的文件的相关信息,包含内核内部的调试数据;每个锁定占据一行,且具有一个惟一的编号;
meminfo:系统中关于当前内存的利用状况等的信息,常由free 命令使用;
mounts:在内核 2.4.29 版本以前,此文件的内容为系统当前挂载的所有文件系统;
modules:当前装入内核的所有模块名称列表,可以由lsmod命令使用,也可以直接查看;
partitions:块设备每个分区的主设备号(major)和次设备号(minor)等信息;
stat:实时追踪自系统上次启动以来的多种统计信息;
swaps:当前系统上的交换分区及其空间利用信息;
uptime:系统上次启动以来的运行时间;
version:当前系统运行的内核版本号;
vmstat:当前系统虚拟内存的多种统计数据;
zoneinfo:内存区域(zone)的详细信息列表;
3、proc 数据结构
proc 核心数据结构源码
实现 proc 文件系统的代码紧围绕这些结构而建立的,proc大量使用 VFS 的数据结构,因为作为一种文件系统,它必须集成到内核的 VFS 抽象层中。
还有一些特定于 proc 的数据结构,用于组织内核提供的数据信息。还必须提供一个到内核各个子系统的接口,使得内核能从其数据结构中提取信息,然后借助/proc 提供给用户空间。proc文件系统中的每个数据项都由 proc_dir_entry 的一个实例描述,具体源码如下:
// fs/proc/internal.h
struct proc_dir_entry {
/*
* number of callers into module in progress;
* negative -> it's going away RSN
*/
atomic_t in_use;
refcount_t refcnt;
struct list_head pde_openers; /* who did ->open, but not ->release */
/* protects ->pde_openers and all struct pde_opener instances */
spinlock_t pde_unload_lock;
struct completion *pde_unload_completion;
const struct inode_operations *proc_iops;
union {
const struct proc_ops *proc_ops;
const struct file_operations *proc_dir_ops;
};
const struct dentry_operations *proc_dops;
union {
const struct seq_operations *seq_ops;
int (*single_show)(struct seq_file *, void *);
};
proc_write_t write;
void *data;
unsigned int state_size;
unsigned int low_ino;
nlink_t nlink;
kuid_t uid;
kgid_t gid;
loff_t size;
struct proc_dir_entry *parent;
struct rb_root subdir;
struct rb_node subdir_node;
char *name;
umode_t mode;
u8 namelen;
char inline_name[];
} __randomize_layout;
装载 proc 文件系统
内核内部用于描述 proc 文件系统结构和内容的数据已初始化之后,下一步是将该文件系统装载到目录树中。在内核添加新文件系统时,会扫描一个链表,查找与该文件系统相关的 file_system_type 实例。源码如下:
static struct file_system_type proc_fs_type = {
.name = "proc",
.init_fs_context = proc_init_fs_context,
.parameters = proc_fs_parameters,
.kill_sb = proc_kill_sb,
.fs_flags = FS_USERNS_MOUNT | FS_DISALLOW_NOTIFY_PERM,
};
proc_sops 中对超级块的各个操作,其中收集内核管理proc文件系统所需的各个函数,具体源码如下:
const struct super_operations proc_sops = {
.alloc_inode = proc_alloc_inode,
.free_inode = proc_free_inode,
.drop_inode = generic_delete_inode,
.evict_inode = proc_evict_inode,
.statfs = simple_statfs,
.show_options = proc_show_options,
};
静态的 proc_dir_entry 实例:
/*
* This is the root "inode" in the /proc tree..
*/
struct proc_dir_entry proc_root = {
.low_ino = PROC_ROOT_INO,
.namelen = 5,
.mode = S_IFDIR | S_IRUGO | S_IXUGO,
.nlink = 2,
.refcnt = REFCOUNT_INIT(1),
.proc_iops = &proc_root_inode_operations,
.proc_dir_ops = &proc_root_operations,
.parent = &proc_root,
.subdir = RB_ROOT,
.name = "/proc",
};
三、super_block 基础
super_block:不同文件系统类型的物理结构有所不同,但通过虚拟文件系统将它们定义成一套统一的数据结构。一个文件系统对应一个超级块,其结构体为super_block,专门用来描述文件系统的相关信息,挂载文件系统的时候在内存中创建超级块的副本。
具体 Linux 内核中,super_block 数据结构源码如下:
如何获取指定块设备的超级块编号,直接使用系统调用get_super()函数处理。
四、挂载文件系统
A、挂载文件系统基础
A1、挂载描述符
Linux 操作系统的一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能够访问这个文件系统。每次挂载文件系统,虚拟文件系统就会创建一个挂载描述符(mount 结构体)。挂载描述符用来描述文件系统的一个挂载实例,同一个存储设备上的文件系统可以多次挂载,每次挂载到不同的目录下。具体挂载描述符的内核源码如下:
struct mount {
struct hlist_node mnt_hash; // 用于将挂载点加入哈希表以便快速查找
struct mount *mnt_parent; // 指向父级挂载点的指针
struct dentry *mnt_mountpoint; // 指向挂载点对应的目录项的指针
struct vfsmount mnt;
union {
struct rcu_head mnt_rcu;
struct llist_node mnt_llist;
};
#ifdef CONFIG_SMP
struct mnt_pcp __percpu *mnt_pcp;
#else
int mnt_count; //引用计数器,记录挂载点被引用的次数
int mnt_writers;
#endif
struct list_head mnt_mounts; /* list of children, anchored here */
struct list_head mnt_child; /* and going through their mnt_child */
struct list_head mnt_instance; /* mount instance on sb->s_mounts */
const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
struct list_head mnt_list;
struct list_head mnt_expire; /* link in fs-specific expiry list */
struct list_head mnt_share; /* circular list of shared mounts */
struct list_head mnt_slave_list;/* list of slave mounts */
struct list_head mnt_slave; /* slave list entry */
struct mount *mnt_master; /* slave is on master->mnt_slave_list 对于从属挂载点加入哈希表,此成员指向主挂载点*/
struct mnt_namespace *mnt_ns; /* containing namespace */
struct mountpoint *mnt_mp; /* where is it mounted */
union {
struct hlist_node mnt_mp_list; /* list mounts with the same mountpoint */
struct hlist_node mnt_umount;
};
struct list_head mnt_umounting; /* list entry for umount propagation */
#ifdef CONFIG_FSNOTIFY
struct fsnotify_mark_connector __rcu *mnt_fsnotify_marks;
__u32 mnt_fsnotify_mask;
#endif
int mnt_id; /* mount identifier */
int mnt_group_id; /* peer group identifier */
int mnt_expiry_mark; /* true if marked for expiry */
struct hlist_head mnt_pins;
struct hlist_head mnt_stuck_children;
} __randomize_layout;
A2、文件系统类型
因每种文件系统的超级块的格式不同,所每种文件系统需要向虚拟文件系统注册文件系统类型file_system_type,并且实现 mount 方法用来读取和解析超级块。具体内核源码如下:
管理员权限可以执行命令:cat /proc/filesystems来查看已注册的文件系统类型。
A3、挂载文件系统
虚拟文件系统在内存中把目录组织为一棵树,一个文件系统,有挂载到内存中目录树的一个目录,进程才能访问这个文件系统。
管理员权限可以执行命令:mount -t fstype [-0 options]device dir。把存储设备 device 上类型为fstype 的文件系统挂载到目录 dir 下。
glibc 库封装挂载文件系统的函数mount,两个卸载文件系统的函数 umount/umount2。
B、系统调用 mount
系统调用 mount 用来挂载文件系统,其定义格式如下:
C、绑定挂载
绑定挂载(bind mount)用来把目录树的一棵子树挂载到其它地方。执行绑定挂载命令:mount --bind olddir newdir。
把目录 olddir 为根的子树挂载到目录newold,以后从目录newdir 和目录 olddir 可以看到相同的内容。