0%

理解 initramfs

这里是 linux kernel Documentation 阅读系列第一篇,在这个系列中,我会记录每一次阅读到的 linux 内核文档。这一篇阅读的是 ramfs, rootfs and initramfs,主要是介绍 initramfs

What is ramfs?

ramfs是 linux 中利用linux的disk caching机制(page cache 和 dentry cache)实现的可动态伸缩的基于RAM做存储的文件系统。

文件一般放在磁盘上,当需要对其读写的时候会将其加载到内存中。为了提高文件的读写效率,根据局部性原理,linux 基本上都会文件缓存到内存中。

  • 当文件的数据被 OS 从后端存储读取到内存的时候,因为这段数据可能之后会被用到,并不会被马上释放,这块数据将会被标记为 clean,就是说可以被释放掉。只有当 VM 系统需要用到这块内存做其他事情的时候,这些数据才会被释放掉。
  • 当有数据写到文件后,这段数据也会被标记为 clean,但是仍然保存在内存中而不释放,以用作缓存的目的。只有当 VM 系统重新申请这块内存的时候,这段数据才会被释放掉。

对于 dentry cache,linux 也有类似的机制,从而大大加快了对于目录的访问。

上述说的是我们常见文件系统的机制,对于 ramfs而言,这里根本没有所谓的后端存储。当你要向 ramfs写文件时,我们像原来一样,在内存上分配 page cachedentry cache,但是这些 cache 不会被写到磁盘这些后端存储中 。所以,这些 cache根本不会被标记为 clean,而会一直存在于内存中,VM系统也无法回收他们的内存,重新分配做它用。

实现 ramfs需要的代码量非常的少,因为他们基本上就是依靠Linux现有的caching infastructure。对于用户而言,我们只是把一个 disk cache加载成文件系统。因此,ramfs就不是通过menuconfig可以被移去的可选模块,它底层的机制是 linux 所必须的。

下面做了一个简单的实验,这是当前系统能够看到的文件系统。

1
2
3
4
5
6
7
8
9
10
11
vagrant@cosmos:~$ df -a
文件系统 1K-块 已用 可用 已用% 挂载点
sysfs 0 0 0 - /sys
proc 0 0 0 - /proc
udev 1014960 0 1014960 0% /dev
devpts 0 0 0 - /dev/pts
tmpfs 204804 5784 199020 3% /run
/dev/sda1 10098468 1978740 8103344 20% /
tmpfs 1024004 0 1024004 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
home_vagrant_workspace 244810132 199558164 45251968 82% /home/vagrant/workspace

我们可以通过下列命令自己创建 ramfs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vagrant@cosmos:~$ mkdir ramfs
vagrant@cosmos:~$ sudo mount -t ramfs -o size=10M ramfs ./ramfs/
vagrant@cosmos:~$ df -a
文件系统 1K-块 已用 可用 已用% 挂载点
sysfs 0 0 0 - /sys
proc 0 0 0 - /proc
udev 1014960 0 1014960 0% /dev
devpts 0 0 0 - /dev/pts
tmpfs 204804 5784 199020 3% /run
/dev/sda1 10098468 1978740 8103344 20% /
tmpfs 1024004 0 1024004 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
home_vagrant_workspace 244810132 199558164 45251968 82% /home/vagrant/workspace
ramfs 0 0 0 - /home/vagrant/ramfs
vagrant@cosmos:~$ ls -alh ramfs/
总用量 21M
drwxr-xr-x 2 root root 0 3月 30 09:34 .
drwxr-xr-x 7 vagrant vagrant 4.0K 3月 30 09:32 ..
-rw-r--r-- 1 root root 20M 3月 30 09:25 test.file

ramfs and ramdisk

ramdisk是比ramfs出现更早的机制,是利用 RAM 模拟生成一个块设备,以此作为文件系统的后端存储。这个块设备是固定大小的,所以它上面 mount 的文件系统也是固定大小的。和实际的块设备一样,我们需要把page cache从这块假的块设备复制到内存,然后把改变复制回去,对于 dentry cache也是一样。除此之外,它还需要文件系统驱动(比如 ext2)去格式化和解释这段数据。

相较于ramfsramdisk会浪费更多的内存,占用更多的内存总线带宽,给 CPU 带来更多的工作,并且污染 CPU 的 cache。相较而言,ramfs的实现机制更加简单和高效。

loopback devices是导致 ramdisk 淘汰的另一个原因,它相对于ramdisk而言提供了一种更加灵活和方便的方式来创建块设备,现在是通过文件而不是通过内存。

ramfs and tmpfs

ramfs的一个问题是,你可以一直往 ramfs里面写数据,直到你用完了所有的内存。而且 VM 系统也不能释放这段内存,因为 VM 认为这些数据应该被写到后端存储,而对于 ramfs而言他没有后端存储。因此,只有 root用户能够往 ramfs写数据。

为了解决上述问题,linux 内核开发者又发明了 tmpfs,给添加了大小的限制和普通用户写数据的权限。

特性 tmpfs ramfs
达到空间上限时继续写入 提示错误信息并终止 可以继续写尚未分配的空间
是否固定大小
是否使用swap
具有易失性

What is rootfs?

