天天看点

linux中vfs和fs区别,解析Linux中的 VFS 文件系统机制(rootfs)一

本文阐述 Linux 中的文件系统部分,源代码来自基于 IA32 的 2.4.20 内核。总体上说 Linux

下的文件系统主要可分为三大块:一是上层的文件系统的系统调用,二是虚拟文件系统 VFS(Virtual Filesystem

Switch),三是挂载到 VFS 中的各实际文件系统,例如 ext2,jffs 等。本文侧重于通过具体的代码分析来解释 Linux

内核中 VFS 的内在机制,在这过程中会涉及到上层文件系统调用和下层实际文件系统的如何挂载。文章试图从一个比较高的角度来解释

Linux 下的 VFS 文件系统机制。

本 文阐述 Linux 中的文件系统部分,源代码来自基于 IA32 的 2.4.20 内核。总体上说 Linux

下的文件系统主要可分为三大块:一是上层的文件系统的系统调用,二是虚拟文件系统 VFS(Virtual Filesystem

Switch),三是挂载到 VFS 中的各实际文件系统,例如 ext2,jffs 等。本文侧重于通过具体的代码分析来解释 Linux

内核中 VFS 的内在机制,在这过程中会涉及到上层文件系统调用和下层实际文件系统的如何挂载。文章试图从一个比较高的角度来解释

Linux 下的 VFS

文件系统机制,所以在叙述中更侧重于整个模块的主脉络,而不拘泥于细节,同时配有若干张插图,以帮助读者理解。

相对来说,VFS 部分的代码比较繁琐复杂,希望读者在阅读完本文之后,能对 Linux 下的 VFS

整体运作机制有个清楚的理解。建议读者在阅读本文前,先尝试着自己阅读一下文件系统的源代码,以便建立起 Linux

下文件系统最基本的概念,比如至少应熟悉 super block, dentry, inode,vfsmount

等数据结构所表示的意义,这样再来阅读本文以便加深理解。

VFS 是一种软件机制,也许称它为 Linux

的文件系统管理者更确切点,与它相关的数据结构只存在于物理内存当中。所以在每次系统初始化期间,Linux 都首先要在内存当中构造一棵

VFS 的目录树(在 Linux 的源代码里称之为 namespace),实际上便是在内存中建立相应的数据结构。VFS 目录树在

Linux 的文件系统模块中是个很重要的概念,希望读者不要将其与实际文件系统目录树混淆,在笔者看来,VFS

中的各目录其主要用途是用来提供实际文件系统的挂载点,当然在 VFS

中也会涉及到文件级的操作,本文不阐述这种情况。下文提到目录树或目录,如果不特别说明,均指 VFS 的目录树或目录。图 1

是一种可能的目录树在内存中的影像:

图 1:VFS 目录树结构

linux中vfs和fs区别,解析Linux中的 VFS 文件系统机制(rootfs)一

这 里的文件系统是指可能会被挂载到目录树中的各个实际文件系统,所谓实际文件系统,即是指VFS

中的实际操作最终要通过它们来完成而已,并不意味着它们一定要存在于某种特定的存储设备上。比如在笔者的 Linux 机器下就注册有

"rootfs"、"proc"、"ext2"、"sockfs" 等十几种文件系统。

在 Linux 源代码中,每种实际的文件系统用以下的数据结构表示:

struct file_system_type {

const char *name;

int fs_flags;

struct super_block *(*read_super) (struct super_block *, void *, int);

struct module *owner;

struct file_system_type * next;

struct list_head fs_supers;

};

注册过程实际上将表示各实际文件系统的 struct file_system_type

数据结构的实例化,然后形成一个链表,内核中用一个名为 file_systems 的全局变量来指向该链表的表头。

在 众多的实际文件系统中,之所以单独介绍 rootfs 文件系统的注册过程,实在是因为该文件系统 VFS 的关系太过密切,如果说

ext2/ext3 是 Linux 的本土文件系统,那么 rootfs 文件系统则是 VFS 存在的基础。一般文件系统的注册都是通过

module_init 宏以及 do_initcalls() 函数来完成(读者可通过阅读module_init 宏的声明及

arch\i386\vmlinux.lds 文件来理解这一过程),但是 rootfs 的注册却是通过 init_rootfs()

