面试篇: Linux虚拟文件系统你了解吗?
每日一句:
Don’t ever let somebody tell you you can’t do something.
别让他人告诉你你不行。
本次分享,我们来一起深入学习下Linux的虚拟文件系统,在这之前,我们先简单了解下什么是用户态和内核态,以及什么是用户空间和内核空间。
特权级
在Linux系统中,出于安全考虑,将指令分成0-3的特权级别,数字越小,特权级别越高,那些和系统底层特别关键的操作,必须由最高特权的程序来完成。
- 0级别的指令,运行在受信任的内核态
- 3级别的指令,运行在受限制的用户态
用户态和内核态
- 内核态:CPU可以访问内存所有数据,包括外围设备(硬盘、网卡),CPU也可以将自己从一个程序切换到另一个程序;
- 只能受限的访问内存,且不允许访问外围设备,占用CPU的能力被剥夺,CPU资源可以被其他程序获取;
用户空间和内核空间
Linux中任何一个用户进程被创建时都包含2个栈:内核栈,用户栈,并且是进程私有的,从用户态开始运行。内核态和用户态分别对应内核空间与用户空间,内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。
内核空间相关
- 内核空间:存放的是内核代码和数据,处于虚拟空间;
- 内核态:当进程执行系统调用而进入内核代码中执行时,称进程处于内核态,此时CPU处于特权级最高的0级内核代码中执行,当进程处于内核态时,执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈;
- CPU堆栈指针寄存器指向:内核栈地址;
- 内核栈:进程处于内核态时使用的栈,存在于内核空间;
- 处于内核态进程的权利:处于内核态的进程,当它占有CPU的时候,可以访问内存所有数据和所有外设,比如硬盘,网卡等等
用户空间相关
- 用户空间:存放的是用户程序的代码和数据,处于虚拟空间;
- 用户态:当进程在执行用户自己的代码(非系统调用之类的函数)时,则称其处于用户态,CPU在特权级最低的3级用户代码中运行,当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态,因为中断处理程序将使用当前进程的内核栈;
- CPU堆栈指针寄存器指向:用户堆栈地址;
- 用户堆栈:进程处于用户态时使用的堆栈,存在于用户空间;
- 处于用户态进程的权利:处于用户态的进程,当它占有CPU的时候,只可以访问有限的内存,而且不允许访问外设,这里说的有限的内存其实就是用户空间,使用的是用户堆栈。
用户态切换到内核态的3种方式
(1)系统调用
所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情,例如从硬盘读取数据等。而唯一可以做这些事情的就是操作系统,所以此时程序就需要先操作系统请求以程序的名义来执行这些操作。这时需要一个这样的机制:用户态程序切换到内核态,但是不能控制在内核态中执行的指令。这种机制叫系统调用,在CPU中的实现称之为陷阱指令(Trap Instruction)。
(2)异常事件
当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。
(3)外围设备的中断
当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。
注意:系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断,这是操作系统为用户特别开放的一种中断,如Linux int 80h中断。所以从触发方式和效果上来看,这三种切换方式是完全一样的,都相当于是执行了一个中断响应的过程。但是从触发的对象来看,系统调用是进程主动请求切换的,而异常和硬中断则是被动的。
虚拟文件系统 VFS
虚拟文件系统(VFS)作为Kernal的子系统,为用户空间提供了文件和文件系统相关的接口.平常我们说对文件的操作就建立在这之上.屏蔽不同文件系统的差异和操作细节。借助VFS可以直接使用**open()、read()、write()**这样的系统调用操作文件,而无须考虑具体的文件系统和实际的存储介质, 为了给内核和用户进程提供统一的文件系统视图,而在内核和用户进程之间加入了抽象层,即虚拟文件系统(Java接口定义).
举个例子,Linux用户程序可以通过read() 来读取ext3、NFS、XFS等文件系统的文件,也可以读取存储在SSD、HDD等不同存储介质的文件,无须考虑不同文件系统或者不同存储介质的差异。
通过VFS系统,Linux提供了通用的系统调用,可以跨越不同文件系统和介质之间执行,极大简化了用户访问不同文件系统的过程。另一方面,新的文件系统、新类型的存储介质,可以无须编译的情况下,动态加载到Linux中。
"一切皆文件"是Linux的基本哲学之一,不仅是普通的文件,包括目录、字符设备、块设备、套接字等,都可以以文件的方式被对待。实现这一行为的基础,正是Linux的虚拟文件系统机制。
虚拟文件系统 VFS的原理
在我们使用C语言编写应用程序时,相信会经常使用到write()系统调用;就是向一个文件中写入数据。write()函数的原型为
ssize_t write(int fd, const void *buf, size_t count);
在用户程序的write(f, &buf, len),向文件描述符为f的文件中,写入len个字节数据,待写入的数据存放在buf中。下图为write()将数据写入硬件上的简易流程。我们看到首先通过虚拟文件系统VFS,然后根据不同文件系统的write()方法将数据写入物理设备上。
简单点说,VFS定义了一个通用文件系统的接口层和适配层:
- 接口层: 为用户进程提供一组操作文件/目录/其他对象的统一方法
- 适配层: 和不同的底层文件系统进行适配
虚拟文件系统 VFS 结构
VFS底层由C语言写的,主要通过结构体实现不同数据结构表示不同结构对象。不同操作函数具体的实现依赖于不同的底层文件系统。
在VFS中主要由以下四个主要的对象类型:
- 超级块对象
- 索引节点对象
- 目录项对象
- 文件对象
超级块对象
整个文件系统的第一块空间,代表一个具体已经安装的文件系统,用于保存一个文件系统的所有元数据,如块大小,inode/block的总量、使用量、剩余量,指向空间 inode 和数据块的指针等相关信息,可以说是文件系统的信息库.文件系统的任意元数据修改都要修改超级块,另外为了性能考虑,该超级块对象是常驻内存并被缓存的。
在内核中该对象由结构体super_block表示,其操作由结构体super_operations表示,定义在linux/fs.h中.
struct super_block {
struct list_head s_list; // 指向所有超级块的链表
const struct super_operations *s_op; // 超级块方法
struct dentry *s_root; // 目录挂载点
struct mutex s_lock; // 超级块信号量
int s_count; // 超级块引用计数
......
struct list_head s_inodes; // inode链表
struct mtd_info *s_mtd; // 存储磁盘信息
fmode_t s_mode; // 安装权限
};
索引节点对象
索引节点对象包含了内核在操作文件或目录是需要的全部信息,如文件的长度、创建及修改时间、权限、所属关系等,通过命令ls -li可查看.一个索引节点代表文件系统中(索引节点仅当文件被访问时才在内存中创建)的一个文件,可以是设备或者管道这样的特殊文件.
在内核中该对象由结构体inode表示,其操作由结构体inode_operations表示,定义在linux/fs.h中.
struct inode {
struct hlist_node i_hash; // 散列表,用于快速查找inode
struct list_head i_list; // 索引节点链表
struct list_head i_sb_list; // 超级块链表超级块
struct list_head i_dentry; // 目录项链表
......
uid_t i_uid; // 使用者id
gid_t i_gid; // 使用组id
struct timespec i_atime; // 最后访问时间
struct timespec i_mtime; // 最后修改时间
struct timespec i_ctime; // 最后改变时间
const struct inode_operations *i_op; // 索引节点操作函数
const struct file_operations *i_fop; // 缺省的索引节点操作
struct super_block *i_sb; // 相关的超级块
struct address_space *i_mapping; // 相关的地址映射
struct address_space i_data; // 设备地址映射
unsigned int i_flags; // 文件系统标志
void *i_private; // fs 私有指针
};
目录项对象
在文件路径中,每个部分都是目录项对象.比如/bin/cp中的/,bin,cp都属于目录项对象.另外目录项对象不需要在磁盘中存储因此没有对应的磁盘数据结构,VFS根据字符串形式的路径名现场创建它.
在内核中,该对象由结构体dentry表示,其操作由结构体dentry_operator表示,定义在linux/dcache.h中.
struct dentry {
atomic_t d_count; // 使用计数
unsigned int d_flags; // 目录项标识
spinlock_t d_lock; // 单目录项锁
struct inode *d_inode; // 相关联的索引节点
struct hlist_node d_hash; // 散列表
struct dentry *d_parent; // 父目录的目录项对象
struct qstr d_name; // 目录项名称
struct list_head d_lru; // 未使用的链表
struct list_head d_subdirs; // 子目录链表
struct list_head d_alias; // 索引节点别名链表
unsigned long d_time; // 重置时间
const struct dentry_operations *d_op; // 目录项操作相关函数
......
};
文件对象
文件对象表示进程已经打开的文件,是已打开的文件在内存中的表示,该对象会由相应的open()系统调用创建,由close()系统调用撤销.需要注意的是,由于多个进程可以同时打开和操作同一个文件,这意味同一个文件也可能存在对个对应的文件对象,但由于是同一个文件,其索引节点对象是唯一的,这样就实现了共享同一个磁盘文件.
在内核中,该对象由file结构体表示,其操作由结构体file_operator表示,定义在linux/fs.h中.
struct file {
union {
struct llist_node fu_llist; // 文件对象链表
struct rcu_head fu_rcuhead; // 释放之后的RCU链表
} f_u;
struct path f_path; // 包含的目录项
struct inode *f_inode; // 缓存值
const struct file_operations *f_op; // 文件操作函数
spinlock_t f_lock; // 锁
atomic_long_t f_count; // 文件对象引用计数
......
struct address_space *f_mapping;
};
除了以上四种对象类型外,还需知道以下对象类型的含义:
- address_space:它表示一个文件在页缓存中已经缓存了的物理页。它是页缓存和外部设备中文件系统的桥梁。如果将文件系统可以理解成数据源,那么address_space可以说关联了内存系统和文件系统.
- block:表示实际记录文件的内容,一个文件可能会占用多个 block。
文件打开列表
文件打开列表包含了内核中所有已经打开的文件。每个列表表项是一个文件对象file.在超级块对象结构体(super_block)中存在s_files指针(内核版本3.19之前存在)指向了“已打开文件列表模块”,该链表信息是所有进程共享的。
进程与虚拟文件系统
系统中每一个进程都有自己的一组打开的文件,其中file_struct,fs_struct,`这两个个结构体有效的将进程和VFS联系在一起.内核中使用结构体task_struct表示单个进程的描述符,它包含一个进程的所有信息,如进程的空间地址,挂起信号,进程状态进程号,打开的文件等信息,其定义如下:
struct task_struct {
volatile long state; //进程状态
......
struct fs_struct *fs; // 文件系统信息
struct files_struct *files; // 打开文件信息
......
}
文件描述符
在Linux中,进程是通过文件描述符(file descriptors,简称fd)而不是文件名来访问文件的,文件描述符实际上是一个整数,它本质就是files_struct结构体中fd_array域数组的索引.
这里给大家提个面试的问题: “一个文件描述符,对应几个可读写的文件呢?”
参考文献
https://juejin.im/entry/6844903696229203982
欢迎关注
欢迎关注,一个来自魔都的程序员,小马哥每个月会付我薪水!后台回复加群即可!下期见!