Linux 系统根目录的构建过程

文章目录

  • [1. 前言](#1. 前言)
  • [2. Linux 系统根目录的构建过程](#2. Linux 系统根目录的构建过程)
    • [2.1 构建临时 RAM rootfs](#2.1 构建临时 RAM rootfs)
    • [2.2 加载、切换到磁盘 rootfs](#2.2 加载、切换到磁盘 rootfs)
  • [3. 参考资料](#3. 参考资料)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. Linux 系统根目录的构建过程

2.1 构建临时 RAM rootfs

这是一个过渡时期,这就像先有鸡还是先有蛋一样:系统需要加载位于磁盘上的 rootfs,然后启动位于指定目录下的 init 程序,但这时候还没有文件系统目录树结构,也就没法通过文件路径来加载 init 程序。Linux 内核通过在 RAM 内存里面,临时构建一个文件系统目录树,来解决这一问题。

  • 构建 RAM rootfs 的根目录
c 复制代码
start_kernel()
	vfs_caches_init()
		mnt_init()
			init_rootfs(); /* 注册 rootfs, ramfs 两种文件系统类型 */
			/*
			 * 初始化系统文件系统 mount 树:
			 * - 挂载 系统初始 rootfs, rootfs 是系统中第一个挂载的文件系统
			 * - 设置 系统初始根路径 和 当前路径(pwd) 为 rootfs 的 根目录
			 * 构建了系统文件系统的 根.
			 */
			init_mount_tree();

注册 RAM rootfs:

c 复制代码
// init/do_mounts.c
static struct file_system_type rootfs_fs_type = {
	.name		= "rootfs",
	.mount		= rootfs_mount,
	.kill_sb	= kill_litter_super,
};

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

	...

	return err;
}

构建、设置 RAM rootfs 的根目录、当前目录:

c 复制代码
/*
 * 初始化系统文件系统 mount 树:
 * - 挂载 系统初始 rootfs, rootfs 是系统中第一个挂载的文件系统
 * - 设置 系统初始根路径 和 当前路径(pwd) 为 rootfs 的 根目录
 * 构建了系统文件系统的 根.
 */
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"); /* &rootfs_fs_type */
	if (!type)
		panic("Can't find rootfs type");
	/*
	 * 挂载 rootfs 文件系统:
	 * - 创建 super block
	 * - 设置 super_block::s_op 操作接口
	 * - 设置 super block 的 block size 等参数
	 * - 创建 root inode, dentry
	 * - 分配 vfsmount 对象, 记录 mount 信息
	 * 从 vfsmount 对象返回 mount 信息, 包括:
	 * - 挂载的根目录: vfsmount::mnt_root
	 * - 挂载的 super block: vfsmount::mnt_sb
	 * - mount point: 也即 mnt->mnt.mnt_root (struct mount)
	 * - mount parent (struct mount)
	 */
	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; /* 系统初始根路径为 mount 的 rootfs 的 根目录 */
	mnt->mnt_flags |= MNT_LOCKED;

	/* 设置 系统初始根路径 和 当前路径(pwd) */
	set_fs_pwd(current->fs, &root); /* 设置 当前进程 所在 fs 当前路径(pwd) 为 系统 初始 根路径 */
	set_fs_root(current->fs, &root); /* 设置 当前进程 所在 fs 根路径 为 系统 初始 根路径 */
}
  • 用 initramfs 或 initrd 镜像构建 RAM rootfs 目录树

系统启动期间,需要一些目录、文件结构让系统过渡到真正的磁盘 rootfs,来看代码细节:

c 复制代码
start_kernel()
	reset_init()
		pid = kernel_thread(kernel_init, NULL, CLONE_FS);

kernel_init()
	kernel_init_freeable()
		do_basic_setup()
			...
			populate_rootfs()

populate_rootfs() 展开了 initramfs 或 initrd 镜像,构建 RAM rootfs 目录树:

c 复制代码
/* 从 initramfs cpio 镜像, 或 initrd 镜像, 构建 initramfs rootfs 文件目录树 */
static int __init populate_rootfs(void)
{
	/* 解压缩内置的 initramfs cpio 镜像, 构建 initramfs rootfs 文件目录树 */
	char *err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
	if (err) /* 解压缩内置的 initramfs 失败 */
		panic("%s", err); /* Failed to decompress INTERNAL initramfs */
	/* If available load the bootloader supplied initrd */
	/* 如果内核使用旧式的 initrd, 而不是 initramfs, 则尝试解压缩 initrd */
	if (initrd_start && !IS_ENABLED(CONFIG_INITRAMFS_FORCE)) {
#ifdef CONFIG_BLK_DEV_RAM
		int fd;
		printk(KERN_INFO "Trying to unpack rootfs image as initramfs...\n");
		/* 解压缩 initrd 镜像, 构建 initramfs rootfs 文件目录树 */
		err = unpack_to_rootfs((char *)initrd_start,
			initrd_end - initrd_start);
		if (!err) { /* 构建 initramfs rootfs 文件目录树成功, 释放不再需要的 initrd 空间 */
			free_initrd();
			goto done; /* 构建 initramfs rootfs 文件目录树成功, 返回 */
		} else {
			clean_rootfs();
			unpack_to_rootfs(__initramfs_start, __initramfs_size);
		}
		/* 构建 initramfs rootfs 失败, 可能是格式不对 */
		printk(KERN_INFO "rootfs image is not initramfs (%s)"
				"; looks like an initrd\n", err);
		/* 尝试打开 /initrd.image 镜像再次构建 */
		fd = sys_open("/initrd.image",
			      O_WRONLY|O_CREAT, 0700);
		if (fd >= 0) {
			ssize_t written = xwrite(fd, (char *)initrd_start,
						initrd_end - initrd_start);

			if (written != initrd_end - initrd_start)
				pr_err("/initrd.image: incomplete write (%zd != %ld)\n",
				       written, initrd_end - initrd_start);

			sys_close(fd);
			free_initrd();
		}
	done:
		/* empty statement */;
#else
		printk(KERN_INFO "Unpacking initramfs...\n");
		err = unpack_to_rootfs((char *)initrd_start,
			initrd_end - initrd_start);
		if (err)
			printk(KERN_EMERG "Initramfs unpacking failed: %s\n", err);
		free_initrd();
#endif
	}
	flush_delayed_fput();
	/*
	 * Try loading default modules from initramfs.  This gives
	 * us a chance to load before device_initcalls.
	 */
	load_default_modules(); /* 从 initramfs 加载一些缺省的 modules */

	return 0;
}

一个好的 initramfs 或 initrd 镜像,应该要包含 /dev/console 字符设备节点:

c 复制代码
static noinline void __init kernel_init_freeable(void)
{
	...
	/* ...
	 * populate_rootfs(): 从 initramfs cpio 镜像, 或 initrd 镜像, 构建 initramfs rootfs 文件目录树
	 * ... 
	 */
	do_basic_setup();
	...

	/* Open the /dev/console on the rootfs, this should never fail */
	if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
		pr_err("Warning: unable to open an initial console.\n");

	...
}

initramfs/initrd 可以内置到内核,也可以是一个独立的文件。本小节来看一个 initramfs 镜像的例子:

bash 复制代码
$ file kernel/linux-4.9/output/rootfs.cpio.gz
kernel/linux-4.9/output/rootfs.cpio.gz: gzip compressed data, last modified: Mon Apr  6 14:59:42 2026, from Unix
$ cd kernel/linux-4.9/output
$ gunzip rootfs.cpio.gz
$ cpio -idmv < rootfs.cpio
$ tree
.
├── bin
│   ├── ash -> busybox
│   ├── busybox
│   ├── cat -> busybox
│   ├── catv -> busybox
│   ├── chattr -> busybox
│   ├── chgrp -> busybox
│   ├── chmod -> busybox
│   ├── chown -> busybox
│   ├── cp -> busybox
│   ├── cpio -> busybox
│   ├── date -> busybox
│   ├── dd -> busybox
│   ├── df -> busybox
│   ├── dmesg -> busybox
│   ├── dnsdomainname -> busybox
│   ├── dumpkmap -> busybox
[......]

2.2 加载、切换到磁盘 rootfs

如果 Linux 内核命令行参数包含 root=/dev/raminitrd,且 initrd 或 initramfs 镜像中的 init 程序,没有进一步启动磁盘上 rootfs 镜像包含的 init 程序,则 Linux 系统使用 initrd/initramfs rootfs 运行;相反,则会切换到磁盘上的 rootfs 运行。来看代码细节,接前面的流程,现在已经有了 RAM rootfs,且从 initrd/initramfs 镜像构建了目录树,当前目录是 RAM rootfs 的根目录

c 复制代码
static noinline void __init kernel_init_freeable(void)
{
	...

	/* ...
	 * populate_rootfs(): 从 initramfs cpio 镜像, 或 initrd 镜像, 构建 initramfs rootfs 文件目录树
	 * ... 
	 */
	do_basic_setup();

	...

	/* Open the /dev/console on the rootfs, this should never fail */
	/*
	 * 打开 RAM rootfs 的控制台设备文件节点, 作为首进程(系统的第一个进程 init_task) 的 stdin。
	 * 包括后面打开的 stdout, stderr, 都会被以后的子进程继承。
	 * 第一个打开的文件 fd == 0。
	 */
	if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
		pr_err("Warning: unable to open an initial console.\n");

	(void) sys_dup(0); /* 首进程的 stdout */
	(void) sys_dup(0); /* 首进程的 stderr */
	/*
	 * check if there is an early userspace init.  If yes, let it do all
	 * the work
	 */

	/* 如果找不到 ramdisk 的 init 程序, 加载 rootfs, 并启动 rootfs 的 init 程序, 进入用户空间 */
	if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
		ramdisk_execute_command = NULL;
		prepare_namespace(); /* 加载 根文件系统 等等 */
	}

	...
}

prepare_namespace() 处理了各种 rootfs 设备场景,这里之分析内核命令行参数 "root=/dev/mmcblk0p4 init=/init" 这一个场景:

c 复制代码
/*
 * Prepare the namespace - decide what/where to mount, load ramdisks, etc.
 */
void __init prepare_namespace(void)
{
	...

	/* 等待设备驱动加载完成 */
	wait_for_device_probe();

	...

	// saved_root_name[]: "root=/dev/mmcblk0p4"
	if (saved_root_name[0]) { /* 通过内核命令行参数 "root=/dev/mmcblk0p4" 指定了根文件系统设备 */
		root_device_name = saved_root_name; // @root_device_name => "/dev/mmcblk0p4"
		if (!strncmp(root_device_name, "mtd", 3)/* "root=mtdXXX" 形式 */ ||
		    !strncmp(root_device_name, "ubi", 3)/* "root=ubiXXX" 形式 */) {
			... // 不是本文分析的场景
		}
		ROOT_DEV = name_to_dev_t(root_device_name);
		if (strncmp(root_device_name, "/dev/", 5) == 0) /* "root=/dev/XXX" 形式 */
			root_device_name += 5; /* 跳过 "/dev/": root_device_name => "mmcblk0p4" */
	}

	...

	mount_root(); /* 挂载 新的 rootfs: 建立 super block 和 根目录 */
out:
	devtmpfs_mount("dev"); /* 将 devtmpfs 重新 挂载到 新 rootfs 的 /dev 目录 */
	sys_mount(".", "/", NULL, MS_MOVE, NULL); /* 从系统旧的 RAM rootfs 切换到 新的 rootfs */
	sys_chroot("."); /* 切换到 新 rootfs 的根目录 */
}

挂载新的 rootfs,从 sys_mount() 的 MS_MOVE 参数可知,这是一次文件系统的迁移(从 RAM rootfs 迁移到磁盘 rootfs),而不是新的挂载:

c 复制代码
sys_mount(".", "/", NULL, MS_MOVE, NULL)
	do_mount()

long do_mount(const char *dev_name, const char __user *dir_name,
		const char *type_page, unsigned long flags, void *data_page)
{
	...
	if (flags & MS_REMOUNT) // remount
		retval = do_remount(&path, flags, sb_flags, mnt_flags,
				    data_page);
	else if (flags & MS_BIND)
		retval = do_loopback(&path, dev_name, flags & MS_REC);
	else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))
		retval = do_change_type(&path, flags);
	else if (flags & MS_MOVE) // move mount:我们这里的场景
		retval = do_move_mount(&path, dev_name);
	else // new mount
		retval = do_new_mount(&path, type_page, sb_flags, mnt_flags,
				      dev_name, data_page);
	...
}