这一初始化函数来完成,这意味着 rootfs 的注册过程是 Linux 内核初始化阶段不可分割的一部分。

在2.6中,

kernel_start()

|____start_kernel()

|____vfs_caches_init()(fs/dcache.c)

|____dcache_init()

|____inode_init()

|____files_init()

|____mnt_init()

|

|____init_rootfs()

|

|

|___register_filesystem(&rootfs_fs_type)

|

|____init_mount_tree()

|____bdev_cache_init()

|____chrdev_init()

init_rootfs() 通过调用

register_filesystem(&rootfs_fs_type) 函数来完成 rootfs

文件系统注册的,其中rootfs_fs_type 定义如下:

struct file_system_type rootfs_fs_type = {

name: "rootfs",

read_super: ramfs_read_super,

fs_flags: FS_NOMOUNT|FS_LITTER,

owner: THIS_MODULE,

}

注册之后的 file_systems 链表结构如下图2所示:

图 2: file_systems

链表结构

linux中vfs和fs区别,解析Linux中的 VFS 文件系统机制(rootfs)一

既 然是树,所以根是其赖以存在的基础,本节阐述 Linux 在初始化阶段是如何建立根结点的,即 "/"目录。这其中会包括挂载

rootfs 文件系统到根目录 "/" 的具体过程。构造根目录的代码是在 init_mount_tree() 函数

(fs\namespace.c) 中。

首 先,init_mount_tree() 函数会调用 do_kern_mount("rootfs", 0, "rootfs",

NULL) 来挂载前面已经注册了的 rootfs

文件系统。这看起来似乎有点奇怪,因为根据前面的说法,似乎是应该先有挂载目录,然后再在其上挂载相应的文件系统,然而此时 VFS

似乎并没有建立其根目录。没关系,这是因为这里我们调用的是

do_kern_mount(),这个函数内部自然会创建我们最关心也是最关键的根目录(在 Linux 中,目录对应的数据结构是

struct dentry)。

在这个场景里,do_kern_mount() 做的工作主要是:

1)调用 alloc_vfsmnt() 函数在内存里申请了一块该类型的内存空间(struct vfsmount

*mnt),并初始化其部分成员变量。(mnt : a

vfs-internal representation of a mount point)

2) 调用 get_sb_nodev() 函数在内存中分配一个超级块结构 (struct super_block)

sb,并初始化其部分成员变量,将成员 s_instances 插入到 rootfs 文件系统类型结构中的 fs_supers

指向的双向链表中。

(sb:描述文件系统信息,例如块大小、文件系统版本号、上次mount 的时间等等。)

3) 通过 rootfs 文件系统中的 read_super 函数指针调用 ramfs_read_super()

函数。还记得当初注册rootfs 文件系统时,其成员 read_super 指针指向了 ramfs_read_super()

函数,参见图2.

4) ramfs_read_super() 函数调用 ramfs_get_inode() 在内存中分配了一个 inode 结构

(struct inode) inode,并初始化其部分成员变量,其中比较重要的有 i_op、i_fop 和 i_sb:

inode->i_op = &ramfs_dir_inode_operations;

inode->i_fop = &dcache_dir_ops;

inode->i_sb = sb;

(一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等),权限,文件大小,创建/修改/访问时间等,也就是ls

-l 命令看到的那些信息,这些信息存在inode

中而不是数据块中。每个文件都有一个inode,一个块组中的所有 inode组成了inode表。)

这使得将来通过文件系统调用对 VFS 发起的文件操作等指令将被 rootfs 文件系统中相应的函数接口所接管。

图3

linux中vfs和fs区别,解析Linux中的 VFS 文件系统机制(rootfs)一

5) ramfs_read_super() 函数在分配和初始化了 inode 结构之后,会调用 d_alloc_root()

函数来为 VFS的目录树建立起关键的根目录 (struct dentry)dentry,并将 dentry 中的 d_sb 指针指向

sb,d_inode 指针指向 inode。

(对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件名之外,ls

-l 命令看到的其它信息都保存在该文件的inode中。注意这个概念:目录也是一种文件,是一种特殊类型的文件。)

6) 将 mnt 中的 mnt_sb 指针指向 sb,mnt_root 和 mnt_mountpoint 指针指向

dentry,而 mnt_parent指针则指向自身。

