文件系统(五)—VFS初始化

本文详细介绍了Linux内核在启动阶段如何初始化虚拟文件系统,包括创建slab缓存、管理散列表、初始化根文件系统以及挂载外部根文件系统。核心步骤涉及dentry和inode实例散列表的初始化,文件结构体slab缓存的创建,以及挂载rootfs和tmpfs。此外,还讨论了如何从initrd或initramfs加载内容到rootfs,以及如何挂载外部介质文件系统作为内核根文件系统。
摘要由CSDN通过智能技术生成

介绍了内核虚拟文件系统的结构,具体文件系统的挂载,本文介绍虚拟文件系统的初始化。虚拟文件系统初始化工作主要包括各数据结构slab缓存的创建、管理散列表的创建,内核初始根文件系统的创建,以及挂载外部根文件系统等。

内核在启动阶段需要对虚拟文件系统进行初始化,内核启动start_kernel函数中调用的与虚拟文件系统初始化相关的函数

asmlinkage void __init start_kernel(void)
{
	……
	vfs_caches_init_early();
	……
	vfs_caches_init(totalram_pages);
	……
}

vfs_caches_init_early()函数在/fs/dcache.c文件内实现,代码如下:

void __init vfs_caches_init_early(void)
{
	dcache_init_early();     //创建并初始化dentry实例全局散列表
	inode_init_early();      //创建并初始化inode实例全局散列表
}
  • dcache_init_early()函数创建并初始化管理dentry实例的全局散列表。
  • inode_init_early()函数创建并初始化管理inode实例的全局散列表。
static void __init dcache_init_early(void)
{
	unsigned int loop;

	/* If hashes are distributed across NUMA nodes, defer
	 * hash allocation until vmalloc space is available.
	 */
	if (hashdist)
		return;
  /* dentry hashtable的空间分配 */
	dentry_hashtable =
		alloc_large_system_hash("Dentry cache",
					sizeof(struct hlist_bl_head),
					dhash_entries,
					13,
					HASH_EARLY,
					&d_hash_shift,
					&d_hash_mask,
					0,
					0);
  /* hashtable的各个链表初始化 */
	for (loop = 0; loop < (1U << d_hash_shift); loop++)
		INIT_HLIST_BL_HEAD(dentry_hashtable + loop);
}
void __init inode_init_early(void)
{
	unsigned int loop;

	/* If hashes are distributed across NUMA nodes, defer
	 * hash allocation until vmalloc space is available.
	 */
	if (hashdist)
		return;
  /* 从cache中分配inode hashtable的内存空间 */
	inode_hashtable =
		alloc_large_system_hash("Inode-cache",
					sizeof(struct hlist_head),
					ihash_entries,
					14,
					HASH_EARLY,
					&i_hash_shift,
					&i_hash_mask,
					0,
					0);
  /* 初始化hashtable 的各个链表 */
	for (loop = 0; loop < (1U << i_hash_shift); loop++)
		INIT_HLIST_HEAD(&inode_hashtable[loop]);
}

vfs_caches_init()函数主要完成虚拟文件系统中数据结构slab缓存的创建,挂载初始化等工作,函数在/fs/dcache.c文件内实现,代码如下

