构建initramfs
为什么需要initramfs
在引导过程的最后,内核启动第一个用户进程,内核需要访问根文件系统,加载相应的可执行程序。这就要求内核能够正确驱动根文件系统所在设备。
寻找根文件系统的过程,我们需要考虑以下两种情况:
- 除非是一个专用系统,目标系统的硬件平台是固定不变的,否则,对于一个通用操作系统,将运行在各种不同的硬件平台上。因此,根文件系统可能存储在各种各样的介质上。为了能够键入更多的硬件平台,显然系统需要支持尽可能条的存储设备。但是如果将所有这些设备的驱动全部编译进内核,显然不是一个好办法。将这些驱动编译为模块,存储在根文件系统中,按需载入内存是一个解决问题的办法。
- 根文件系统可能不再一个简单的硬盘上,比如RAID和NFS。某些根文件系统经过压缩、加盟,在挂在前需要解压缩、解密等操作。如果这些都由内核处理,将会使内核变得异常复杂。将复杂的操作移到用户空间是解决上述问题的一个思路。
但是,无论是将驱动编译为模块,还是将处理如RAID挂载的程序存储在文件系统上,都会导致一个鸡和蛋的问题:内核要加载这些模块或者运行这些程序才能正确识别根文件系统所在的设备,但是保存这些模块或者程序的根文件系统又存储在这些设备上。
为了解决上述问题,内核开发者们设计良initramfs机制。initramfs是一个临时的文件系统,其中包含了必要的设备如硬盘、网卡、文件系统等的驱动以及加载驱动的工具机器运行环境,比如基本的c库,动态库的链接加载器等等。同时,哪些处理根文件系统在RAID、网络设备上的程序也存在initramfs中。由第三方程序复杂将initramfs从硬盘装载进内存。以驱动硬盘为例,内核就不必再从硬盘,而是从已经加载到内存的initramfs中获取硬盘控制器等相关驱动力,继而可以驱动硬盘,范文硬盘上的根文件系统,从而解决了前面提到的鸡和蛋的矛盾。
再初始化的最后,内核运行initramfs中的init程序,该程序将弹出硬件设备、加载驱动,挂载真正的文件系统,执行文件系统上的/sbin/iit,进而切换到真正的用户空间,真正的文件系统挂载后,initramfs即完成了使命,其占用的内存也会被释放。
initramfs原理探讨
initramfs是怎样工作的呢?
当2.6版本的内核引导时,在挂载真正的根文件系统之前,首先将挂载一个名为rootfs的文件系统,并将rootfs的根作为虚拟文件系统目录树的总根。那么为什么要使用rootfs这么一个中间过程呢?原因之一还是为了解决鸡和蛋的问题。内核需要根文件系统上的驱动已经持续来驱动和挂载根文件系统,但是这些驱动和持续有可能没有编译进内核,而在根文件系统上。toorfs时一个ramfs,时在内存中的,内核不需要特殊的驱动就可以瓜子rootfs,所以内核使用rootfs作为一个过度的桥梁。
在挂载了rootfs后,内核将Bootloader加载到内存中的initramfs打包的文件解压到rootfs中,而这些文件中包含了驱动以及挂载真正的根文件系统的工具,内核通过加载这些驱动、使用这些工具,实现了挂载真正的根文件系统。此后,rootfs也完成了历史使命,别真正的根文件系统覆盖。但是rootfs作为虚拟文件系统目录书的总根,并不能被卸载。但是没有关系,rootfs基于ramfs,删除其中的文件即可释放其占用的空间。
挂载rootfs
挂载footfs时,涉及了一些文件系统相关的概念,先来链接以下文件系统的物理组织结构,以对这些抽象的giant有个具体的认识。