rootfsramfs或者tmpfs的一种特殊实例,根文件系统包含系统启动时所必须的目录和关键性的文件,以及使其他文件系统得以挂载(mount)所必要的文件。例如:

  • init进程的应用程序必须运行在根文件系统上
  • 根文件系统提供了根目录“/”
  • linux挂载分区时所依赖的信息存放于根文件系统/etc/fstab这个文件中
  • shell命令程序必须运行在根文件系统上,譬如ls、cd等命令

一套linux体系,只有内核本身是不能工作的,必须要rootfs(上的etc目录下的配置文件、/bin /sbin等目录下的shell命令,还有/lib目录下的库文件等···)相配合才能工作。

Linux启动时,第一个必须挂载的是根文件系统;若系统不能从指定设备上挂载根文件系统,则系统会出错而退出启动。成功之后可以自动或手动挂载其他的文件系统。

下面是 linux 的内核代码。rootfs是基于内存的文件系统,所有操作都在内存中完成;也没有实际的存储设备,所以不需要设备驱动程序的参与。基于以上原因,linux在启动阶段使用rootfs文件系统,当磁盘驱动程序和磁盘文件系统成功加载后,linux系统会将系统根目录从rootfs切换到磁盘文件系统。

1
2
3
4
5
6
7
8
9
10
11
init/main.c->
start_kernel()->
vfs_caches_init(totalram_pages)–>
mnt_init()–>
     /* sysfs用来记录和展示linux驱动模型,sysfs先于rootfs挂载是为全面展示linux驱动模型做好准备 */
     sysfs_init();
     /* mnt_init()调用sysfs_init()注册并挂载sysfs文件系统,然后调用kobject_create_and_add()创建fs目录 */
fs_kobj = kobject_create_and_add("fs", NULL);
     /* init_rootfs()注册rootfs,然后调用init_mount_tree()挂载rootfs */
     init_rootfs();
     init_mount_tree();

下面是 init_rootfs的代码,可以看到,如果 CONFIG_TMPFS开启,rootfs 将会默认使用 tmpfs,否则使用 ramfs

init/do_mounts.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __init init_rootfs(void)
{
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();
is_tmpfs = true;
} else {
err = init_ramfs_fs();
}

if (err)
unregister_filesystem(&rootfs_fs_type);

return err;
}

What is initramfs?

initramfs是一种ramfs文件系统,在内核启动完成后把它复制到rootfs中,作为内核初始的根文件系统,它的任务是挂载系统真正的根文件系统。所有的 2.6 版本的 linux 内核都包含一个gzip 压缩过的 cpio 存档,在kernel 启动的时候,将会将其解压成 rootfs。解压之后,内核将会检查 rootfs 是否有一个 init文件,如果有的话,将会执行 init程序作为 PID 为 1 的进程。在这之后,init进程将会负责其启动整个系统,包括找到并加载真正的根设备。

相对于老的 initrd机制,initramfs有以下几点区别:

  • 老的 initrd总是一个独立的文件,而initramfs存档是被链接到内核镜像中去的
  • 老的 initrd文件是一个 gzip 压缩过的文件系统镜像,新的 initramfs是 gzip 压缩过的 cpio存档,相对而言更简单
  • 以往的基于ramdisk 的initrd 使用pivot_root命令切换到新的根文件系统,然后卸载ramdisk。但是initramfs是rootfs,而rootfs既不能pivot_root,也不能umount。为了从initramfs中切换到新根文件系统,需要作如下处理:
    • 删除rootfs的全部内容,释放空间
      find -xdev / -exec rm '{}' ';'
    • 安装新的根文件系统,并切换
      cd /newmount; mount --move . /; chroot .
    • 把stdin/stdout/stderr 附加到新的/dev/console,然后执行新文件系统的init程序

上述步骤比较麻烦,而且要解决一个重要的问题:第一步删除rootfs的所有内容也删除了所有的命令,那么后续如何再使用这些命令完成其他步骤?busybox的解决方案是,提供了switch_root命令,完成全部的处理过程,使用起来非常方便。

switch_root命令的格式是:

1
$ switch_root [options] <newrootdir> <init> <args to init>
  • newrootdir是实际的根文件系统的挂载目录,执行switch_root命令前需要挂载到系统中
  • init是实际根文件系统的init程序的路径,一般是/sbin/init;
  • args to init`则是传递给实际的根文件系统的init程序的参数,也是可选的。

需要特别注意的是:switch_root命令必须由PID=1的进程调用,也就是必须由initramfs的init程序直接调用,不能由init派生的其他进程调用,否则会出错,提示:switch_root: not rootfs。也是同样的原因,init脚本调用switch_root命令必须用exec命令调用,否则也会出错,提示:switch_root: not rootfs

Contents of initramfs

An initramfs archive is a complete self-contained root filesystem for Linux.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat > hello.c << EOF
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
printf("Hello world!\n");
sleep(999999999);
}
EOF
$ gcc -static hello.c -o init
$ echo init | cpio -o -H newc | gzip > test.cpio.gz
# Testing external initramfs using the initrd loading mechanism.
$ qemu -kernel /boot/vmlinuz -initrd test.cpio.gz /dev/zero

参考资料