void __init vfs_caches_init(void)
{
	//生成names_cache slab缓存,是VFS为保存路径path信息,而临时使用的内存空间缓存
	//可以通过__getname()/__putname()等函数使用names_cache slab缓存
	names_cachep = kmem_cache_create("names_cache", PATH_MAX, 0,
			SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
	//为支持对目录项的缓存做准备,生成目录项的slab缓存后,构建散列表进行管理
	//此外设置系统可用内存不足时回收目录项缓存的方式
	dcache_init();    //创建dentry结构体slab缓存
	
	//对索引节点执行缓存做准备,和初始化目录项一样,生成索引节点的slab缓存,构建
	//散列表后设置回收方式
	inode_init();    //创建inode结构体slab缓存
	//VFS使用file结构体显示系统打开的文件,为了管理file结构体
	files_init();   //创建file结构体(表示进程文件)slab缓存
	files_maxfiles_init();  //设置files_stat.max_files成员(files_stat_struct实例)
	//Linux系统中所有的文件系统通过VFS层向用户显示,被挂载的文件系统通过vfsmount结构体显示,该结构体还显示VFS层
	//生成vfsmount结构体的mnt_cacha_slab缓存,虚拟系统则初始化sysfs,rootfs
	mnt_init();      //挂载roofs作为初始内核根文件系统
	//Linux将与系统连接的所有设备显示为文件,这些文件就是设备文件,注册块设备驱动
	bdev_cache_init();  //块设备bdev伪文件系统初始化
	//字符设备初始化
	chrdev_init();
}

1 dentry/inode初始化

dentry_cache = KMEM_CACHE(dentry,
		SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|SLAB_MEM_SPREAD|SLAB_ACCOUNT);

	/* Hash may have been set up in dcache_init_early */
	if (!hashdist)
		return;
inode_cachep = kmem_cache_create("inode_cache",
					 sizeof(struct inode),
					 0,
					 (SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|
					 SLAB_MEM_SPREAD|SLAB_ACCOUNT),
					 init_once);

	/* Hash may have been set up in inode_init_early */
	if (!hashdist)
		return;

2 进程文件初始化

进程打开的文件由file结构体表示,它将关联到内核根文件系统中的inode实例。files_init()函数定义在/fs/file_table.c文件内,主要完在file结构体slab缓存的创建,代码如下:

void __init files_init(void)
{ 
  /*创建file结构体slab缓存*/
	filp_cachep = kmem_cache_create("filp", sizeof(struct file), 0,
			SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);
  /*初始化全局变量nr_files*/
	percpu_counter_init(&nr_files, 0, GFP_KERNEL);
}

files_maxfiles_init()函数定义在/fs/file_table.c文件内,代码如下:

void __init files_maxfiles_init(void)
{
	unsigned long n;
	unsigned long memreserve = (totalram_pages - nr_free_pages()) * 3/2;

	memreserve = min(memreserve, totalram_pages - 1);
	n = ((totalram_pages - memreserve) * (PAGE_SIZE / 1024)) / 10;
  /* 设置全局files_stat_struct结构体实例files_stat最大文件数量成员 */
	files_stat.max_files = max_t(unsigned long, n, NR_FILE);
}

内核函数files_maxfiles_init计算系统默认的最大文件数量,依据是每个文件及与其关联的inode和dcache结构,总的大小约为1K。默认文件占用的空间不超出内存的10%,file-max的最小值为NR_FILE,即8192个。

3 挂载初始化

挂载初始化函数mnt_init()主要是创建挂载相关数据结构的slab缓存,创建初始内核根文件系统,此函数在/fs/namespace.c文件内实现,代码如下:

void __init mnt_init(void)
{
	unsigned u;
	int err;
  /*创建挂载mount结构体slab缓存*/
	mnt_cache = kmem_cache_create("mnt_cache", sizeof(struct mount),
			0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);
  /*创建管理mount实例散列表*/
	mount_hashtable = alloc_large_system_hash("Mount-cache",
				sizeof(struct hlist_head),
				mhash_entries, 19,
				0,
				&m_hash_shift, &m_hash_mask, 0, 0);
  /*创建管理mountpoint实例散列表*/
	mountpoint_hashtable = alloc_large_system_hash("Mountpoint-cache",
				sizeof(struct hlist_head),
				mphash_entries, 19,
				0,
				&mp_hash_shift, &mp_hash_mask, 0, 0);

	if (!mount_hashtable || !mountpoint_hashtable)
		panic("Failed to allocate mount hash table\n");
  /*初始化散列表*/
	for (u = 0; u <= m_hash_mask; u++)
		INIT_HLIST_HEAD(&mount_hashtable[u]);
	for (u = 0; u <= mp_hash_mask; u++)
		INIT_HLIST_HEAD(&mountpoint_hashtable[u]);
  /*创建kernfs_node结构体slab缓存(用于kernfs文件系统)*/
	kernfs_init();
  /*sysfs文件系统初始化*/
	err = sysfs_init();
	if (err)
		printk(KERN_WARNING "%s: sysfs_init error: %d\n",
			__func__, err);
  /*在sysfs文件系统根目录下创建fs目录*/
	fs_kobj = kobject_create_and_add("fs", NULL);
	if (!fs_kobj)
		printk(KERN_WARNING "%s: kobj create error\n", __func__);
	init_rootfs();          /*注册rootfs文件系统类型*/
	init_mount_tree();      /*挂载rootfs文件系统作为内核初始根文件系统*/
}

这里需要重点介绍一下init_rootfs()和init_mount_tree()函数,前者用于注册rootfs文件系统类型,后者用于挂载rootfs文件系统作为初始内核根文件系统。

在介绍函数实现前我们先了解一下几个非常重要的文件系统

  • initrd:内存盘,保存内核初始化挂载的内存文件系统。它实际上是保存在内存中的文件系统,独立于内核镜像存在,引导加载程序加载完内核镜像后,还需要将initrd加载到内存,并将起始地址和大小通过命令行参数传递给内核,目前已经被initramfs代替
  • ramfs:基于内存的简易文件系统类型,是完全基于虚拟文件系统数据结构实例的文件系统,由dentry实例表示目录项,inode实例表示节点,文件系统没有大小限制,文件内容不能交换至外部交换区
  • tmpfs:ramfs文件系统类型的增强版,对文件大小进行限制,文件内容可交换至交换区。需选择TMPFS配置选项,不仅可用于内核跟文件系统,还可用进程间通信的共享内存机制等
  • rootfs:初始内核根文件系统类型,可以是ramfs或tmpfs其中之一。内核在以下条件都成立时选择tmpfs作为初始根文件系统类型,否则选用ramfs文件系统类型:
    (1)选择了TMPFS配置选项,支持tmpfs文件系统。
    (2)命令行参数"rootfstype=tmpfs"或未定义。
    (3)命令行参数"root=
    "未定义。*
  • initramfs:保存初始根文件系统内容,它是一个.cpio类型的文件,链接内核时保存在内核镜像的初始化段中。内核在do_basic_setup()函数中,初始化子系统时调用populate_rootfs()函数(/init/initramfs.c)将initramfs的内容解压至根文件系统中。initramf具有默认的内容(/usr/),用户可通过配置选项指定编入其中的文件夹,编译内核时会将指文件夹的内容编译入initramfs内,目标文件格式为.cpio。使用initramfs传递根文件系统内容需要选择BLK_DEV_INITRD配置选项,并在命令行参数中添加"noinitrd"参数。

下面来看一下init_rootfs()函数的实现,函数代码如下(/init/do_mounts.c):

int __init init_rootfs(void)
{
  /*注册rootfs文件系统类型*/
	int err = register_filesystem(&rootfs_fs_type);

	if (err)
		return err;

	if (IS_ENABLED(CONFIG_TMPFS) && !saved_root_name[0] &&
		(!root_fs_names || strstr(root_fs_names, "tmpfs"))) {
		err = shmem_init();  /*注册tmpfs文件系统类型并挂载 */
		is_tmpfs = true;
	} else {
		err = init_ramfs_fs();  /*注册ramfs文件系统类型 */
	}

	if (err)
		unregister_filesystem(&rootfs_fs_type);

	return err;
}

函数内首先注册rootfs文件系统类型,然后判断rootfs文件系统类型是tmpfs还是ramfs,分别对tmpfs或ramfs文件系统进行初始化。rootfs文件系统类型的mount()函数中挂载tmpfs或ramfs文件系统。
注意shmem_init()函数内挂载的tmpfs文件系统并不是作为内核根文件系统,而是用于共享内存等用途。

挂载初始化函数mnt_init()中调用的init_mount_tree()函数用于挂载rootfs文件系统作为初始内核根文件系统,函数在/fs/namespace.c文件内实现:

static void __init init_mount_tree(void)
{
	struct vfsmount *mnt;
	struct mnt_namespace *ns;
	struct path root;
	struct file_system_type *type;
  /*获取文件系统类型实例指针*/
	type = get_fs_type("rootfs");
	if (!type)
		panic("Can't find rootfs type");
  /*挂载rootfs文件系统(没有关联到挂载点)*/
	mnt = vfs_kern_mount(type, 0, "rootfs", NULL);
	put_filesystem(type);
	if (IS_ERR(mnt))
		panic("Can't create rootfs");
  /*创建初始挂载命名空间 */
	ns = create_mnt_ns(mnt);
	if (IS_ERR(ns))
		panic("Can't allocate initial namespace");
  /*初始化进程的相关命名空间*/
	init_task.nsproxy->mnt_ns = ns;
	get_mnt_ns(ns);
  /*更新root的相关字段*/
	root.mnt = mnt;
	root.dentry = mnt->mnt_root;        /*rootfs文件系统根目录项*/
	mnt->mnt_flags |= MNT_LOCKED;

	set_fs_pwd(current->fs, &root);    /*设置init进程当前工作目录为rootfs文件系统根目录*/
	set_fs_root(current->fs, &root);   /*设置init进程根目录为rootfs文件系统根目录*/
}

vfs_kern_mount对rootfs文件系统进行内核挂载,即创建超级块super_block、根目录项dentry、挂载mount的结构体实例,此时内核还不存在跟文件系统,因此无法关联挂载点。
实际上此时创建的rootfs文件系统根目录项,就是初始内核根文件系统的根目录项。此时rootfs文件系统的内容为空,内核在启动后期,初始化子系统时调用**populate_rootfs()**函数将initramfs中的内容解压至rootfs文件系统。

在init_mount_tree()函数最后,将rootfs文件系统根目录项设为内核(init进程)的根目录和当前工作目录,即init进程(内核自身)能看见整个内核根文件系统。

在这里插入图片描述

每个进程有一个根目录和当前工作目录(由fs_struct结构体表示),这两个目录指向内核根文件系统中的一个目录。根目录是进程能看见内核根文件系统的起点,也就是说此目录以上的部分对进程不可见,进程只能看到此目录以下的部分。进程能看到的文件系统是内核根文件系统的一部分。当前工作目录,即在不指定的情况下,进程在当前工作目录下搜索、打开文件等。

在init_mount_tree()函数中将内核init进程的根目录和当前工作目录都设为rootfs文件系统根目录,即init进程可以看见整个内核根文件系统。init进程创建子进程时,其根目录和当前工作目录信息会传递给子进程。

4 挂载外部根文件系统

在挂载初始化函数mnt_init()中已经挂载了rootfs文件系统用为初始内核根文件系统。在启动阶段末期创建的kernel_init内核线程中会将initramfs中的内容导入到根文件系统。kernel_init内核线程随后会判断能否从rootfs文件系统运行第一个用户进程,如果可以则从中运行第一个用户进程(覆盖kernel_init线程),内核启动完成。

从rootfs文件系统中运行第一个用户进程时,可执行文件由命令行参数"rdinit=***“指定,若未指定则设为”/init"。

在这里插入图片描述

从外部介质根文件系统中运行第一个用户进程时,可执行文件由命令行参数"init=***"指定,若未指定则依次尝试运行以下可执行文件:

  • /sbin/init
  • /etc/init
  • /bin/init
  • /bin/sh
    在这里插入图片描述

由上可知,运行第一个用户进程加载可执行文件的优先顺序为:

/rootfs根文件系统/

  • rdinit=***
  • /init

/加载外部介质根文件系统/

  • init=***
  • /sbin/init
  • /etc/init
  • /bin/init
  • /bin/sh

若需要加载外部介质根文件系统,kernel_init线程将调用**prepare_namespace()**函数挂载外部介质文件系统作为内核根文件系统,并从中运行第一个用户进程。

挂载外部介质文件系统相关的命令行参数有:

  • "root="**:块设备文件名,值保存在saved_root_name变量(字符数组)。
  • “ro”,“rw”:只读、读写模式挂载外部介质文件系统,值保存在root_mountflags变量。
  • “rootdelay=***”:延时挂载的时间,等待块设备准备好,值保存在root_delay变量。
  • "rootfstype="**:外部介质文件系统类型,值保存在root_fs_names变量。
  • “rootflags=***”:挂载数据,值保存在root_mount_data变量。
  • “rootwait”:等待挂载介质准备好,类似于"rootdelay=***",内核设置root_wait变量为1。

prepare_namespace()函数定义在/init/do_mounts.c文件内,代码如下:

void __init prepare_namespace(void)
{
	int is_floppy;
  /*延时加载,等待块设备初始化完成,root_delay值由"rootdelay=*"命令行参数传递*/
	if (root_delay) {
		printk(KERN_INFO "Waiting %d sec before mounting root device...\n",
		       root_delay);
		ssleep(root_delay);
	}

  /*等待设备激活 */
	wait_for_device_probe();

	md_run_setup();
  /*如果定义了命令行参数“root=***”*/
	if (saved_root_name[0]) {
		root_device_name = saved_root_name;          /*外部介质设备名称字符串指针*/
		if (!strncmp(root_device_name, "mtd", 3) ||
		    !strncmp(root_device_name, "ubi", 3)) {
      /*如果root参数值为"mtd"或"ubi",运行以下代码*/
			mount_block_root(root_device_name, root_mountflags);
			goto out;
		}
    /*如果root参数值不为"mtd"或"ubi" */
		ROOT_DEV = name_to_dev_t(root_device_name);
		if (strncmp(root_device_name, "/dev/", 5) == 0)
			root_device_name += 5;
	}
  /* 使用initramfs(设置命令行参数“noinitrd”),ininrd_load()返回0 */
	if (initrd_load())
		goto out;

	/* 没有传递root命令行参数 */
	if ((ROOT_DEV == 0) && root_wait) {
		printk(KERN_INFO "Waiting for root device %s...\n",
			saved_root_name);
		while (driver_probe_done() != 0 ||
			(ROOT_DEV = name_to_dev_t(saved_root_name)) == 0)
			msleep(100);
		async_synchronize_full();
	}

	is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;

	if (is_floppy && rd_doload && rd_load_disk(0))
		ROOT_DEV = Root_RAM0;
  /*挂载外部介质文件系统至/root,并设置为线程当前工作目录*/
	mount_root();
out:
	devtmpfs_mount("dev");     /*自动将devtmpfs文件系统挂载至/dev */
  /*移动块设备文件系统至rootfs文件系统根目录(挂载)*/
	sys_mount(".", "/", NULL, MS_MOVE, NULL);
  /*设置/root为kernel_init内核线程根目录,最后传递给用户进程*/
	sys_chroot(".");
}

prepare_namespace()函数将命令行参数"root=***"传递的设备名称转换成块设备号,保存至全局变量ROOT_DEV,随后调用mount_root()函数将外部块设备文件系统挂载至rootfs文件系统/root目录下,并设置为线程当前工作目录,自动挂载devtmpfs文件系统至块设备文件系统/dev目录,将块设备文件系统移动到初始根文件系统根目录(挂载),最后设置块设备文件系统根目录为kernel_init内核线程根目录。至此外部介质文件系统根目录成为kernel_init内核线程根目录和当前工作目录。

### 回答1: Ext4文件系统是一种支持日志操作的Linux文件系统。它支持分层目录结构,以及支持更大文件和分区的大小。要挂载Ext4文件系统,首先需要格式化分区,然后将其挂载到VFS(虚拟文件系统)。格式化分区可以使用以下命令:sudo mkfs -t ext4 /dev/<disk-name>。接下来,可以使用以下命令挂载到VFS:sudo mount -t ext4 /dev/<disk-name> /mnt/<mount-point>。这些命令将Ext4文件系统挂载到VFS,以便可以访问分区内的文件和文件夹。 ### 回答2: ext4文件系统是Linux中最常用的文件系统之一。当我们要将ext4文件系统挂载到VFS(Virtual File System,虚拟文件系统)时,需要经过以下步骤: 1. 首先,需要通过系统调用(syscall)打开ext4文件系统的设备。在打开设备后,系统会为设备创建一个file对象,用于描述该设备。可以在Linux内核的文件`fs/ext4/super.c`中找到相关源代码。 2. 接下来,需要通过调用`mount_bdev()`函数来进行文件系统的挂载。该函数的作用是将设备与文件系统关联起来,创建超级块对象,并将其添加到全局的超级块链表中。详细代码可以在`fs/ext4/super.c`文件中的`ext4_fill_super()`函数中找到。 3. 在`ext4_fill_super()`函数中,还需要解析设备上的ext4文件系统的超级块信息,并进行一系列的初始化操作,如加载块组描述符表、inode表等。相关代码可以在`fs/ext4/super.c`文件中的`ext4_parse_sb()`函数中找到。 4. 通过调用`get_sb_bdev()`函数,将ext4的超级块对象与设备对象(在第一步中创建的file对象)关联起来。该函数会调用`ext4_fill_super()`函数,从而完成挂载过程。相关代码可以在`fs/ext4/super.c`文件中找到。 5. 挂载完成后,VFS会通过将ext4文件系统挂载点与超级块对象关联起来,并维护一系列的内存数据结构,如VFS的super_block结构等。这些数据结构用于管理和操作ext4文件系统的各种操作。 总结起来,ext4文件系统的挂载过程主要涉及打开设备、创建超级块对象、解析超级块信息以及与VFS的关联操作。在源码的层面上,主要通过调用相关的函数完成这些操作,涉及的源代码位于`fs/ext4/super.c`文件中。 ### 回答3: ext4是一种常用的文件系统,在Linux内核中有自己的实现。VFS(Virtual File System)是Linux内核提供的抽象层,用于统一不同文件系统的接口。 源码角度解释ext4文件系统如何挂载到VFS需要了解以下主要步骤: 1. 初始化ext4文件系统:在Linux内核中,ext4的初始化文件系统对象super_block的填充和注册函数init_ext4_fs()完成。这些函数会设置文件系统的相关参数,并创建并初始化超级块(super_block)对象。 2. 注册ext4文件系统:通过调用register_filesystem()函数,将ext4的文件系统类型注册到VFS中。此时,Linux内核就能够识别ext4文件系统。 3. 挂载ext4文件系统:在内核中,挂载操作由mount()函数实现。当用户使用mount命令挂载ext4文件系统时,该命令会调用mount()函数完成挂载过程。在函数中,首先会检查是否具有挂载权限,并调用find_filesystem()函数查找ext4文件系统类型。如果找到,则会调用ext4_mount()函数对具体的ext4文件系统进行挂载。 4. ext4文件系统挂载过程:在ext4_mount()函数中,首先会调用ext4_fill_super()函数填充文件系统的超级块信息。然后,会调用read_super()函数读取磁盘上的超级块,并对文件系统进行一些初始化操作。最后,将文件系统的根目录(root)设置为当前进程的工作目录,并返回挂载成功的信息。 通过以上步骤,ext4文件系统就成功挂载到VFS中了。在挂载完成后,用户可以通过与其他文件系统相同的接口进行文件和目录的访问操作。 总结一下,ext4文件系统的挂载到VFS主要包括初始化ext4文件系统、注册ext4文件系统、挂载ext4文件系统这三个主要步骤。这些步骤通过相应的函数调用和参数设置来实现,在源码层面上保证了ext4文件系统VFS的兼容性和良好的交互。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值