一文讲解Linux内核中根文件系统挂载流程

根文件系统的概念

根文件系统是控制权从linux内核转移到用户空间的一个桥梁。linux内核就类似于一个黑匣子,只向用户提供各种功能的接口,但是功能的具体实现不可见,用户程序通过对这些功能接口的不同整合实现不同的功能需求。以用户的角度来说,应用程序调用内核的接口实现不同的功能,此时系统的控制权在用户手中,但是实际上却是先有内核的初始化提供这些接口,用户才可以使用这些接口的,也就是系统的控制权最初应该属于内核。那么控制权是如何从内核转交到用户的呢?通过调用init程序实现,而一般把存在init程序的文件系统称之为根文件系统。

文件系统是基于物理存储设备至上的一种机制,用于存储空间的管理,并维护文件内容与磁盘单元之间的对应关系,便于对文件内容的访问。由前面所述,init程序存储在文件系统之中,如果需要访问init程序必须能够识别对应文件系统的格式(通过挂载实现),而文件系统又建立在物理存储设备之上,所以需要物理存储设备的驱动程序已准备就绪。

文件系统的挂载需要提供挂载点(挂载目录),linux内核在初始化时会初始化一个虚拟的“/”目录用于根文件系统的挂载,其初始化过程如下:

start_kernel vfs_caches_init() mnt_init() init_rootfs() register_filesystem(&rootfs_fs_type) //注册虚拟的rootfs文件系统 init_mount_tree() //创建“/”目录 bdev_cache_init() chrdev_init() rest_init() kernel_thread(kernel_init, NULL, CLONE_FS) kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

initramfs和initrd

若init程序存在的物理磁盘设备在内核访问时已准备就绪,内核便可以直接挂载根文件系统并运行init程序,实现kernel空间到用户空间的跳转。但是用户程序可能根据不同的需要存储在诸如IDE、SCSI、USB等多种介质当中,如果将所有的驱动程序都编译进内核,将导致内核异常臃肿,因此,为了使内核适应不同的存储介质,同时不将非必要的驱动程序编译进内核,可使用一种过渡根文件系统。过渡根文件系统与linux内核存放在同一个存储设备中,因此可以直接挂载,而此时该文件系统中的init程序只是加载实际根文件系统的驱动和其他的一些初始化工作,待初始化完成,该init程序会挂载实际的根文件系统,并再次跳转到实际根文件系统中执行实际的init程序。

过渡的根文件系统根据是否直接编译进内核分为initramfs和initrd,而initrd根据文件系统的打包格式又分为cpio-initrd和image-initrd,通过cpio打包的文件系统可以直接释放到“/”,而无需挂载过程,initramfs也是cpio的打包格式。

根文件系统的整体挂载流程如下图所示:

首先,内核根据配置利用过渡根文件系统或默认设置初始化“/”目录;然后检测初始化的“/”目录中是否存在指定应用程序(默认为init程序),如果存在,则直接执行该init程序;否则,根据配置挂载实际根文件系统。与之对应的函数流程为:

kernel_init kernel_init_freeable() do_basic_setup() do_initcalls() do_initcall_level(level) [1] do_one_initcall if (!ramdisk_execute_command) //如果没有定义rdinit参数,则默认执行rootfs中的/init程序 ramdisk_execute_command = "/init"; [2] sys_access((const char __user *) ramdisk_execute_command, 0) //查看rootfs是否有/init程序,若包含则直接运行,否则运行prepare_namespace ramdisk_execute_command = NULL; //并将ramdisk_execute_command置为NULL [3] prepare_namespace() //此时已经处于实际根文件系统的目录下 if (ramdisk_execute_command) { //首先尝试运行rdinit=指定的程序 run_init_process(ramdisk_execute_command); [4] if (execute_command) //再尝试运行init=指定的程序 run_init_process(execute_command) if (!try_to_run_init_process("/sbin/init") || //如果没有rdinit= 和 init= 指定的程序,则查找如下的程序运行 !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") ||

【1】初始化“/”目录

内核初始化时会依次调用处于.init段的初始化函数,其中与根文件系统相关的初始化函数为default_rootfs和populate_rootfs,两个函数根据内核配置项的选择决定是否会被运行:

Makefile: obj-y += noinitramfs.o obj-$(CONFIG_BLK_DEV_INITRD) += initramfs.o mounts-$(CONFIG_BLK_DEV_INITRD) += do_mounts_initrd.o initramfs.c rootfs_initcall(populate_rootfs); noinitramfs.c: #if !IS_BUILTIN(CONFIG_BLK_DEV_INITRD) rootfs_initcall(default_rootfs); #endif

