文章目录
- [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/ram 或 initrd,且 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实操")