然后通过 sys_chroot(),设置(当前进程)的 root 目录到新的磁盘 rootfs 的根目录:

c 复制代码
// fs/open.c

SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
	struct path path;
	int error;
	unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
retry:
	error = user_path_at(AT_FDCWD, filename, lookup_flags, &path);
	if (error)
		goto out;

	...

set_fs_root(current->fs, &path); /* 切换新的 root 目录 */
	error = 0;
dput_and_out:
	path_put(&path);
	...
out:
	return error;
}

void set_fs_root(struct fs_struct *fs, const struct path *path)
{
	struct path old_root;

	path_get(path);
	spin_lock(&fs->lock);
	write_seqcount_begin(&fs->seq);
	old_root = fs->root;
	fs->root = *path; /* 新的 root 目录 */
	write_seqcount_end(&fs->seq);
	spin_unlock(&fs->lock);
	if (old_root.dentry)
		path_put(&old_root);
}

最后,在这里顺便也看一下 init 程序的加载过程:

c 复制代码
static int __ref kernel_init(void *unused)
{
	...
	kernel_init_freeable();
	...

	/* 如果 ramdisk 里有 @ramdisk_execute_command 指向的 init 程序文件, 则启动它进入用户空间 */
	if (ramdisk_execute_command) {
		ret = run_init_process(ramdisk_execute_command);
		if (!ret)
			return 0;
		...
	}

	/*
	 * ramdisk 里没有 @ramdisk_execute_command 指向的 init 程序文件, 
	 * 则运行 "init=XXX" 指定的 init 程序.
	 */
	if (execute_command) {
		ret = run_init_process(execute_command);
		if (!ret)
			return 0;
		panic("Requested init %s failed (error %d).",
		      execute_command, ret);
	}

	/*
	 * 如果 没有通过 "init=XXX" 指定的 init 程序, 则依次使用以下几个默认的 
	 * init 程序路径:
	 * /sbin/init
	 * /etc/init
	 * /bin/init
	 * /bin/sh
	 */
	if (!try_to_run_init_process("/sbin/init") ||
	    !try_to_run_init_process("/etc/init") ||
	    !try_to_run_init_process("/bin/init") ||
	    !try_to_run_init_process("/bin/sh"))
		return 0;

	panic("No working init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/admin-guide/init.rst for guidance.");
}