由上述代码段可知,在初始化阶段,default_rootfs和papulate_rootfs是互斥的,并由CONFIF_BLK_DEV_INITRD决定哪一个函数会在初始化阶段被执行。CONFIF_BLK_DEV_INITRD配置内核对过渡根文件系统的支持,当配置CONFIF_BLK_DEV_INITRD=y时,包含papulate_rootfs的initramfs.c被编译进内核并在初始化时被执行,并对过渡根文件系统进行解析,而相反的包含default_rootfs的noinitramfs虽然被包含进内核,但是却由于#if语句不会被执行。相反,当CONFIF_BLK_DEV_INITRD不被配置时,default_rootfs会在初始化时被执行,而papulate_rootfs则不会被运行。default_rootfs只是在“/”目录下初始相关节点即完成初始化,如下:

default_rootfs sys_mkdir((const char __user __force *) "/dev", 0755) sys_mknod((const char __user __force *) "/dev/console",S_IFCHR | S_IRUSR | S_IWUSR,new_encode_dev(MKDEV(5, 1))); sys_mkdir((const char __user __force *) "/root", 0700);

papulate_rootfs则对过渡根文件系统进行解析,如果过渡根文件系统是cpio格式的initramfs和initrd,则直接解压并释放到“/”目录中,以initramfs或initrd中的内容对“/”进行初始化,而如果是image格式的initrd则需要通过创建虚拟的ramdisk对过渡根文件系统进行挂载才能访问,具体过程如下:

