版本
引用代码版本:linux-3.10.104
功能
- 提供一个通用接口
- 提供各种cache以提高文件系统性能
VFS所处理的系统调用
系统调用名 | 说明 |
---|---|
mount umount umount2 | 安装/卸载文件系统 |
sysfs | 获取文件系统信息 |
ustat statfs fstatfs statfs64 fstatfs64 | 获取文件系统统计信息 |
chroot pivot_root | 更改根目录 |
chdir fchdir getcwd | 对当前目录进行操作 |
mkdir rmdir | 创建、删除目录 |
getdents getdents64 readdir link unlink rename lookup_dcookie | 对目录项进行操作 |
readlink symlink | 对软连接进行操作 |
chown fchown lchown chown16 fchown16 lchown16 | 更改文件所有者 |
chmod fchmod utime | 更改文件属性 |
stat fstat lstat access oldstat oldfstat oldlstat | 读取文件状态 |
stat64 lstat64 fstat64 | |
open close creat umask | 打开、关闭、创建文件 |
dup dup2 fcntl fcntl64 | 对文件描述符进行操作 |
select poll | 等待一组文件描述符上发生的事件 |
truncate ftruncate truncate64 ftruncate64 | 更改文件长度 |
lseek | 更改文件指针 |
read write readv writev sendfile sendfile64 readahead | 进行文件I/O操作 |
io_setup io_submit io_getevents io_cancel io_destroy | 异步I/O |
pread64 pwrite64 | 搜索并访问文件 |
mmap mmap2 munmap madvise mincore remap_file_pages | 处理文件内存映射 |
fdatasync fsync sync msync | 同步文件数据 |
flock | 处理文件锁 |
setxattr lsetxattr fsetxattr getxattr lgetxattr | 处理文件扩展属性 |
fgetxattr listxattr llistxattr flistxattr removexattr | |
lremovexattr fremovexattr |
VFS数据结构
- 超级块对象和inode对象分别对应有物理数据,在磁盘上有静态信息。
- 目录项对象和文件对象描述的是一种关系,前者描述的文件与文件名的关系,后者描述的是进程与文件的关系,所以没有对应物理数据
- 进程每打开一个文件,就会有一个file结构与之对应。同一个进程可以多次打开同一个文件而得到多个不同的file结构,file结构描述被打开文件的属性,如文件的当前偏移量等信息
- 两个不同的file结构可以对应同一个dentry结构。进程多次打开同一个文件时,对应的只有一个dentry结构
- 在存储介质中,每个文件对应唯一的inode结点,但是每个文件又可以有多个文件名。
- Inode中不存储文件的名字,它只存储节点号;而dentry则保存有名字和与其对应的节点号,所以就可以通过不同的dentry访问同一个inode
Super Block
- 超级块用来描述特定文件系统的信息。它存放在磁盘特定的扇区中 ,它在使用的时候将信息存在于内存中
- 当内核对一个文件系统进行初始化和注册时在内存为其分配一个超级块,这就是VFS超级块。即,VFS超级块是各种具体文件系统在安装时建立的,并在这些文件系统卸载时被自动删除
关键成员:
- s_list
所有的超级块形成一个双联表,s_list.prev和s_list.next分别指向与当前超级块相邻的前一个元素和后一个元素 - s_fs_info
字段指向具体文件系统的超级块。
例如:超级块对象指的是Ext2文件系统,该字段就指向ext2_sb_info数据结构 - alloc_super()
超级块对象是通过函数alloc_super()创建并初始化的(由sget()
调用,在fs/super.c
文件中)。在文件系统安装时,内核会调用该函数以便从磁盘读取文件系统超级块,并且将其信息填充到内存中的超级块对象中
include/linux/fs.h
1 | struct super_block { |
super_operation
include/linux/fs.h
1 | struct super_operations { |
Inode
索引节点,索引节点与文件一一对应,并且随文件存在而存在。内存中索引节点由一个inode结构表示。
include/linux/fs.h
1 | struct inode { |
i_state 成员
- I_DIRTY_SYNC
已被改变但不需要同步 - I_DIRTY_DATASYNC
数据部分已被改变 - I_DIRTY_PAGES
有脏的数据页 - I_NEW
索引节点对象已经分配,但还没有用从磁盘索引节点读取来的数据填充 - I_WILL_FREE
即将被释放 - I_FREEING
索引节点对象正在被释放 - I_CLEAR
索引节点对象的内容不再有意义 - I_SYNC
正在同步过程中 - I_REFERENCED
在LRU list中标记inode为最近被引用过 - I_DIO_WAKEUP
Never Set - I_DIRTY
I_DIRTY_SYNC | I_DIRTY_DATASYNC | I_DIRTY_PAGES
inode_operations
include/linux/fs.h
1 | struct inode_operations { |
File
- 文件对象描述进程怎样与一个打开文件进行交互。
- 几个进程可以同时访问一个文件,因此文件指针必须放在文件对象而不是索引节点对象中
include/linux/fs.h
1 | struct file { |
file_operations
include/linux/fs.h
1 | struct file_operations { |
Dentry
VFS把每一个目录看作由若干个目录和文件组成的一个普通文件。
本来inode中应该包括“目录节点”的名称,但由于硬链接的存在,导致一个物理文件可能有多个文件名,因此把和“目录节点”名称相关的部分从 inode 中分开,放在一个专门的 dentry 结构(目录项)中
描述一个文件和一个名字的对应关系,或者说dentry就是一个“文件名”
- 在内存中, 每个文件都至少有一个dentry(目录项)和inode(索引节点)结构
- dentry记录着文件名,上级目录等信息,正是它形成了我们所看到的树状结构
- 有关该文件的组织和管理的信息主要存放inode里面,它记录着文件在存储介质上的位置与分布
include/linux/fs.h
1 | struct dentry { |
dentry的四种状态
- 空闲状态(Free)
该状态的目录对象不包括有效的信息,没有被vfs使用。 - 未使用状态(unused)
该状态的目录对象还没有被内核使用。引用计数器d_count
为0,d_inode
字段仍然指向关联的索引节点,目录对象包含有效的信息。为了在必要时回收内存,它的内容可能被丢弃。 - 正在使用状态(in use)
该状态的目录对象正在被内核使用。引用计数器d_count
大于0,d_inode
字段指向关联的索引节点,目录对象包含有效的信息,不能被丢弃。 - 负状态(negative)
与目录对象关联的索引节点不存在,相应的磁盘索引节点已被删除。d_inode
被置为NULL,该对象仍被保存在目录项高速缓存中,以便同一文件目录名的查找能够快速完成。
dentry_oprations
include/linux/dcache.h
1 | struct dentry_operations { |
Dentry定位文件
首先,通过dir对应的dentry0找到inode0节点,有了inode节点就可以读取目录中的信息。其中包含了该目录包含的下一级目录与文件文件列表,包括name与inode号。
1 | ls -i |
然后,根据通过根据subdir0对应的inode号重建inode2,并通过文件数据(目录也是文件)与inode2重建subdir0的dentry节点:dentry1。
1 | ls -i |
接着,根据file1对应的inode号重建inode4,并通过文件数据与inode4重建file1的dentry节点。最后,就可以通过inode4节点访问文件了。
Dentry Cache
由于从硬盘读入一个目录项并构造相应的目录项对象需要花费大量的时间,也为了最大限度提高目处理这些录项对象的效率,VFS采用了dentry cache的设计。当有用户用ls命令查看某一个目录或用open命令打开一个文件时,VFS会为这里用的每个目录项与文件建立dentry项与inode,即“按需创建”。然后维护一个LRU(Least Recently Used)列表,当Linux认为VFS占用太多资源时,VFS会释放掉长时间没有被使用的dentry项与inode项。
这里的建立于释放是从内存占用的角度看。从Linux角度看,dentry与inode是VFS中固有的东西。所不同的只是VFS是否把dentry与inode读到了内存中。对于Ext2/3文件系统,构建dentry与inode的过程非常简单,但对于其他文件系统,则会慢得多。
Process & File
task_struct (include/linux/sched.h
)1
2
3
4
5
6
7
8
9
10
11
12struct task_struct {
...
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* namespaces */
struct nsproxy *nsproxy;
...
};
进程控制块task_struct
(include/linux/sched.h
)中有两个变量与文件有关:fs
(struct fs_struct
)与files
(struct files_struct
)。
fs_struct (include/linux/fs_struct.h
)1
2
3
4
5
6
7
8struct fs_struct {
int users;
spinlock_t lock;
seqcount_t seq;
int umask;
int in_exec;
struct path root, pwd;
};
path (include/liinux/path.h
)1
2
3
4struct path {
struct vfsmount *mnt;
struct dentry *dentry;
};
fs
中存储着root
与pwd
两个指向dentry
项的指针。用户定路径时,绝对路径会通过root
进行定位;相对路径会通过pwd
进行定位。进程的root
不一定是文件系统的根目录。如ftp进程的根目录不是文件系统的根目录,这样才能保证用户只能访问ftp目录下的内容。files
是一个file object列表,其中每一个节点对应着一个被打开了的文件。
files_struct (include/linux/fdtable.h
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct files_struct {
/*
* read mostly part
*/
atomic_t count;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
当进程定位到文件时,会构造一个file object,并通过f_inode
关联到inode
节点。文件关闭时(close),进程会释放对应对应file object。
fdtable (include/linux/fdtable.h
)1
2
3
4
5
6
7struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
struct rcu_head rcu;
};
fd数组第一个元素[0]是进程的标准输入文件;第二个元素[1]是进程的标准输出文件;第三个元素[2]是进程的标准错误文件。
文件系统类型
特殊文件系统
文件系统 | 安装点 | 说明 |
---|---|---|
bdev | 无 | 块设备 |
binfmt_misc | 任意 | 其他可执行格式 |
devpts | /dev/pts | 伪终端支持 |
eventpollfs | 无 | 由事件轮询机制使用 |
futexfs | 无 | 由futex(快速用户空间加锁)机制使用 |
pipefs | 无 | 管道 |
proc | /proc | 对内核数据结构的常规访问点 |
rootfs | 无 | 为启动阶段提供一个空的根目录 |
shm | 无 | IPC共享线性区 |
mqueue | 任意 | 实现POSIX消息队列时使用 |
sockfs | 无 | 套接字 |
sysfs | /sys | 对系统数据的常规访问点 |
tmpfs | 任意 | 临时文件(若不被交换出去,则常驻内存中) |
usbfs | /proc/bus/usb | USB设备 |
内核给每个安装的特殊文件系统分配一个虚拟的块设备,让其主设备号为0,次设备号有任意值(每个特殊的文件系统有不同的值)
文件系统类型注册
文件系统的源码要么包含在内核映像中,要么作为一个模块被动态装入。每个注册的文件系统都用一个类型为file_system_type
的对象来表示。
file_system_type (include/linux/fs.h
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24struct file_system_type {
const char *name; /* 文件系统名称 */
int fs_flags; /* 文件系统类型标志 */
struct dentry *(*mount) (struct file_system_type *, int, const char *, void *);
void (*kill_sb) (struct super_block *); /* 删除超级块的方法 */
struct module *owner; /* 指向实现文件系统的模块指针 */
struct file_system_type * next; /* 指向文件系统类型链表下一个元素的指针 */
struct hlist_head fs_supers; /* 具有相同文件系统类型的超级块对象链表的头*/
struct lock_class_key s_lock_key;
struct lock_class_key s_umount_key;
struct lock_class_key s_vfs_rename_key;
struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];
struct lock_class_key i_lock_key;
struct lock_class_key i_mutex_key;
struct lock_class_key i_mutex_dir_key;
};
所有文件系统类型的对象都插入道一个单向链表中,由变量file_systems
指向链表的第一个元素,file_systems->next
指向链表的下一个元素。
int register_filesystem(struct file_system_type * fs)
[fs/filesystems.c]
注册一个新的文件系统,传入要注册文件系统的结构体,在file_systems
列表中查找受否与传入fs->name
同名的文件系统,如果存在则返回找到的fs,若不存在将传入文件系统fs
加入file_systems
列表
对比一下ramfs_fs_type
和root_fs_type
1
2
3
4
5
6
7
8
9
10
11static struct file_system_type ramfs_fs_type = {
.name = "ramfs",
.mount = ramfs_mount,
.kill_sb = ramfs_kill_sb,
.fs_flags = FS_USERNS_MOUNT,
};
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.mount = rootfs_mount,
.kill_sb = kill_litter_super,
};
发现fs_flags = FS_USERNS_MOUNT
,这个flag似乎想告诉我们,ramfs是挂载到用户命名空间的,言外之意rootfs不是挂载到用户空间的,那便是内核空间喽。
文件系统处理
- 每个文件系统都有自己的根目录
- 已安装文件系统的根目录隐藏了父文件系统的安装点目录原来的内容
命名空间
通常进程共享一个命名空间,位于系统的根文件系统且被init进程使用的已安装文件系统树。如果clone()
系统调用以CLONE_NEWNS
标志创建一个新进程,那么新进程将获取一个新的命名空间,新进程再创建的新新进程将继承新命名空间。
- fork
fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容 - vfork
vfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行 - clone
有选择性的继承父进程的资源,可以选择像vfork一样和父进程共享一个虚存空间,可以不和父进程共享,甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。灵活性更强。
KeyPoint
启动流程
- start_kernel[init/main.c]
- vfs_caches_init(totalram_pages)[fs/dcache.c]
- dcache_init()[fs/dcache.c]
初始化dentry_cache
和dentry_hashtable
- inode_init()[fs/inode.c]
初始化inode_cachep
和inode_hashtable
- files_init()[fs/file_table.c]
初始化file_cachep
、files_stat.max_files
- mnt_init()[fs/namespace.c]
初始化mnt_cache
,mount_hashtable
,mountpoint_hashtable
- sysfs_init()[fs/sysfs/mount.c]
初始化sysfs_dir_cachep
sysfs_inode_init()[s/sysfs/inode.c] ->bdi_init(&sysfs_backing_dev_info)
register_filesystem(&sysfs_fs_type)
sysfs_mnt = kern_mount(&sysfs_fs_type)
- init_root()[fs/ramfs/inode.c]
bdi_init(&ramfs_backing_dev_info)
register_filesystem(&rootfs_fs_type)
- init_mount_tree()[fs/namespace.c]
挂载rootfs到/
mnt = vfs_kern_mount(type, 0, "rootfs", NULL)
创建namespacens = create_mnt_ns(mnt)
设置init_task
(PID=0)命名空间init_task.nsproxy->mnt_ns = ns
设置当前进程的pwd
和root
set_fs_pwd(current->fs, &root); set_fs_root(current->fs, &root);
- sysfs_init()[fs/sysfs/mount.c]
- bdev_cache_init()[fs/block_dev.c]
初始化bdev_cachep
- mount bdev
- chrdev_init()[fs/char_dev.c]
初始化cdev_map
Mount文件系统
mount时,linux先找到磁盘分区的super block,然后通过解析磁盘的inode table与file data,构建出自己的dentry列表与inode列表。需要注意的是,VFS实际上是按照Ext的方式进行构建的,所以两者非常相似(毕竟Ext是Linux的原生文件系统)。比如inode节点,Ext与VFS中都把文件管理结构称为inode,但实际上它们是不一样的。Ext的inode节点在磁盘上;VFS的inode节点在内存里。Ext-inode中的一些成员变量其实是没有用的,如引用计数等。保留它们的目的是为了与vfs-node保持一致。这样在用ext-inode节点构造vfs-inode节点时,就不需要一个一个赋值,只需一次内存拷贝即可。