Linux 初始 RAM 磁盘(initrd)
Linux® 初始 RAM 磁盘(initrd)是在系统引导过程中挂载的一个临时根文件系统,用来支持两阶段的引导过程。initrd 文件中包含了各种可执行程序和驱动程序,它们可以用来挂载实际的根文件系统,然后再将这个 initrd RAM 磁盘卸载,并释放内存。在很多嵌入式 Linux 系统中,initrd 就是最终的根文件系统。本文将探索 Linux 2.6 的初始 RAM 磁盘,包括如何创建以及如何在 Linux 内核中使用。
初始 RAM 磁盘(initrd)是在实际根文件系统可用之前挂载到系统中的一个初始根文件系统。initrd 与内核绑定在一起,并作为内核引导过程的一部分进行加载。内核然后会将这个 initrd 文件作为其两阶段引导过程的一部分来加载模块,这样才能稍后使用真正的文件系统,并挂载实际的根文件系统。
initrd 中包含了实现这个目标所需要的目录和可执行程序的最小集合,例如将内核模块加载到内核中所使用的 insmod 工具。
在桌面或服务器 Linux 系统中,initrd 是一个临时的文件系统。其生存周期很短,只会用作到真实文件系统的一个桥梁。在没有存储设备的嵌入式系统中,initrd 是永久的根文件系统。本文将对这两种情况进行探索。
![]()
initrd 映像中包含了支持 Linux 系统两阶段引导过程所需要的必要可执行程序和系统文件。
根据我们运行的 Linux 的版本不同,创建初始 RAM 磁盘的方法也可能会有所不同。在 Fedora Core 3 之前,initrd 是使用 loop 设备 来构建的。loop 设备 是一个设备驱动程序,利用它可以将文件作为一个块设备挂载到系统中,然后就可以查看这个文件系统中的内容了。在您的内核中可能并没有 loop 设备,不过这可以通过内核配置工具(make menuconfig)选择 Device Drivers > Block Devices > Loopback Device Support 来启用。我们可以按照下面的方法来查看 loop 设备的内容(initrd 文件的名字可能会稍有不同):
清单 1. 查看 initrd 的内容(适用于 FC3 之前的版本)
# mkdir temp ; cd temp # cp /boot/initrd.img.gz . # gunzip initrd.img.gz # mount -t ext -o loop initrd.img /mnt/initrd # ls -la /mnt/initrd #
现在我们就可以查看 /mnt/initrd 子目录中的内容了,这就代表了 initrd 文件的内容。注意,即使您的 initrd 映像文件不是以 .gz 结尾,它也可能是一个压缩文件,您可以给这个文件添加上 .gz 后缀,然后再使用 gunzip 对其进行解压。
从 Fedora Core 3 开始,默认的 initrd 映像变成了一个经过压缩的 cpio 归档文件。我们不用再使用 loop 设备来将 initrd 作为压缩映像进行挂载,而是可以将其作为 cpio 归档文件来使用。要查看 cpio 归档文件的内容,可以使用下面的命令:
清单 2. 查看 initrd 的内容(适用于 FC3 及其以后的版本)
# mkdir temp ; cd temp # cp /boot/initrd-2.6.14.2.img initrd-2.6.14.2.img.gz # gunzip initrd-2.6.14.2.img.gz # cpio -i --make-directories < initrd-2.6.14.2.img #
结果会生成一个很小的根文件系统,如清单 3 所示。在 ./bin 目录中有一组很少但却非常必要的应用程序,包括 nash(即 not a shell,是一个脚本解释器)、insmod(用来加载内核模块)和 lvm(逻辑卷管理工具)。
# ls -la # drwxr-xr-x 10 root root 4096 May 7 02:48 . drwxr-x--- 15 root root 4096 May 7 00:54 .. drwxr-xr-x 2 root root 4096 May 7 02:48 bin drwxr-xr-x 2 root root 4096 May 7 02:48 dev drwxr-xr-x 4 root root 4096 May 7 02:48 etc -rwxr-xr-x 1 root root 812 May 7 02:48 init -rw-r--r-- 1 root root 1723392 May 7 02:45 initrd-2.6.14.2.img drwxr-xr-x 2 root root 4096 May 7 02:48 lib drwxr-xr-x 2 root root 4096 May 7 02:48 loopfs drwxr-xr-x 2 root root 4096 May 7 02:48 proc lrwxrwxrwx 1 root root 3 May 7 02:48 sbin -> bin drwxr-xr-x 2 root root 4096 May 7 02:48 sys drwxr-xr-x 2 root root 4096 May 7 02:48 sysroot #
清单 3 中比较有趣的是 init 文件就在根目录中。与传统的 Linux 引导过程类似,这个文件也是在将 initrd 映像解压到 RAM 磁盘中时被调用的。在本文稍后我们将来探索这个问题。
![]()
![]()
![]()
使用 cpio 命令,我们可以对 cpio 文件进行操作。cpio 是一种文件格式,它简单地使用文件头将一组文件串接在一起。cpio 文件格式可以使用 ASCII 和二进制文件。为了保证可移植性,我们可以使用 ASCII 格式。为了减小文件大小,我们可以使用二进制的版本。
下面让我们回到最开始,来看一下 initrd 映像最初是如何构建的。对于传统的 Linux 系统来说,initrd 映像是在 Linux 构建过程中创建的。有很多工具,例如 mkinitrd,都可以用来使用必要的库和模块自动构建 initrd,从而用作与真实的根文件系统之间的桥梁。mkinitrd 工具实际上就是一个 shell 脚本,因此我们可以看到它究竟是如何来实现这个结果的。还有一个 YAIRD(即 Yet Another Mkinitrd)工具,可以对 initrd 构建过程的各个方面进行定制。
![]()
由于在很多基于 Linux 的嵌入式系统上没有硬盘,因此 initrd 也会作为这种系统上的永久根文件系统使用。清单 4 显示了如何创建一个 initrd 映像文件。我使用了一个标准的 Linux 桌面,这样您即使没有嵌入式平台,也可以按照下面的步骤来执行了。除了交叉编译,其他概念(也适用于 initrd 的构建)对于嵌入式平台都是相同的。
#!/bin/bash # Housekeeping... rm -f /tmp/ramdisk.img rm -f /tmp/ramdisk.img.gz # Ramdisk Constants RDSIZE=4000 BLKSIZE=1024 # Create an empty ramdisk image dd if=/dev/zero of=/tmp/ramdisk.img bs=$BLKSIZE count=$RDSIZE # Make it an ext2 mountable file system /sbin/mke2fs -F -m 0 -b $BLKSIZE /tmp/ramdisk.img $RDSIZE # Mount it so that we can populate mount /tmp/ramdisk.img /mnt/initrd -t ext2 -o loop=/dev/loop0 # Populate the filesystem (subdirectories) mkdir /mnt/initrd/bin mkdir /mnt/initrd/sys mkdir /mnt/initrd/dev mkdir /mnt/initrd/proc # Grab busybox and create the symbolic links pushd /mnt/initrd/bin cp /usr/local/src/busybox-1.1.1/busybox . ln -s busybox ash ln -s busybox mount ln -s busybox echo ln -s busybox ls ln -s busybox cat ln -s busybox ps ln -s busybox dmesg ln -s busybox sysctl popd # Grab the necessary dev files cp -a /dev/console /mnt/initrd/dev cp -a /dev/ramdisk /mnt/initrd/dev cp -a /dev/ram0 /mnt/initrd/dev cp -a /dev/null /mnt/initrd/dev cp -a /dev/tty1 /mnt/initrd/dev cp -a /dev/tty2 /mnt/initrd/dev # Equate sbin with bin pushd /mnt/initrd ln -s bin sbin popd # Create the init file cat >> /mnt/initrd/linuxrc << EOF #!/bin/ash echo echo "Simple initrd is active" echo mount -t proc /proc /proc mount -t sysfs none /sys /bin/ash --login EOF chmod +x /mnt/initrd/linuxrc # Finish up... umount /mnt/initrd gzip -9 /tmp/ramdisk.img cp /tmp/ramdisk.img.gz /boot/ramdisk.img.gz
![]()
Minimax 是一个开放源码项目,其设计目标是成为一个全部封装在 initrd 中的 Linux 发行版。它的大小是 32MB,为了尽量小,它使用了 BusyBox 和 uClibc。除了非常小之外,它还使用了 2.6 版本的 Linux 内核,并提供了很多有用的工具。
为了创建 initrd,我们最开始创建了一个空文件,这使用了 /dev/zero(一个由零组成的码流)作为输入,并将其写入到 ramdisk.img 文件中。所生成的文件大小是 4MB(4000 个 1K 大小的块)。然后使用 mke2fs 命令在这个空文件上创建了一个 ext2(即 second extended)文件系统。现在这个文件变成了一个 ext2 格式的文件系统,我们使用 loop 设备将这个文件挂载到 /mnt/initrd 上了。在这个挂载点上,我们现在就有了一个目录,它以 ext2 文件系统的形式呈现出来,我们可以对自己的 initrd 文件进行拼装了。接下来的脚本提供了这种功能。
下一个步骤是创建构成根文件系统所需要的子目录:/bin、/sys、/dev 和 /proc。这里只列出了所需要的目录(例如没有库),但是其中包含了很多功能。
![]()
尽管 ext2 是一种通用的 Linux 文件系统格式,但是还有一些替代品可以减小 initrd 映像文件以及所挂载上来的文件系统的大小。这种文件系统的例子有 romfs(ROM 文件系统)、cramfs(压缩 ROM 文件系统)和 squashfs(高度压缩只读文件系统)。如果我们需要暂时将数据写入文件系统中,ext2 可以很好地实现这种功能。最后,e2compr 是 ext2 文件系统驱动程序的一个扩展,可以支持在线压缩。
为了可以使用根文件系统,我们使用了 BusyBox。这个工具是一个单一映像,其中包含了很多在 Linux 系统上通常可以找到的工具(例如 ash、awk、sed、insmod 等)。BusyBox 的优点是它将很多工具打包成一个文件,同时还可以共享它们的通用元素,这样可以极大地减少映像文件的大小。这对于嵌入式系统来说非常理想。将 BusyBox 映像从自己的源目录中拷贝到自己根目录下的 /bin 目录中。然后创建了很多符号链接,它们都指向 BusyBox 工具。BusyBox 会判断所调用的是哪个工具,并执行这个工具的功能。我们在这个目录中创建了几个链接来支持 init 脚本(每个命令都是一个指向 BusyBox 的链接。)
下一个步骤是创建几个特殊的设备文件。我从自己当前的 /dev 子目录中直接拷贝了这些文件,这使用了 -a 选项(归档)来保留它们的属性。
倒数第二个步骤是生成 linuxrc 文件。在内核挂载 RAM 磁盘之后,它会查找 init 文件来执行。如果没有找到 init 文件,内核就会调用 linuxrc 文件作为自己的启动脚本。我们在这个文件中实现对环境的基本设置,例如挂载 /proc 文件系统。除了 /proc 之外,我还挂载了 /sys 文件系统,并向终端打印一条消息。最后,我们调用了 ash(一个 Bourne Shell 的克隆),这样就可以与根文件系统进行交互了。linuxrc 文件然后使用 chmod 命令修改成可执行的。
最后,我们的根文件系统就完成了。我们将其卸载掉,然后使用 gzip 对其进行压缩。所生成的文件(ramdisk.img.gz)被拷贝到 /boot 子目录中,这样就可以通过 GNU GRUB 对其进行加载了。
要构建初始 RAM 磁盘,我们可以简单地调用 mkird,这样就会自动创建这个映像文件,并将其拷贝到 /boot 目录中。
![]()
对于 Linux 内核来说,要支持初始 RAM 磁盘,内核必须要使用 CONFIG_BLK_DEV_RAM 和 CONFIG_BLK_DEV_INITRD 选项进行编译。
新的 initrd 映像现在已经在 /boot 目录中了,因此下一个步骤是使用默认的内核来对其进行测试。现在我们可以重新启动 Linux 系统了。在出现 GRUB 界面时,按 C 键启动 GRUB 中的命令行工具。我们现在可以与 GRUB 进行交互,从而定义要加载哪个内核和 initrd 映像文件。kernel 命令让我们可以指定内核文件,initrd 命令可以用来指定 initrd 映像文件。在定义好这些参数之后,就可以使用 boot 命令来引导内核了,如清单 5 所示。
GNU GRUB version 0.95 (638K lower / 97216K upper memory) [ Minimal BASH-like line editing is supported. For the first word, TAB lists possible command completions. Anywhere else TAB lists the possible completions of a device/filename. ESC at any time exits.] grub> kernel /bzImage-2.6.1 [Linux-bzImage, setup=0x1400, size=0x29672e] grub> initrd /ramdisk.img.gz [Linux-initrd @ 0x5f2a000, 0xb5108 bytes] grub> boot Uncompressing Linux... OK, booting the kernel.
在内核启动之后,它会检查是否有 initrd 映像文件可用(稍后会更详细介绍),然后将其加载,并将其挂载成根文件系统。在清单 6 中我们可以看到这个 Linux 启动过程最后的样子。在启动之后,ash shell 就可以用来输入命令了。在这个例子中,我们将浏览一下根文件系统的内容,并查看一下虚拟 proc 文件系统中的内容。我们还展示了如何通过 touch 命令在文件系统中创建文件。注意所创建的第一个进程是 linuxrc(通常都是 init)。
清单 6. 使用简单的 initrd 引导 Linux 内核
... md: Autodetecting RAID arrays md: autorun md: ... autorun DONE. RAMDISK: Compressed image found at block 0 VFS: Mounted root (ext2 file system). Freeing unused kernel memory: 208k freed / $ ls bin etc linuxrc proc sys dev lib lost+found sbin / $ cat /proc/1/cmdline /bin/ash/linuxrc / $ cd bin /bin $ ls ash cat echo mount sysctl busybox dmesg ls ps /bin $ touch zfile /bin $ ls ash cat echo mount sysctl busybox dmesg ls ps zfile
![]()
现在我们已经了解了如何构建并使用定制的初始 RAM 磁盘,本节将探索内核是如何识别 initrd 并将其作为根文件系统进行挂载的。我们将介绍启动链中的几个主要函数,并解释一下到底在进行什么操作。
引导加载程序,例如 GRUB,定义了要加载的内核,并将这个内核映像以及相关的 initrd 拷贝到内存中。我们可以在 Linux 内核源代码目录中的 ./init 子目录中找到很多这种功能。
在内核和 initrd 映像被解压并拷贝到内存中之后,内核就会被调用了。它会执行不同的初始化操作,最终您会发现自己到了 init/main.c:init()(subdir/file:function)函数中。这个函数执行了大量的子系统初始化操作。此处会执行一个对 init/do_mounts.c:prepare_namespace() 的调用,这个函数用来准备名称空间(挂载 dev 文件系统、RAID 或 md、设备以及最后的 initrd)。加载 initrd 是通过调用 init/do_mounts_initrd.c:initrd_load() 实现的。
initrd_load() 函数调用了 init/do_mounts_rd.c:rd_load_image(),它通过调用 init/do_mounts_rd.c:identify_ramdisk_image() 来确定要加载哪个 RAM 磁盘。这个函数会检查映像文件的 magic 号来确定它是 minux、etc2、romfs、cramfs 或 gzip 格式。在返回到 initrd_load_image 之前,它还会调用 init/do_mounts_rd:crd_load()。这个函数负责为 RAM 磁盘分配空间,并计算循环冗余校验码(CRC),然后对 RAM 磁盘映像进行解压,并将其加载到内存中。现在,我们在一个适合挂载的块设备中就有了这个 initrd 映像。
现在使用一个 init/do_mounts.c:mount_root() 调用将这个块设备挂载到根文件系统上。它会创建根设备,并调用 init/do_mounts.c:mount_block_root()。在这里调用 init/do_mounts.c:do_mount_root(),后者又会调用 fs/namespace.c:sys_mount() 来真正挂载根文件系统,然后 chdir 到这个文件系统中。这就是我们在清单 6 中所看到的熟悉消息 VFS: Mounted root (ext2 file system). 的地方。
最后,返回到 init 函数中,并调用 init/main.c:run_init_process。这会导致调用 execve 来启动 init 进程(在本例中是 /linuxrc)。linuxrc 可以是一个可执行程序,也可以是一个脚本(条件是它有脚本解释器可用)。
这些函数的调用层次结构如清单 7 所示。尽管此处并没有列出拷贝和挂载初始 RAM 磁盘所涉及的所有函数,但是这足以为我们提供一个整体流程的粗略框架。
清单 7. initrd 加载和挂载过程中所使用的主要函数的层次结构
init/main.c:init
init/do_mounts.c:prepare_namespace
init/do_mounts_initrd.c:initrd_load
init/do_mounts_rd.c:rd_load_image
init/do_mounts_rd.c:identify_ramdisk_image
init/do_mounts_rd.c:crd_load
lib/inflate.c:gunzip
init/do_mounts.c:mount_root
init/do_mounts.c:mount_block_root
init/do_mounts.c:do_mount_root
fs/namespace.c:sys_mount
init/main.c:run_init_process
execve
![]()
与嵌入式引导的情况类似,本地磁盘(软盘或 CD-ROM)对于引导内核和 ramdisk 根文件系统来说都不是必需的。DHCP(Dynamic Host Configuration Protocol)可以用来确定网络参数,例如 IP 地址和子网掩码。TFTP(Trivial File Transfer Protocol)可以用来将内核映像和初始 ramdisk 映像传输到本地设备上。传输完成之后,就可以引导 Linux 内核并挂载 initrd 了,这与本地映像引导的过程类似。
![]()
在构建嵌入式系统时,我们可能希望将 initrd 映像文件做得尽可能小,这其中有一些技巧需要考虑。首先是使用 BusyBox(本文中已经展示过了)。BusyBox 可以将数 MB 的工具压缩成几百 KB。
在这个例子中,BusyBox 映像是静态链接的,因此它不需要其他库。然而,如果我们需要标准的 C 库(我们自己定制的二进制可能需要这个库),除了巨大的 glibc 之外,我们还有其他选择。第一个较小的库是 uClibc,这是为对空间要求非常严格的系统准备的一个标准 C 库。另外一个适合空间紧张的环境的库是 dietlib。要记住我们需要使用这些库来重新编译想在嵌入式系统中重新编译的二进制文件,因此这需要额外再做一些工作(但是这是非常值得的)。
![]()
初始 RAM 磁盘最初是设计用来通过一个临时根文件系统来作为内核到最终的根文件系统之间的桥梁。initrd 对于在嵌入式系统中加载到 RAM 磁盘里的非持久性根文件系统来说也非常有用。
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- “Linux 引导过程内幕”(developerWorks,2006 年 5 月)探索了 Linux 从最初的 bootstrap 到启动第一个用户空间应用程序的过程。
- 在 “从 FireWire 设备引导 Linux”(developerWorks,2004 年 7 月)中,我们可以学习在各种平台的各种设备上如何(使用 initrd)启动 Linux。
- cpio 文件格式 既简单又简洁。因此 Fedora 团队选择使用它作为 initrd 的格式就没什么奇怪的了。
- mkinitrd 工具非常适合创建 initrd 映像文件。除了创建 initrd 映像之外,它还可以确定要为您的系统加载哪些模块,并将这些模块也全部加入到这个映像中。
- loop 设备 是一个非常有用的驱动程序,可以将映像文件作为文件系统挂载。
- Network Boot and Exotic Root HOWTO 不但介绍了从网络上引导 Linux 的过程,还介绍了诸如软盘引导、CD-ROM 引导和嵌入式环境中的内容。
- 在 developerWorks Linux 专区 中可以找到为 Linux 开发人员准备的更多资源。
- 随时关注 developerWorks 技术事件和网络广播。
https://www.ibm.com/developerworks/cn/linux/l-k26initrd/
Linux 的 initrd 技术是一个非常普遍使用的机制,linux2.6 内核的 initrd 的文件格式由原来的文件系统镜像文件转变成了 cpio 格式,变化不仅反映在文件格式上, linux 内核对这两种格式的 initrd 的处理有着截然的不同。本文首先介绍了什么是 initrd 技术,然后分别介绍了 Linux2.4 内核和 2.6 内核的 initrd 的处理流程。最后通过对 Linux2.6 内核的 initrd 处理部分代码的分析,使读者可以对 initrd 技术有一个全面的认识。为了更好的阅读本文,要求读者对 Linux 的 VFS 以及 initrd 有一个初步的了解。
initrd 的英文含义是 boot loader initialized RAM disk,就是由 boot loader 初始化的内存盘。在 linux内核启动前, boot loader 会将存储介质中的 initrd 文件加载到内存,内核启动时会在访问真正的根文件系统前先访问该内存中的 initrd 文件系统。在 boot loader 配置了 initrd 的情况下,内核启动被分成了两个阶段,第一阶段先执行 initrd 文件系统中的"某个文件",完成加载驱动模块等任务,第二阶段才会执行真正的根文件系统中的 /sbin/init 进程。这里提到的"某个文件",Linux2.6 内核会同以前版本内核的不同,所以这里暂时使用了"某个文件"这个称呼,后面会详细讲到。第一阶段启动的目的是为第二阶段的启动扫清一切障爱,最主要的是加载根文件系统存储介质的驱动模块。我们知道根文件系统可以存储在包括IDE、SCSI、USB在内的多种介质上,如果将这些设备的驱动都编译进内核,可以想象内核会多么庞大、臃肿。
Initrd 的用途主要有以下四种:
1. linux 发行版的必备部件
linux 发行版必须适应各种不同的硬件架构,将所有的驱动编译进内核是不现实的,initrd 技术是解决该问题的关键技术。Linux 发行版在内核中只编译了基本的硬件驱动,在安装过程中通过检测系统硬件,生成包含安装系统硬件驱动的 initrd,无非是一种即可行又灵活的解决方案。
2. livecd 的必备部件
同 linux 发行版相比,livecd 可能会面对更加复杂的硬件环境,所以也必须使用 initrd。
3. 制作 Linux usb 启动盘必须使用 initrd
usb 设备是启动比较慢的设备,从驱动加载到设备真正可用大概需要几秒钟时间。如果将 usb 驱动编译进内核,内核通常不能成功访问 usb 设备中的文件系统。因为在内核访问 usb 设备时, usb 设备通常没有初始化完毕。所以常规的做法是,在 initrd 中加载 usb 驱动,然后休眠几秒中,等待 usb设备初始化完毕后再挂载 usb 设备中的文件系统。
4. 在 linuxrc 脚本中可以很方便地启用个性化 bootsplash。
![]()
![]()
为了使读者清晰的了解Linux2.6内核initrd机制的变化,在重点介绍Linux2.6内核initrd之前,先对linux2.4内核的initrd进行一个简单的介绍。Linux2.4内核的initrd的格式是文件系统镜像文件,本文将其称为image-initrd,以区别后面介绍的linux2.6内核的cpio格式的initrd。 linux2.4内核对initrd的处理流程如下:
1. boot loader把内核以及/dev/initrd的内容加载到内存,/dev/initrd是由boot loader初始化的设备,存储着initrd。
2. 在内核初始化过程中,内核把 /dev/initrd 设备的内容解压缩并拷贝到 /dev/ram0 设备上。
3. 内核以可读写的方式把 /dev/ram0 设备挂载为原始的根文件系统。
4. 如果 /dev/ram0 被指定为真正的根文件系统,那么内核跳至最后一步正常启动。
5. 执行 initrd 上的 /linuxrc 文件,linuxrc 通常是一个脚本文件,负责加载内核访问根文件系统必须的驱动, 以及加载根文件系统。
6. /linuxrc 执行完毕,真正的根文件系统被挂载。
7. 如果真正的根文件系统存在 /initrd 目录,那么 /dev/ram0 将从 / 移动到 /initrd。否则如果 /initrd 目录不存在, /dev/ram0 将被卸载。
8. 在真正的根文件系统上进行正常启动过程 ,执行 /sbin/init。 linux2.4 内核的 initrd 的执行是作为内核启动的一个中间阶段,也就是说 initrd 的 /linuxrc 执行以后,内核会继续执行初始化代码,我们后面会看到这是 linux2.4 内核同 2.6 内核的 initrd 处理流程的一个显著区别。
![]()
![]()
linux2.6 内核支持两种格式的 initrd,一种是前面第 3 部分介绍的 linux2.4 内核那种传统格式的文件系统镜像-image-initrd,它的制作方法同 Linux2.4 内核的 initrd 一样,其核心文件就是 /linuxrc。另外一种格式的 initrd 是 cpio 格式的,这种格式的 initrd 从 linux2.5 起开始引入,使用 cpio 工具生成,其核心文件不再是 /linuxrc,而是 /init,本文将这种 initrd 称为 cpio-initrd。尽管 linux2.6 内核对 cpio-initrd和 image-initrd 这两种格式的 initrd 均支持,但对其处理流程有着显著的区别,下面分别介绍 linux2.6 内核对这两种 initrd 的处理流程。
1. boot loader 把内核以及 initrd 文件加载到内存的特定位置。
2. 内核判断initrd的文件格式,如果是cpio格式。
3. 将initrd的内容释放到rootfs中。
4. 执行initrd中的/init文件,执行到这一点,内核的工作全部结束,完全交给/init文件处理。
1. boot loader把内核以及initrd文件加载到内存的特定位置。
2. 内核判断initrd的文件格式,如果不是cpio格式,将其作为image-initrd处理。
3. 内核将initrd的内容保存在rootfs下的/initrd.image文件中。
4. 内核将/initrd.image的内容读入/dev/ram0设备中,也就是读入了一个内存盘中。
5. 接着内核以可读写的方式把/dev/ram0设备挂载为原始的根文件系统。
6. .如果/dev/ram0被指定为真正的根文件系统,那么内核跳至最后一步正常启动。
7. 执行initrd上的/linuxrc文件,linuxrc通常是一个脚本文件,负责加载内核访问根文件系统必须的驱动, 以及加载根文件系统。
8. /linuxrc执行完毕,常规根文件系统被挂载
9. 如果常规根文件系统存在/initrd目录,那么/dev/ram0将从/移动到/initrd。否则如果/initrd目录不存在, /dev/ram0将被卸载。
10. 在常规根文件系统上进行正常启动过程 ,执行/sbin/init。
通过上面的流程介绍可知,Linux2.6内核对image-initrd的处理流程同linux2.4内核相比并没有显著的变化, cpio-initrd的处理流程相比于image-initrd的处理流程却有很大的区别,流程非常简单,在后面的源代码分析中,读者更能体会到处理的简捷。
4.cpio-initrd同image-initrd的区别与优势
没有找到正式的关于cpio-initrd同image-initrd对比的文献,根据笔者的使用体验以及内核代码的分析,总结出如下三方面的区别,这些区别也正是cpio-initrd的优势所在:
cpio-initrd的制作非常简单,通过两个命令就可以完成整个制作过程
#假设当前目录位于准备好的initrd文件系统的根目录下 bash# find . | cpio -c -o > ../initrd.img bash# gzip ../initrd.img
而传统initrd的制作过程比较繁琐,需要如下六个步骤
#假设当前目录位于准备好的initrd文件系统的根目录下 bash# dd if=/dev/zero of=../initrd.img bs=512k count=5 bash# mkfs.ext2 -F -m0 ../initrd.img bash# mount -t ext2 -o loop ../initrd.img /mnt bash# cp -r * /mnt bash# umount /mnt bash# gzip -9 ../initrd.img
本文不对上面命令的含义作细节的解释,因为本文主要介绍的是linux内核对initrd的处理,对上面命令不理解的读者可以参考相关文档。
通过上面initrd处理流程的介绍,cpio-initrd的处理流程显得格外简单,通过对比可知cpio-initrd的处理流程在如下两个方面得到了简化:
1. cpio-initrd并没有使用额外的ramdisk,而是将其内容输入到rootfs中,其实rootfs本身也是一个基于内存的文件系统。这样就省掉了ramdisk的挂载、卸载等步骤。
2. cpio-initrd启动完/init进程,内核的任务就结束了,剩下的工作完全交给/init处理;而对于image-initrd,内核在执行完/linuxrc进程后,还要进行一些收尾工作,并且要负责执行真正的根文件系统的/sbin/init。通过图1可以更加清晰的看出处理流程的区别:
图1内核对cpio-initrd和image-initrd处理流程示意图
如图1所示,cpio-initrd不再象image-initrd那样作为linux内核启动的一个中间步骤,而是作为内核启动的终点,内核将控制权交给cpio-initrd的/init文件后,内核的任务就结束了,所以在/init文件中,我们可以做更多的工作,而不比担心同内核后续处理的衔接问题。当然目前linux发行版的cpio-initrd的/init文件的内容还没有本质的改变,但是相信initrd职责的增加一定是一个趋势。
![]()
![]()
上面简要介绍了Linux2.4内核和2.6内核的initrd的处理流程,为了使读者对于Linux2.6内核的initrd的处理有一个更加深入的认识,下面将对Linuxe2.6内核初始化部分同initrd密切相关的代码给予一个比较细致的分析,为了讲述方便,进一步明确几个代码分析中使用的概念:
rootfs: 一个基于内存的文件系统,是linux在初始化时加载的第一个文件系统,关于它的进一步介绍可以参考文献[4]。
initramfs: initramfs同本文的主题关系不是很大,但是代码中涉及到了initramfs,为了更好的理解代码,这里对其进行简单的介绍。Initramfs是在 kernel 2.5中引入的技术,实际上它的含义就是:在内核镜像中附加一个cpio包,这个cpio包中包含了一个小型的文件系统,当内核启动时,内核将这个cpio包解开,并且将其中包含的文件系统释放到rootfs中,内核中的一部分初始化代码会放到这个文件系统中,作为用户层进程来执行。这样带来的明显的好处是精简了内核的初始化代码,而且使得内核的初始化过程更容易定制。Linux 2.6.12内核的 initramfs还没有什么实质性的东西,一个包含完整功能的initramfs的实现可能还需要一个缓慢的过程。对于initramfs的进一步了解可以参考文献[1][2][3]。
cpio-initrd: 前面已经定义过,指linux2.6内核使用的cpio格式的initrd。
image-initrd: 前面已经定义过,专指传统的文件镜像格式的initrd。
realfs: 用户最终使用的真正的文件系统。
内核的初始化代码位于 init/main.c 中的 static int init(void * unused)函数中。同initrd的处理相关部分函数调用层次如下图,笔者按照这个层次对每一个函数都给予了比较详细的分析,为了更好的说明,下面列出的代码中删除了同本文主题不相关的部分:
init函数是内核所有初始化代码的入口,代码如下,其中只保留了同initrd相关部分的代码。
static int init(void * unused){
[1] populate_rootfs();
[2] if (sys_access((const char __user *) "/init", 0) == 0)
execute_command = "/init";
else
prepare_namespace();
[3] if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
[4] if (execute_command)
run_init_process(execute_command);
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
}
代码[1]:populate_rootfs函数负责加载initramfs和cpio-initrd,对于populate_rootfs函数的细节后面会讲到。
代码[2]:如果rootfs的根目录下中包含/init进程,则赋予execute_command,在init函数的末尾会被执行。否则执行prepare_namespace函数,initrd是在该函数中被加载的。
代码[3]:将控制台设置为标准输入,后续的两个sys_dup(0),则复制标准输入为标准输出和标准错误输出。
代码[4]:如果rootfs中存在init进程,就将后续的处理工作交给该init进程。其实这段代码的含义是如果加载了cpio-initrd则交给cpio-initrd中的/init处理,否则会执行realfs中的init。读者可能会问:如果加载了cpio-initrd, 那么realfs中的init进程不是没有机会运行了吗?确实,如果加载了cpio-initrd,那么内核就不负责执行realfs的init进程了,而是将这个执行任务交给了cpio-initrd的init进程。解开fedora core4的initrd文件,会发现根目录的下的init文件是一个脚本,在该脚本的最后一行有这样一段代码:
……….. switchroot --movedev /sysroot
就是switchroot语句负责加载realfs,以及执行realfs的init进程。
对cpio-initrd的处理位于populate_rootfs函数中。
void __init populate_rootfs(void){
[1] char *err = unpack_to_rootfs(__initramfs_start,
__initramfs_end - __initramfs_start, 0);
[2] if (initrd_start) {
[3] err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start, 1);
[4] if (!err) {
printk(" it is\n");
unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start, 0);
free_initrd_mem(initrd_start, initrd_end);
return;
}
[5] fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 700);
if (fd >= 0) {
sys_write(fd, (char *)initrd_start,
initrd_end - initrd_start);
sys_close(fd);
free_initrd_mem(initrd_start, initrd_end);
}
}
代码[1]:加载initramfs, initramfs位于地址__initramfs_start处,是内核在编译过程中生成的,initramfs的是作为内核的一部分而存在的,不是 boot loader加载的。前面提到了现在initramfs没有任何实质内容。
代码[2]:判断是否加载了initrd。无论哪种格式的initrd,都会被boot loader加载到地址initrd_start处。
代码[3]:判断加载的是不是cpio-initrd。实际上 unpack_to_rootfs有两个功能一个是释放cpio包,另一个就是判断是不是cpio包, 这是通过最后一个参数来区分的, 0:释放 1:查看。
代码[4]:如果是cpio-initrd则将其内容释放出来到rootfs中。
代码[5]:如果不是cpio-initrd,则认为是一个image-initrd,将其内容保存到/initrd.image中。在后面的image-initrd的处理代码中会读取/initrd.image。
对image-initrd的处理在prepare_namespace函数里,包含了对image-initrd进行处理的代码,相关代码如下:
void __init prepare_namespace(void){
[1] if (initrd_load())
goto out;
out:
umount_devfs("/dev");
[2] sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
security_sb_post_mountroot();
mount_devfs_fs ();
}
代码[1]:执行initrd_load函数,将initrd载入,如果载入成功的话initrd_load函数会将realfs的根设置为当前目录。
代码[2]:将当前目录即realfs的根mount为Linux VFS的根。initrd_load函数执行完后,将真正的文件系统的根设置为当前目录。
initrd_load函数负责载入image-initrd,代码如下:
int __init initrd_load(void)
{
[1] if (mount_initrd) {
create_dev("/dev/ram", Root_RAM0, NULL);
[2] if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
sys_unlink("/initrd.image");
handle_initrd();
return 1;
}
}
sys_unlink("/initrd.image");
return 0;
}
代码[1]:如果加载initrd则建立一个ram0设备 /dev/ram。
代码[2]:/initrd.image文件保存的就是image-initrd,rd_load_image函数执行具体的加载操作,将image-nitrd的文件内容释放到ram0里。判断ROOT_DEV!=Root_RAM0的含义是,如果你在grub或者lilo里配置了 root=/dev/ram0 ,则实际上真正的根设备就是initrd了,所以就不把它作为initrd处理 ,而是作为realfs处理。
handle_initrd()函数负责对initrd进行具体的处理,代码如下:
static void __init handle_initrd(void){
[1] real_root_dev = new_encode_dev(ROOT_DEV);
[2] create_dev("/dev/root.old", Root_RAM0, NULL);
mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY);
[3] sys_mkdir("/old", 0700);
root_fd = sys_open("/", 0, 0);
old_fd = sys_open("/old", 0, 0);
/* move initrd over / and chdir/chroot in initrd root */
[4] sys_chdir("/root");
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
mount_devfs_fs ();
[5] pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD);
if (pid > 0) {
while (pid != sys_wait4(-1, &i, 0, NULL))
yield();
}
/* move initrd to rootfs' /old */
sys_fchdir(old_fd);
sys_mount("/", ".", NULL, MS_MOVE, NULL);
/* switch root and cwd back to / of rootfs */
[6] sys_fchdir(root_fd);
sys_chroot(".");
sys_close(old_fd);
sys_close(root_fd);
umount_devfs("/old/dev");
[7] if (new_decode_dev(real_root_dev) == Root_RAM0) {
sys_chdir("/old");
return;
}
[8] ROOT_DEV = new_decode_dev(real_root_dev);
mount_root();
[9] printk(KERN_NOTICE "Trying to move old root to /initrd ... ");
error = sys_mount("/old", "/root/initrd", NULL, MS_MOVE, NULL);
if (!error)
printk("okay\n");
else {
int fd = sys_open("/dev/root.old", O_RDWR, 0);
printk("failed\n");
printk(KERN_NOTICE "Unmounting old root\n");
sys_umount("/old", MNT_DETACH);
printk(KERN_NOTICE "Trying to free ramdisk memory ... ");
if (fd < 0) {
error = fd;
} else {
error = sys_ioctl(fd, BLKFLSBUF, 0);
sys_close(fd);
}
printk(!error ? "okay\n" : "failed\n");
}
handle_initrd函数的主要功能是执行initrd的linuxrc文件,并且将realfs的根目录设置为当前目录。
代码[1]:real_root_dev,是一个全局变量保存的是realfs的设备号。
代码[2]:调用mount_block_root函数将initrd文件系统挂载到了VFS的/root下。
代码[3]:提取rootfs的根的文件描述符并将其保存到root_fd。它的作用就是为了在chroot到initrd的文件系统,处理完initrd之后要,还能够返回rootfs。返回的代码参考代码[7]。
代码[4]:chroot进入initrd的文件系统。前面initrd已挂载到了rootfs的/root目录。
代码[5]:执行initrd的linuxrc文件,等待其结束。
代码[6]:initrd处理完之后,重新chroot进入rootfs。
代码[7]:如果real_root_dev在 linuxrc中重新设成Root_RAM0,则initrd就是最终的realfs了,改变当前目录到initrd中,不作后续处理直接返回。
代码[8]:在linuxrc执行完后,realfs设备已经确定,调用mount_root函数将realfs挂载到root_fs的 /root目录下,并将当前目录设置为/root。
代码[9]:后面的代码主要是做一些收尾的工作,将initrd的内存盘释放。
到此代码分析完毕。
![]()
![]()
通过本文前半部分对cpio-initrd和imag-initrd的阐述与对比以及后半部分的代码分析,我相信读者对Linux 2.6内核的initrd技术有了一个较为全面的了解。在本文的最后,给出两点最重要的结论:
1. 尽管Linux2.6既支持cpio-initrd,也支持image-initrd,但是cpio-initrd有着更大的优势,在使用中我们应该优先考虑使用cpio格式的initrd。
2. cpio-initrd相对于image-initrd承担了更多的初始化责任,这种变化也可以看作是内核代码的用户层化的一种体现,我们在其它的诸如FUSE等项目中也看到了将内核功能扩展到用户层实现的尝试。精简内核代码,将部分功能移植到用户层必然是linux内核发展的一个趋势。
![]()
![]()
从下面三篇文章中,可以获得更多的关于initramfs的知识:
[1]http://tree.celinuxforum.org/pubwiki/moin.cgi/EarlyUserSpace
[2]http://lwn.net/Articles/14776/
[3]http://www.ussg.iu.edu/hypermail/linux/kernel/0211.0/0341.html
从下面这篇文章中读者可以了解到关于linux VSF、rootfs的相关知识:
[4] http://www.ibm.com/developerworks/cn/linux/l-vfs/
下面是一些initrd的参考资料:

浙公网安备 33010602011771号