static int __init populate_rootfs(void) { [1.1] if (do_skip_initramfs) { //跳过initrd过程 if (initrd_start) free_initrd(); return default_rootfs(); //只创建包含基本目录结构的default_rootfs } [1.2] err = unpack_to_rootfs(__initramfs_start, __initramfs_size); //首先尝试解压initramfs到rootfs [1.3] if (initrd_start && !IS_ENABLED(CONFIG_INITRAMFS_FORCE)) { //如果存在initrd #ifdef CONFIG_BLK_DEV_RAM //如果定义了CONFIG_BLK_DEV_RAM,则需要以cpio.initrd和image.initrd两种格式尝试对initrd进行解析 [1.4] err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start); if (!err) { //若以cipo.initrd的格式将initrd解压到rootfs成功,则释放initrd占用的空间 free_initrd(); goto done; } else { clean_rootfs(); unpack_to_rootfs(__initramfs_start, __initramfs_size); //若以cpio.initrd解压到rootfs失败,则以initramfs unpack将rootfs还原到初始状态 } [1.5] fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 0700); //以image.initrd的格式对initrd进行解析 if (fd >= 0) { //将initrd内容复制到rootfs的/initrd.image目录下,即ramdisk中 ssize_t written = xwrite(fd, (char *)initrd_start, initrd_end - initrd_start); sys_close(fd); free_initrd(); //释放initrd占用的内存空间 } done: #else //如果没有定义CONFIG_BLK_DEV_RAM,则只以cpio.initrd的格式解压initrd到rootfs [1.4] err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start); free_initrd(); //释放initrd原来占用的空间 #endif }

当内核支持过渡根文件系统时,由于内核并不知道过渡根文件系统以何种形式存在,所以总是试图以initramfs、cpio-initrd到image-initrd的顺序对过渡根文件系统进行解析/挂载。若采用initramfs的格式对过渡根文件系统进行封装,则需要配置CONFIG_INITRAMFS_SOURCE项,指定过渡根文件系统的路径并将该路径下的内容编译进内核。该路径下的内容可以是一个包含所有过渡根文件系统所需文件的目录、或是一个已将内容打包完成的cpio包,亦或是一个指导内核打包相关文件的配置文件。当路径下的内容为一个目录或一个配置文件时,编译时会自动生成cpio格式的initramfs并包含进内核,而cpio包则需要与cpio-initrd一样预先打包完成。编译出来时,initramfs的位置由内核指定,因此内核可直接通过地址__initramfs_start对其进行访问。即使当该项为空时,内核同样会在该位置编译一个空的包。

【1.1】do_skip_initramfs

do_skip_initramfs为一个布尔变量,默认为0,可以通过kernel cmdline中的skip_initramfs参数将其指定为1,如下:

static int __initdata do_skip_initramfs; static int __init skip_initramfs_param(char *str) { if (*str) return 0; do_skip_initramfs = 1; return 1; } __setup("skip_initramfs", skip_initramfs_param);

当指定该参数为1时,表示内核忽略过渡根文件系统,直接以default_rootfs初始化“/”目录,并直接挂载实际根文件系统。如果此时存在过渡根文件系统,并且过渡根文件系统同样被配置为一个具有实际功能的文件系统(不仅仅起过渡作用),那么可以通过cmdline的skip_initramfs指引内核转移到具有不同功能的用户空间中,如android的recovery模式和boot模式。

【1.2】unpack_to_rootfs(initramfs)

无论内核中是否存在实际的initramfs,内核总是会首先以initramfs的格式试图对其进行解析。

【1.3】initrd解析

initrd与内核一样由uboot加载到内存的指定位置以待处理,此时内核并不知道initrd在内存中的位置,需要通过uboot传递的参数进行指定,在内核中该参数表现为initrd_start变量。当该变量不为0时,表示存在initrd需要解析,而initrd又分为cpio-initrd和image-initrd,cpio-initrd可以直接被解压释放到“/”目录中,而image-initrd则需要借用虚拟设备ramdisk,首先将image-initrd的内容装载到ramdisk中,然后对ramdisk进行挂载,才能对image-initrd中的内容进行访问。

内核是否支持建立虚拟设备ramdisk由CONFIG_BLK_DEV_RAM配置项决定。当配置CONFIG_BLK_DEV_RAM=y时,内核需要分别以cpio-initrd和image-initrd的格式对initrd_start处的内容进行解析(【1.4】和【1.5】),否则只以cpio-initrd的格式进行解析(【1.4】)。

【1.4】unpack_to_rootfs(initrd)

cpio格式的initrd也直接解压并释放到“/”目录中,与initramfs中不同的是,initramfs存在于内核的所占内存,释放到“/”目录之后,原initramfs所占内存不会被释放。而initrd由uboot加载进内存,当initrd被释放到“/”目录之后,initrd所占的内存可以被释放以节约内存空间。

【1.5】image-initrd

若initrd并不是cpio格式,则以image-initrd的格式对其进行解析,此时只是将image-initrd的内容转移到“/initrd.image”,并释放initrd的占用空间,具体的处理过程在prepare_namespace中的load_initrd中。

【2】检查初始化程序是否存在

在无论哪种形式完成“/”目录的初始化之后,检查“/”目录中是否包含初始化init程序,如果有则直接运行该程序并跳转到用户空间。该init程序的具体执行流程由用户决定。如果过渡根文件系统不是最终的根文件系统,用户可通过init程序安装实际根文件系统的驱动并挂载真正的根文件系统。

【3】挂载实际根文件系统

如果“/”目录下不存在初始化程序,则尝试在内核中直接挂载根文件系统,分为两种情况:根文件系统存在于MTD/UBI设备,驱动程序在内核初始化阶段已安装,可直接挂载;根文件系统存在其他存储设备中,以过渡根文件系统安装存储设备的驱动,最后由内核挂载根文件系统。具体过程如下:

void __init prepare_namespace(void) { if (root_delay) { //root_delay由rootdelay= 参数指定,__setup("rootdelay=", root_delay_setup); ssleep(root_delay); //再挂载根文件系统之前等待一段时间,待驱动程序准备就绪 } wait_for_device_probe(); //等待device设备初始化完成 md_run_setup(); dm_run_setup(); if (saved_root_name[0]) { //saved_root_name由root=参数指定,__setup("root=", root_dev_setup) root_device_name = saved_root_name; if (!strncmp(root_device_name, "mtd", 3) || !strncmp(root_device_name, "ubi", 3)) { //若root=指定的根设备为mtd或ubi分区,则直接挂载根文件系统,不经过initrd [3.1] mount_block_root(root_device_name, root_mountflags); goto out; } ROOT_DEV = name_to_dev_t(root_device_name); //获取根设备的设备号 if (strncmp(root_device_name, "/dev/", 5) == 0) //若root=指定的根设备为/dev下的节点,则需要通过initrd挂载真正的根文件系统 root_device_name += 5; } [2.2] if (initrd_load()) //若CONFIG_BLK_DEV_INITRD没有被定义,该函数返回为0 goto out; 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(5); async_synchronize_full(); } is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR; if (is_floppy && rd_doload && rd_load_disk(0)) ROOT_DEV = Root_RAM0; [3.3] mount_root(); //如果不支持initrd,或initrd挂载根文件系统失败,则会直接尝试mount_root挂载实际的根文件系统 out: devtmpfs_mount("dev"); sys_mount(".", "/", NULL, MS_MOVE, NULL); //将当前目录移动到“/”目录下 sys_chroot("."); //将当前目录设置为系统根目录

【3.1】如果根文件系统在MTD/UBI设备上,则直接进行挂载。因为其驱动程序为内核必须,所以在内核初始化时便会加载。此时将根文件系统将被挂载到“/root”下:

mount_block_root: mount_block_root(root_device_name, root_mountflags); do_mount_root(name, p, flags, root_mount_data); sys_mount(name, "/root", fs, flags, data);

【3.2】load_initrd

当根文件系统存储在其他存储设备上时,此时需要利用过渡根文件系统对存储设备进行识别并挂载实际的根文件系统,当然前提是使能了initrd和ramdisk。image-initrd格式的数据并不能直接被释放到“/”目录下,而需要通过挂载来访问,而挂载的前提是存储设备驱动已安装,内核将内存中特定的一段区域抽象为一个虚拟的存储设备以供挂载使用,即ramdisk。将image-initrd中的内容加载到ramdisk中,并通过挂载ramdisk,便能够对image-initrd中的内容进行访问了。如果ramdisk被指定为最终的文件系统存储设备,则此时只是将image-initrd的内容加载到ramdisk中,但并不在此对其进行挂载,而在最终的mount_root对其进行挂载。过程如下:

bool __init initrd_load(void)

{

if (mount_initrd) { //mount_initrd默认为1,可通过noinitrd将其指定为0,__setup("noinitrd", no_initrd);

create_dev("/dev/ram", Root_RAM0); //创建/dev/ram节点

if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) { //如果/initrd.image中存在initrd,则将其装载到/dev/ram中,并且判断/dev/ram是否被指定为最终的文件系统

sys_unlink("/initrd.image"); //如果存在image.initrd,且/dev/ram不是指定的根设备,则需要通过initrd挂载实际的根文件系统

handle_initrd(); //挂载实际根文件系统

return true;

}

}

sys_unlink("/initrd.image"); //如果initrd不存在,或者/dev/ram即为最终根文件系统,则直接返回,通过mount_root进行挂载

return false;

}

【3.3】挂载实际根文件系统

对于存储在除MTD和UBI的其他存储设备的文件系统,若不存在过渡根文件系统,或ramdisk为实际的根文件系统,则尝试直接对实际根文件系统进行挂载。实际上,如果根文件系统所在的驱动程序已经编译进内核,则不需要initramfs和initrd的过渡,内核会跳到此处直接对其进行挂载。

void__initmount_root(void){#ifdef CONFIG_ROOT_NFSif(ROOT_DEV==Root_NFS){if(mount_nfs_root())return;ROOT_DEV=Root_FD0;}#endif#ifdef CONFIG_BLK_DEV_FDif(MAJOR(ROOT_DEV)==FLOPPY_MAJOR){if(rd_doload==2){if(rd_load_disk(1)){ROOT_DEV=Root_RAM1;root_device_name=NULL;}}elsechange_floppy("root floppy");}#endif#ifdef CONFIG_BLOCK{interr=create_dev("/dev/root",ROOT_DEV);mount_block_root("/dev/root",root_mountflags);}#endif}

实际根文件系统挂载到“root”目录下,通过sys_mount的MS_MOVE参数将根文件系统的内容移动到“/”目录下,并且将当前目录切换为系统根目录,即此后从init程序派生的子进程的根目录为当前目录。MS_MOVE参数的意义:

mount--moveolddirnewdirmount-Molddirnewdir这个mount动作是将原来在olddir中的所有文件内容,都显示到newdir中。原文件内容的保存位置不变。此时olddir必须是一个挂载点。

【4】执行用户初始化程序

此时根文件系统已经挂载完成,则查找根文件系统中的初始化程序并执行,init程序一般由kernel cmdline或dts指定:

__setup("init=",init_setup);

当“/”目录下不存在init程序时,内核也会从sbin/etc/bin等其他目录下查找默认的可执行程序。

写在后面

实际上是否采用initramfs/initrd由实际需求而定,当实际存储根文件系统的存储设备驱动已安装时,没必要采用initramfs/initrd的过渡。如果需要initramfs/initrd的过渡,则需要根据以上的根文件系统挂载流程判断使用哪种格式的过渡根文件系统,然后对其进行配置。cpio格式的文件系统根据init程序决定该文件系统是过渡文件系统还是本身已是最终的根文件系统,该init程序的内容由用户自行定义。

init程序为用户空间的第一个程序,是其他所有程序的祖先,可以将init程序成为用户空间的内核,为用户空间其他程序的运行提供基础环境的初始化,init程序在多个init管理系统中呈现不同的运行、配置规则,如sysvinit、busybox init、systemd等,如果我们需要在init程序中添加自己对环境的配置,则需要了解这些init系统的运行规则。

更多Linux内核源码高阶知识请加开发交流Q群篇【318652197】获取,进群免费获取相关资料,免费观看公开课技术分享,入群不亏,快来加入我们吧~

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

【腾讯文档】全网最详Linux内核技术解析【附视频教程和源码资料】



©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容