由于在下水平相当有限,不当之处,还望大家批评指正^_^
在Linux shell中执行mount命令,通常可以看到某个做了文件系统的磁盘分区或flash分区或内存文件系统做为所谓的根文件系统被mount到了挂载点/处。
实际上内核中最初始的根文件系统,并不是来自内核外部,他是由内核自己构建出来的。
为了说明这个过程,我们先说说mount的过程。
系统调用sys_mount是在fs/namespace.c中实现的,主要工作则由do_mount完成。
一个常规的mount操作大致包含两个动作:
(1)将一个文件系统加载到内核中
注意,这里仅仅是加载。
该动作是由vfs_kern_mount完成的。
每一个文件系统被加载到内核后,内核中都会产生如下几个结构:
一个struct mount结构
一个struct super_block结构
一个struct dentry结构,他是此文件系统的根目录的目录顶,名称为/。注意仅仅是此文件系统的根目录。
struct mount结构中有指针分别指向super_block结构和此文件系统的根目录顶结构。
super_block结构和此文件系统的根目录顶结构也有指针相互指向对方。
另外,struct mount结构中的mnt_devname记录了所mount的块设备文件。
(2)将上一步加载的文件系统,挂接到以/为起点的目录树中的某个位置处。
此工作由do_add_mount完成。
其实就是将此mount结构串接到某个已有的文件系统内的某个目录项上(类型需要为目录)。
实质性的操作函数如下:
void mnt_set_mountpoint(struct mount *mnt,
这里child_mnt为被挂载的文件系统的mount结构。
mp->m_dentry是挂载点路径中最后一个目录对应的目录项。
mnt为mp->m_dentry所在文件系统的mount结构。
另外,mp->m_dentry->d_flags还会被置上 DCACHE_MOUNTED标记,
这个动作完成后,外界就可以访问到这个文件系统了。
这个过程感觉挺复杂,在下对其代码实现理解得也很有限^_^
不过,可以通过open系统调用的实现,看到内核遍历路径的过程中,是如何转向被挂载的文件系统内部的。
下面列出了sys_open的函数调用链(从上到下),
其中最后的函数__lookup_mnt展示了由挂载点目录项查找被挂载的文件系统对应的struct mount结构的过程。
sys_open
do_sys_open
do_filp_open
path_openat
path_openat
link_path_walk
walk_component
lookup_fast
__follow_mount_rcu
__lookup_mnt
前面说了,一个普通的mount包含上述两个步骤。
然而,内核中最初始的根文件系统,由于其特殊性(没有地方可以挂接),所以只执行了上述两步中的第一步。
这里先贴一下相关函数吧^_^
fs/namespace.c
mnt_init
init_mount_tree
核心是init_mount_tree,其代码如下。
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");
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.mnt = mnt;
root.dentry = mnt->mnt_root;
set_fs_pwd(current->fs, &root);
set_fs_root(current->fs, &root);
}
可见内核通过如下方式调用vfs_kern_mount加载了一个文件系统到内核中。
vfs_kern_mount(type, 0, "rootfs", NULL);
特殊之处:一般的mount,设备文件为/dev/path/to/block_dev_file,而这里的设备文件为rootfs。
所以,相应的mnt_devname就是rootfs了。注意,只有这个最早的rootfs对应的块设备文件为rootfs.
文件系统类型type(即名叫rootfs文件系统类型)的实现在哪里呢?
答案是在fs/ramfs/inode.c中。如下即是。
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.mount = rootfs_mount,
.kill_sb = kill_litter_super,
};
而此文件正是ramfs文件系统的实现文件,其结构定义如下。
static struct file_system_type ramfs_fs_type = {
.name = "ramfs",
.mount = ramfs_mount,
.kill_sb = ramfs_kill_sb,
.fs_flags = FS_USERNS_MOUNT,
};
可见初始的rootfs文件系统类型,基本上就是ramfs,超级块的填充、文件系统的操作,与ramfs都是共用同一份代码。
而rootfs包装一个自己的mount函数rootfs_mount,只是为了传个MS_NOUSER标记而已。
对于设备名rootfs,rootfs_mount压根就没用到,而实际上也不存在这个设备。
那么这里的vfs_kern_mount完成后,达成了什么效果呢。
同前面介绍的一样,会产生一个mount结构,一个super_block结构,一个文件系统内的根目录对应的名称为/的目录项。
那么这个文件系统往哪挂呢?没地方挂 ^_^
接下来可以看到,内核的主要操作如下:
ns = create_mnt_ns(mnt);
init_task.nsproxy->mnt_ns = ns;
get_mnt_ns(ns);
root.mnt = mnt;
root.dentry = mnt->mnt_root;
set_fs_pwd(current->fs, &root);
set_fs_root(current->fs, &root);
创建一个mnt_namespace(mount命名空间),将rootfs的mount结构做为其root。
然后将init_task的mount命名空间指定为此命名空间。
然后,将此初始rootfs的根目录设置为当前任务的pwd及root。
好了,至此目录树的起点/算是有了。但是目前rootfs里面还没有内容呢。
接下来start_kernel的流程会顺着rest_init -) kernel_init -) kernel_init_freeable往下走。
那接下来顺着kernel_init_freeable往下看rootfs相关内容。
先是走到do_pre_smp_initcalls,从而调用到了由rootfs_initcall(populate_rootfs);定义的初始化函数populate_rootfs。
如果系统配置了使用initramfs,那么后面populate_rootfs会将内存中的根文件系统压缩包解压到rootfs中。
压缩包起始于__initramfs_start地址处,大小为__initramfs_size。
注意,这只是向初始的rootfs中增加内容,并没有更换rootfs。
具体过程,就是解压压缩包,根据解压出的内容,在初始的根文件系统中创建目录、文件,然后将解压出的文件的内容部分write到创建的文件中。由于已经有根文件系统了,因此相关代码就是通过调用通用的文件操作方面的系统调用,来完成上述任务的。
具体函数有do_name、do_copy、do_symlink。
对于压缩包压缩算法的识别,是由decompress_method函数完成的。
相关代码如下:
struct compress_format {
unsigned char magic[2];
const char *name;
decompress_fn decompressor;
};
static const struct compress_format compressed_formats[] __initconst = {
{ {037, 0213}, "gzip", gunzip },
{ {037, 0236}, "gzip", gunzip },
{ {0x42, 0x5a}, "bzip2", bunzip2 },
{ {0x5d, 0x00}, "lzma", unlzma },
{ {0xfd, 0x37}, "xz", unxz },
{ {0x89, 0x4c}, "lzo", unlzo },
{ {0, 0}, NULL, NULL }
};
decompress_fn __init decompress_method(const unsigned char *inbuf, int len,
const char **name)
{
const struct compress_format *cf;
if (len < 2)
return NULL; /* Need at least this much... */
for (cf = compressed_formats; cf->name; cf++) {
if (!memcmp(inbuf, cf->magic, 2))
break;
}
if (name)
*name = cf->name;
return cf->decompressor;
}
实际上,这里识别的前两个字节,应该就是相应算法的压缩文件的前两个字节。
例如xz文件的前两个字节内容如下(前两个字节正是上面的xz对应的magic数字):
[root@localhost ~]# hexdump -n 2 -C test.tar.xz
00000000 fd 37 |.7|
00000002
[root@localhost ~]#
initramfs环节完成了,继续顺着kernel_init_freeable往下看。
如果ramdisk_execute_command指向的init程序不可访问,
就进入prepare_namespace,但是这个过程涉及到内核命令行参数中与rootfs有关的内容。
例如,noinitrd、 initrd=adrress,size、 root=/dev/ram、root=/dev/mmcblk0p1、root=/dev/nfs等。
这一步做完,初始的根文件系统基本上就被替换掉了。
最终的根文件系统可能是内存文件系统、可能是flash存储介质上的一块区域,也可能是nfs,就看用户的系统是如何定制的了。
如果是内存文件系统,这里应该会创建/dev/ram或/dev/root块设备文件节点,并将之mount为新的rootfs。
实际的过程为:
sys_mount(block_dev_file, "/root", fs, flags, data);
sys_chdir("/root");
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");