介绍了内核虚拟文件系统的结构,具体文件系统的挂载,本文介绍虚拟文件系统的初始化。虚拟文件系统初始化工作主要包括各数据结构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内核线程根目录和当前工作目录。