init_task是系统中的0号进程,也就是第一个进程,这个进程永远不会被撤消,它的描述符被静态的分配到内核数据段中,也就是说init_task的进程描述符是预先由编译器分配的,在运行的过程中保持不变,而其它描述符是在运行的过程中,由系统根据当前的内存状况随机分配的,撤消的时再在归还给系统。

init_task进程0,(start_kernel()->sched_init()->init_idle(current)????)总结一下有如下几个要点:

1. 进程0是所有其他进程的祖先,

也称作idle进程或swapper进程。

2. 进程0是在系统初始化时由kernel自身从无到有创建。(过程集中在start_kernel函数内)

3. 进程0的数据成员大部分是静态定义的,即由预先定义好的(如上)INIT_TASK, INIT_MM等宏初始化。

进程0的描述符init_task定义在arch/mips/kernel/init_task.c,由INIT_TASK宏初始化。

init_mm等结构体定义在include/linux/init_task.h内,为init_task成员的初始值,分别由对应的初始化宏如INIT_MM等初始化,

进程1:

进程0最终会通过调用kernel_thread创建一个内核线程去执行init函数,这个新创建的内核线程即Process

1(这时还是共享着内核线程0的资源属性如地址空间等)。init函数继续完成剩余的内核初始化,并在函数的最后调用execve系统调用装入用户空间的可执行程序/sbin/init,这时进程1就拥有了自己的属性资源,成为一个普通进程(init进程)。至此,内核初始化和启动过程结束。下面就进入了用户空间的初始化,最后运行shell登陆界面。

(注:Init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。)

进程1的创建过程如下:

start_kernel

-> rest_init -> kernel_thread

-> init

static void noinline rest_init(void)

__releases(kernel_lock)

{

system_state = SYSTEM_BOOTING_SCHEDULER_OK;

kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);

numa_default_policy();

unlock_kernel();

__preempt_enable_no_resched();

schedule();

preempt_disable();

cpu_idle();

}

执行schedule()启动进程1后,进程0便调用cpu_idle()函数进入睡眠状态。

下面看一下kernel_thread函数

pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned

long flags)

{

struct pt_regs regs;

long

pid;

memset(&regs, 0, sizeof(regs));

regs.ARM_r1 = (unsigned long)arg;

regs.ARM_r2 = (unsigned long)fn;

regs.ARM_r3 = (unsigned long)do_exit;

regs.ARM_pc = (unsigned long)kernel_thread_helper;

regs.ARM_cpsr = SVC_MODE;

pid

= do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, &regs,

0, NULL, NULL);

MARK(kernel_thread_create, "%ld %p", pid, fn);

return pid;

}

可以发现kernel_thread 最终也是调用的do_fork完成内核线程的创建。

do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, pregs, 0, NULL,

NULL);

flag参数标志决定了新创建的进程的行为方式以及父子进程之间共享的资源种类,这里用到的几个flag标志意思如下:

CLONE_FS 父子进程共享文件系统信息

CLONE_SIGHAND 父子进程共享信号处理函数

CLONE_VM 父子进程共享地址空间

CLONE_UNTRACED 防止跟踪进程在子进程上强制执行CLONE_PTRACE

看看新创建的内核线程1(Process 1)做了些什么事: ( 即init函数)

static int init(void * unused)