以ExtX文件系统为例。虽然用于不同操作系统的文件系统其物理存储结构是不同的,但是Linux的虚拟文件系统通过为这些文件系统建立中间适配层,模拟这里介绍的概念来实现对这些文件系统的支持。
ExtX文件系统使用块作为基本存储单元。块的大小是在创建文件系统时指定的。ExtX文件系统将整个分区分成多个块组,除了最后一个块组,其他块组都包含相同数量的块。下面介绍每个块组包含的部分:
- 超级块,超级块描述整个文件系统的信息
- 块组描述符,描述包含所有块组的描述。每个块组描述符存储一个块组的描述信息
- 块位图,快位图用力啊米搜狐块组中哪些块已用、哪些块空闲。其中每个位对一个本块组中的一个块。
- 索引节点位图,索引节点为土用来描述索引节点表中哪些Inode已用、哪些Inode空虚。其中每个为对应索引节点位图中的一个Inode。
- 所以节点表,一个文件除了需要存储数据意外,一些描述信息也需要存储,如文件类型、全新、文件大小、创建/修改/范文时间等,这些信息存储在Inode中而不是数据块中。每个文件都有一个对应的Inode,一个块组中的所有inode组成了索引节点表。除了文件属性信息外,inode中还记录了存储文件数据的数据块。
- 数据块,数据块中存储的就是文件的数字。丹斯对于不同的文件类型,数据库中存储的内容是不同的,以常规文件和目录为例:
- 对于常规文件,数据库中存储的是文件的数据
- 对于目录,数据库中存储的是该目录下的所有文件名和子目录名。
Linux的虚拟文件系统将文件系统组织为属性结构。在初始化阶段,内核挂载rootfs文件系统,虚拟文件系统从无到有,rootfs的根作为虚拟文件系统这颗大树中的第一个节点,自然称为所有后来创建的节点的祖先。
解压initramfs到rootfs
挂载了rootfs后,内核将Bootloader加载到内存中的initramfs中的文件解压到rootfs中。
一旦配置内核支持initramfs,那么内核将编译文件initramfs.c
挂载并切换到真正的根目录
配置内核支持initramfs
- 执行make menuconfig
- 选择 General setup
- 选中 Initial RAM filesystem and RAM disk(initramfs/initrd) support
构建基本的initramfs
我们先建立一个initramfs原型,用来验证内核配置以及这个initramfs原型。我们在/vita目录下创建一个initramfs目录,initramfs的内容保存在这个目录中。
$ cd /vita
$ mkdir initramfs
如果没有在传递给内核的命令行参数中指定“rdinit”,内核启动后,执行的initramfs中的第一个持续是根目录下的init。
基本上所有的init持续都是采用shell脚本编写的,创建文件/vita/initramfs/init,内容如下
$ cd /vita/initramfs
$ echo '#!/bin/bash
echo "Hello Linux!"
exec /bin/bash' > init
为其赋予可执行权限:
$ chmod +x init
init 需要shell来运行,所有initramfs中还需要bash持续:
$ cd /vita/initramfs
$ mkdir bin
$ cp ../sysroot/bin/bash bin/
bash 依赖于libc/libdl/libgcc_s.so.1,因此,我们需要在initramfs中安装这三个库,以及安装在家动态库的动态加载器。
$ cd /vita/initramfs
$ mkdir lib
$ cp -d /vita/sysroot/lib/libdl* lib/
$ cp /vita/sysroot/lib/libc-2.15.so lib/
$ cp -d /vita/sysroot/lib/libc.so.6 lib/
$ cp /vita/cross-tool/i686-none-linux-gnu/lib/libgcc_s.so.1 lib/
$ cp -d /vita/sysroot/lib/ld-* lib/
$ find . | cpio -o -H newc | gzip -9 > /vita/sysroot/boot/initrd.img
$ cd /vita/sysroot/boot
$ scp bzImage initrd.img root@192.168.1.103:/vita/boot
更改实验机的grub.cfg:
menuentry 'vita' {
set root='(hd0,2)'
linux /boot/bzImage root=/dev/sda2 ro
initrd /boot/initrd.img
}
将硬盘驱动编译为模块
initramfs 的重要作用之一就是允许内核将保存根文件系统的存储设备的驱动不再编译进内核。
配置devtmpfs
通常情况下,某些需要从用户空间访问的设备都会在文件系统中建立一个设备文件,作为用户空间访问设备的接口。得益于Linux中虚拟文件系统的涉及,用户空间的持续可以像访问普通文件一样,使用标准的文件访问接口实现与设备的交互。
从2.6.18开始采用udev,/dev目录使用了基于内存的文件系统tmpfs管理设备文件。
Linux2.6.32开始使用devtmpfs。体验devtmpfs需要安装ls和mount。
$ cd /vita/build
$ tar xvf ../source/coreutils-8.20.tar.xz # ls在 coreutils中
$ cd coreutils-8.20
$ ./configure --prefix=/usr
$ make install
$ cd /vita
$ cp sysroot/usr/bin/ls initramfs/bin/
$ cp -d sysroot/lib/librt* initramfs/lib/
$ cp -d sysroot/lib/libpthread* initramfs/lib
$ cd build
$ tar xvf ../source/util-linux-2.22.tar.xz # mount 在util-linux中
$ cd util-linux-2.22
$ ./configure --prefix=/usr --disable-use-tty-group --disable-login --disable-sulogin --disable-su --without-ncurses
$ make
$ make install
$ find $SYSROOT -name "*.la" -exec rm -f '{}' \;
$ cd /vita
$ cp sysroot/bin/mount initramfs/bin
$ cp -d sysroot/lib/libmount.so.1* initramfs/lib
$ cp -d sysroot/lib/libblkid.so.1* initramfs/lib
$ cp -d sysroot/lib/libuuid.so.1* initramfs/lib
$ cd initramfs
$ find . | cpio -o -H newc | gzip -9 > /vita/sysroot/boot/initrd.img # 打包
准备支持devtmpfs的内核,步骤如下:
- 执行make menuconfig
- 选择 Device Drivers
- 选择 Generic Driver Options
- 选中Maintain a devtmpfs filessystem to mount at /dev
编译内核。
将编译好的initrd.img和bzImge放到测试机的/vita/boot目录
修改initramfs中的init文件:
$ cd /vita/initramfs
$ echo '
#!/bin/bash
echo "Hello Linux!"
mount -d -t devtmpfs udev /dev
exec bin/bash' > init
将硬盘控制器驱动配置为模块
与前面硬盘控制器驱动类似,只不过这里我们将“AHCI STAT support”和Intel ESB,ICH,PIIX3, PIIX4 PATA/SATA support“ 配置为模块。编译安装:
$ cd /vita/build/linux-3.7.4
$ make bzImage
$ make modules
$ make INSTALL_MOD_PATH=$SYSROOT modules_install
$ cd /viata
$ mkdir -p initramfs/lib/modules/3.7.4/kernel/drivers/ata/
$ cp sysroot/lib/modules/3.7.4/kernel/drivers/ata/* initramfs/lib/modules/3.7.4/kernel/drivers/ata/
$ cd build
$ tar xvf ../source/kmod-12.tar.xz # 为了加载内核模块,我们需要安装加载、卸载等管理模块的工具
$ cd kmod-12
$ ./configure --prefix=/usr # 这里在12.04里面需要安装xsltproc
$ make
$ make install
$ find $SYSROOT -name "*.la" -exec rm -f '{}' \;
$ cd /vita/initramfs
$ mkdir -p usr/bin
$ cp ../sysroot/usr/bin/kmod usr/bin
$ cp -d ../sysroot/usr/lib/libkmod.so.2* lib/
$ cd /vita/sysroot/sbin
$ ln -s ../usr/bin/kmod insmod
$ ln -s ../usr/bin/kmod rmmod
$ ln -s ../usr/bin/kmod modinfo
$ ln -s ../usr/bin/kmod lsmod
$ ln -s ../usr/bin/kmod modprobe
$ ln -s ../usr/bin/kmod depmod
$ cd /vita
$ mkdir initramfs/sbin
$ cd /vita/sysroot/sbin
$ cp -d insmod rmmod modinfo lsmod modprobe depmod /vita/initramfs/sbin/
$ cp /vita/sysroot/lib/modules/3.7.4/modules.dep.bin /vita/initramfs/lib/modules/3.7.4
$ cd /vita/initramfs
$ mkdir proc sys # 创建挂载目录
修改init脚本用于命令搜索:
$ cd /vita/initramfs
$ echo '#!/bin/bash
echo "Hello Linux!"
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
mount -n -t devtmpfs udev /dev
mount -n -t proc proc /proc
mount -n -t sysfs sysfs /sys
exec bin/bash
' > init
在vmware中,可以把BusLoic作为模块以替代书中的功能。
自动加载硬盘控制器驱动
从2.6版内核开始,Linux采用udev管理驱动模块的加载以及设别节点的管理。每当内核发现新的设备,便通过NETLINK向用户空间发送新设备事件,该事件中记录了设备的相关信息。用户空间的udev服务进程收到内核事件后,根据事件中携带的信息,首先判断该设备的驱动是否以及加载,如果没有,则加载驱动。驱动加载后,内核会再次向用户空间报告发信新设备事件,这是设备以及成功驱动力,并且主次设备号等信息也以及准备好了,udev收到事件后,或者位设备建立节点,或者执行某些特定的操作。

既然有了devtmpfs,为什么还需要udev?
- 首先,devtmfs仅是记录了设备驱动注册的节点。udev除了创建设备节点外,还要复杂加载设备驱动。后者是devtmpfs所不能实现的,devtmpfs仅是一个被动的记录数据的文件系统而已。
- 其次,使用udev,在发现新设备或者设备发生了跟新时,可以有机会执行某些特定的动作。
内核向用户空间发送时间
PC机上的硬盘控制器,无论时IDE接口的,还是SATA接口的,一般都是通过PCI总线连接到计算机上的。内核在引导时,PCI子系统将进行初始化,美军总线上的设备,并尝试位设备匹配驱动;然后将收集到的设备相关信息组织位uevent事件;接着调用kobject_ueven,通过NETLINK将组织好的uevent发送到用户空间,同之udev有新设备了。
udev加载驱动和建立设备节点
udev是用况空间动态管理设备的机制吗,包括加载驱动、管理设备节点等。udev机制的核心是其服务进程udevd。当启动过程进入用户空间阶段后,udevd将被启动。udevd启动后,首先读取并分析所有的规则文件,并将其缓存在内存中。然后,udevd通过NETLINK协议,监听并处理来自内核的uevent事件。每当udevd收到一个内核uevent,udevd均创建一个单独的紫禁城处理uevent。
对于每个内核报告的uevent,udevd根据uevent中的变量逐个匹配规则。当所有的匹配条件都满足后,赋值动作会法相。规则中可以加载驱动模块;规定如何给设备节点命名、建立符号链接;设备连接和断开时分别执行指定的程序。
处理冷插拔设备
当啮合引导美军设备时,系统运行在内核空间,没有启动用户空间的udev服务,因此内核发送到用户空间的uevent会被丢弃。
为了解决这个问题,开发人员基于sys文件系统设计良一种巧妙的机制。在Linux操作系统进入用户空间,udevd启动后,通过sys文件系统请求内核重新发出uevent。
编译安装udev
$ cd /vita/build
$ tar xvf ../source/udev-173.tar.bz2
$ cd udev-173
$ ./configure --prefix=/usr \
--sysconfdir=/etc --sbindir=/sbin --libexecdir=/lib/udev \
--disable-hwdb --disable-introspection \
--disable-keymap --disable-gudev
$ make
$ make install
$ cd /vita
$ cp sysroot/sbin/udevd initramfs/bin/
$ cp sysroot/sbin/udevadm initramfs/bin/
$ mkdir -p initramfs/lib/udev/rules.d
$ cp sysroot/lib/udev/rules.d/80-drivers.rules initramfs/lib/udev/rules.d/
配置内核支持netlink
内核与udevd通过Unix Domain Sockets使用NETLINK协议通信,因此,我们需要配置内核支持Unix Domain Sockets与NETLINK协议。
- 执行 make menuconfig;
- 选中Networking support;
- 选中Networking options;
- 选中Unixdomain sockets。
对于,NETLINK协议,只要配置内核支持网络,NETLINK协议默认就被支持。
配置内核支持inotify
因为udev使用inotify机制检查udev的规则文件是否发生变化,所以配置内饰使其支持inotify机制。步骤:
- 执行 make menuconfig;
- 选中 File Systems;
- 选中 Inotify support for userspace。
安装modules.alias.bin 文件
在安装内核模块时,安装脚本最后会自动调用depmod创建modules.alias.bin/modules.alias文件,我们直接将其复制到initramfs即可。
$ cd /vita
$ cp sysroot/lib/modules/3.7.4/modules.alias.bin \
initramfs/lib/modules/3.7.4/
验证modules.alias.bin是否可以正确执行。需要安装两个工具:一个时lspci,这个工具用来云心在目标系统上,查看硬盘设备在PCI总线上的为子,这个工具在软件包pciutils中;另外一个时coreutils中的工具cat。
$ cd /vita/build
$ tar xvf ../source/pciutils-3.1.10.tar.xz
$ cd pciutils-3.1.10
$ make PREFIX=/usr \
ZLIB=no SHARED=yes PCI_COMPRESSED_IDS=0 all
$ make PREFIX=/usr \
ZLIB=no SHARED=yes PCI_COMPRESSED_IDS=0 install
$ cd /vita
$ cp sysroot/usr/sbin/lspci initramfs/bin
$ cp -d sysroot/usr/lib/libpci.so.3* initramfs/lib/
$ cp -d sysroot/lib/libresolv* initramfs/lib
$ mkdir -p initramfs/usr/share
$ cp sysroot/usr/share/pci.ids initramfs/usr/share/
$ cp sysroot/usr/bin/cat initramfs/bin/
$ mkdir initramfs/run
启动udevd和模拟热插拔
修改init脚本用于命令搜索:
$ cd /vita/initramfs
$ echo '#!/bin/bash
echo "Hello Linux!"
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
mount -n -t devtmpfs udev /dev
mount -n -t proc proc /proc
mount -n -t sysfs sysfs /sys
mount -n -t ramfs ramfs /run
udevd --daemon
udevadm trigger --action=add
udevadm settle
exec bin/bash
' > init
挂载并切换到根文件系统
现在已经正确的驱动了硬盘,那么接下来,我们就切换到硬盘上的真正的根文件系统。
挂载根文件系统
$ cd /vita/initramfs
$ echo '#!/bin/bash
echo "Hello Linux!"
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
export ROOTMNT=/root
export ROFLAG=-r
mount -n -t devtmpfs udev /dev
mount -n -t proc proc /proc
mount -n -t sysfs sysfs /sys
mount -n -t ramfs ramfs /run
udevd --daemon
udevadm trigger --action=add
modprobe BusLogic
udevadm settle
for x in $(cat /proc/cmdline); do
case $x in
root=*)
ROOT=${x#root=}
;;
ro)
ROFLAG=-r
;;
rw)
ROFLAG=-w
;;
esac
done
mount ${ROFLAG} ${ROOT} ${ROOTMNT}
exec bin/bash
' > init
切换到根文件系统
真正的根文件系统已经被gauze了,我们接下来就要切换到真正的根文件系统。
initramsf中的这个init脚本也完成了它的历史使命。系统的第一个进程应该使用根文件系统中的一个程序。真正的用户空间的init程序依然使用shell脚本。一般而言,init存储在/sbin目录下:
$ cd /vita/rootfs
$ mkdir sbin
$ cd sbin
$ echo '#!/bin/bash
exec /bin/bash' > init
$ chmod +x init
根文件系统准备好后,接下来开始向根文件系统切换,步骤如下:
- 删除rootfs文件系统中不再需要的内容,释放内存空间。
- 停止正在运行的进程,这里就是udevd
- 将/dev、/run、/proc和/sys目录移动到真正的文件系统上。
- 将根文件系统从'/root'移动到”/“下。
- 更改进程的文件系统namespace,使其指向真正的根文件系统。因为当前进程就是进程1,而后续进程都是从进程1复制的,所以后续进程的文件系统的namespace自然就是使用的真正的根文件系统。
- 运行真正的文件系统中的init程序。
需要将这几个步骤封装到一个二进制文件中,switch_root.c:
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <fcntl.h>
#include <unistd.h>
int delete_dir(char* directory);
void delete(char *what)
{
if(unlink(what)){
if(errno == EISDIR){
if(!delete_dir(what))
rmdir(what);
}
}
}
int delete_dir(char* directory)
{
DIR *dir;
struct dirent *d;
struct stat st1, st2;
char path[PATH_MAX];
if(lstat(directory, &st1))
return errno;
if(!(dir=opendir(directory)))
return errno;
while((d = readdir(dir))){
if(d->d_name[0] == '.' &&
(d->d_name[1] == '\0' ||
(d->d_name[1] == '.' && d->d_name[2] == '\0')))
continue;
sprintf(path, "%s/%s", directory, d->d_name);
lstat(path, &st2);
if(st2.st_dev != st1.st_dev)
continue;
delete(path);
}
closedir(dir);
return 0;
}
int main(int argc, char*argv[])
{
int console_fd;
chdir(argv[1]);
delete_dir("/");
mount(".", "/", NULL, MS_MOVE, NULL);
chroot(".");
chdir("/");
console_fd=open("/dev/console", O_RDWR);
dup2(console_fd, 0);
dup2(console_fd, 1);
dup2(console_fd, 2);
close(console_fd);
execlp(argv[2], argv[2], NULL);
return 0;
}
脚本:
$ cd /vita/initramfs
$ echo '#!/bin/bash
echo "Hello Linux!"
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
export ROOTMNT=/root
export ROFLAG=-r
mount -n -t devtmpfs udev /dev
mount -n -t proc proc /proc
mount -n -t sysfs sysfs /sys
mount -n -t ramfs ramfs /run
udevd --daemon
udevadm trigger --action=add
modprobe BusLogic
udevadm settle
for x in $(cat /proc/cmdline); do
case $x in
root=*)
ROOT=${x#root=}
;;
ro)
ROFLAG=-r
;;
rw)
ROFLAG=-w
;;
esac
done
mount ${ROFLAG} ${ROOT} ${ROOTMNT}
udevadm control --exit
mount -n --move /dev ${ROOTMNT}/dev
mount -n --move /run ${ROOTMNT}/run
mount -n --move /proc ${ROOTMNT}/proc
mount -n --move /sys ${ROOTMNT}/sys
switch_root ${ROOTMNT} /sbin/init
' > init
在根文件系统中安装两个程序cat和ls:
$ cd /vita
$ cp sysroot/usr/bin/cat rootfs/bin
$ cp sysroot/usr/bin/ls rootfs/bin