static noinline void __init kernel_init_freeable(void)
{
	...
	if (!ramdisk_execute_command)
		ramdisk_execute_command = "/init"; /* ramdisk 的 init 程序默认路径 */

	/* 如果找不到 ramdisk 的 init 程序, 加载 rootfs, 并启动 rootfs 的 init 程序, 进入用户空间 */
	if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
		ramdisk_execute_command = NULL;
		prepare_namespace(); /* 加载 根文件系统 等等 */
	}
	...
}

从以上代码逻辑可以看到,init 程序可以分别从 initramfs/initrd 镜像、磁盘 rootfs 加载,它们之间存在一定的优先顺序:

  • 如果 initramfs/initrd 镜像包含 init 程序,则从 initramfs/initrd "rdinit=XXX" 的指定路径默认路径 /init 路径加载 init,同时不挂载磁盘 rootfs
  • 如果 initramfs/initrd 镜像不包含 init 程序,则挂载磁盘 rootfs,然后从磁盘 rootfs 的几种不同路径,依次尝试加载 init

3. 参考资料

1\] [使用初始 RAM 磁盘 (initrd)](https://linuxkernel.org.cn/doc/html/latest/admin-guide/initrd.html "使用初始 RAM 磁盘 (initrd)") \[2\] [构建初始内存盘 (initrd, init ram disk)](https://osh-2020.github.io/lab-1/initrd/ "构建初始内存盘 (initrd, init ram disk)") \[3\] [initrd和initramfs实操](https://www.cnblogs.com/xiaomanblog/articles/18248227 "initrd和initramfs实操")

相关推荐
Harvy_没救了2 小时前
Vim 快捷键手册
linux·编辑器·vim
C^h2 小时前
RT thread使用u8g2点亮oled显示屏
linux·单片机·嵌入式硬件·嵌入式
航Hang*2 小时前
第2章:进阶Linux系统——第8节:配置与管理MariaDB服务器
linux·运维·服务器·数据库·笔记·学习·mariadb
wqww_12 小时前
Linux查看磁盘IO问题
linux·运维·服务器
2023自学中2 小时前
正点原子 Linux 驱动开发:多点电容触摸屏实验,gt9147 触摸芯片
linux·驱动开发·嵌入式
航Hang*2 小时前
第2章:进阶Linux系统——第10节:Linux 系统编程与 Shell 脚本全解笔记(GCC+Make+Vim+Shell Script)
linux·运维·服务器·学习·vim·apache·vmware
孙同学_2 小时前
【Linux篇】应用层协议HTTP
linux·运维·http
DeadPool loves Star2 小时前
新版VSCode登录Old Linux
linux·ide·vscode
我爱学习好爱好爱2 小时前
Ansible Loop循环 循环遍历的属性 Notify和Handlers
linux·运维·ansible