{

lock_kernel();

set_cpus_allowed(current, CPU_MASK_ALL);

child_reaper = current;

smp_prepare_cpus(max_cpus);

init_hardirqs();

do_pre_smp_initcalls();

smp_init();

sched_init_smp();

cpuset_init_smp();

……

run_init_process("/sbin/init");

run_init_process("/etc/init");

run_init_process("/bin/init");

run_init_process("/bin/sh");

panic("No init found. Try passing init= option to

kernel.");

}

这里的 run_init_process 实际上就是通过execve()来运行 init

程序。这里首先运行/sbin/init,如果失败再运行/etc/init,然后是/bin/init,然后是/bin/sh(也就是说,init

可执行文件可以放在上面代码中的4 个目录中都可以),如果都失败,则可以通过在系统

启动时在添加的启动参数来指定init,比如init=/ linuxrc。

run_init_process源码如下,最终通过execve

系统调用执行/sbin/init程序。

static void run_init_process(char

*init_filename)

{

argv_init[0] = init_filename;

execve(init_filename, argv_init, envp_init);

}

Exec函数族(execve为其中一种)提供了一个在进程中启动另外一个程序执行的方法。它可以根据指定的文件名或目录找到可执行文件,并用它来取代原先调用进程的属性包括数据段,代码段和堆栈段等。在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。这里在执行execve之前,进程1还是共享着内核线程0资源属性的内核线程。执行execve后(即执行了用户空间的init程序),此时,进程1就拥有了自己的地址空间等属性,成为一个普通进程。

这 样,当 do_kern_mount() 函数返回时,以上分配出来的各数据结构和 rootfs 文件系统的关系将如上图 3

所示。图中 mnt、sb、inode、dentry

结构块下方的数字表示它们在内存里被分配的先后顺序。限于篇幅的原因,各结构中只给出了部分成员变量,读者可以对照源代码根据图中所示按图索骥,以加深理

解。

最后,init_mount_tree() 函数会为系统最开始的进程(即 init_task

进程)准备它的进程数据块中的namespace 域,主要目的是将 do_kern_mount() 函数中建立的 mnt 和

dentry 信息记录在了 init_task 进程的进程数据块中,这样所有以后从 init_task 进程 fork

出来的进程也都先天地继承了这一信息,在后面用sys_mkdir 在 VFS

中创建一个目录的过程中,我们可以看到这里为什么要这样做。为进程建立 namespace 的主要代码如下:

namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);

list_add(&mnt->mnt_list, &namespace->list);

//mnt is returned by do_kern_mount()

namespace->root = mnt;

init_task.namespace = namespace;

for_each_task(p) {

get_namespace(namespace);

p->namespace = namespace;

}

set_fs_pwd(current->fs, namespace->root, namespace->root->mnt_root);

set_fs_root(current->fs, namespace->root, namespace->root->m

该段代码的最后两行便是将 do_kern_mount() 函数中建立的 mnt 和 dentry 信息记录在了当前进程的 fs结构中。

以 上讲了一大堆数据结构的来历,其实最终目的不过是要在内存中建立一颗 VFS 目录树而已,更确切地说,

init_mount_tree() 这个函数为 VFS 建立了根目录

"/",而一旦有了根,那么这棵数就可以发展壮大,比如可以通过系统调用 sys_mkdir

在这棵树上建立新的叶子节点等,所以系统设计者又将 rootfs 文件系统挂载到了这棵树的根目录上。关于 rootfs

这个文件系统,读者如果看一下前面图 2 中它的file_system_type 结构,会发现它的一个成员函数指针 read_super

指向的是 ramfs_read_super,单从这个函数名称中的

ramfs,读者大概能猜测出这个文件所涉及的文件操作都是针对内存中的数据对象,事实上也的确如此。从另一个角度而言,因为 VFS

本身就是内存中的一个数据对象,所以在其上的操作仅限于内存,那也是非常合乎逻辑的事。在接下来的章节中,我们会用一个具体的例子来讨论如何利用

rootfs所提供的函树为 VFS 增加一个新的目录节点。

VFS

中各目录的主要用途是为以后挂载文件系统提供挂载点。所以真正的文件操作还是要通过挂载后的文件系统提供的功能接口来进行。

在2.6中,

init_mount_tree()(fs/namespace.c)

|____do_kern_mount("rootfs",0,"rootfs",0)(fs/super.c)

|____vfs_kern_mount("rootfs")(fs/super.c)

|___mnt=alloc_vfsmnt("rootfs")(分配mnt结构)

|___get_sb(fs_type,...,ramfs_fill_super,mnt)

(就是调用rootfs_get_sb(),get_sb_nodev(),ramfs_fill_super()分配sb块)

|___inode=ramfs_get_inode(sb,...)(分配inode)

|___root=d_alloc_root(inode)(分配root结构,填充'/'目录)

注意:start_kernel()

-->...-->kernel_thread("init",***)-->init()

(在运行内核init前,根文件系统已经建立起来了)-->do_basic_setup();(调用rootfs_initcall()由cpio档填充文件系统内容,并作为根文件系统)-->如果在文件系统中发现了/init文件,交给init处理,否则调用prepare_namespace()继续装载其它文件系统,并mount到之前的rootfs上,并作为根文件系统和运行其上的/init)