转帖:ubuntu/debian initrd 过程

from:

http://unixboy.javaeye.com/blog/157717

自己最近花时间研究了一下 linux 的 initial ram disk,然后就写了这篇文章。因为是要给别人看的,所以写的时候就比较小心,不能放过细节。这样就要搞清楚之前不太在意的一些问题,对自己也是一个提高的过程。而且写的过程中发现,有些话不是 很好组织,估计是自己的写作能力下降了吧。所以,linux 版的广大版友们,没事把自 己研究的一些东西写一写贴上来对大家都是有好处的 8^)

虽然本文讲的是 Debian etch 的 initrd,但是基本原理都是差不多的。希望此文 对大家有所帮助。

Debian etch 基本系统 initial ram disk 的分析

本文适合的人群

最基本的当然是你正在用或用过 linux。

扩展的要求:你要会一些 shell script,最好还知道一点内核模块方面东西。关于这两点你只需要知道一点点就可以了,因为这时候你已经知道到哪里查你不知道的东西了。

什么是 initial ram disk (缩写 initrd)

它是由 bootloader 初始化的内存盘。在 linux 启动之前,bootloader 会将它(通常是 initrd.img-xxx...xxx 文件)加载到内存中。内核启动的时候会将这个文件解开,并作为根文件系统使用。

设计 initrd 的主要目的是让系统的启动分为两个阶段。首先,带有最少但是必要的驱动的内核启动。然后,其它需要的模块将从 initrd 中根据实际需要加载。这样就可以不必将所有的驱动都编译进内核,而根据实际情况有选择地加载。对于启动较慢的设备如 usb 设备等,如果将驱动编译进内核,当内核访问其上的文件系统时,通常设备还没有准备好,就会造成访问失败。所以,通常在 initrd 中加载 usb 驱动,然后休眠几秒钟,带设备初始化完成后,再挂载其中的文件系统。

initrd 的具体形式

目前有两种形式:cpio-initrd 和 image-initrd。

image-initrd 的制作相对麻烦,处理流程相对复杂(内核空间->用户空间->内核空间 与初始化越来越多的在用户空间进行的趋势不符),本文不对其进行介绍。

cpio-initrd 的处理流程(内核空间->用户空间):

1. boot loader 把内核以及 initrd 文件加载到内存的特定位置。

2. 内核判断 initrd 的文件格式,如果是 cpio 格式。

3. 将 initrd 的内容释放到 rootfs 中。

4. 执行 initrd 中的 /init 文件,执行到这一点,内核的工作全部结束,完全交给 /init 文件处理。

cpio-initrd 的制作:

首先在一个目录中建立必要的文件及目录。例如:

song@ubuntu:/home/linux_src/initrd/debian_etch/initrd$ ls -l

总用量 5

drwxr-xr-x 2 song song 864 2007-05-01 21:37 bin

drwxr-xr-x 3 song song 160 2007-05-01 21:37 conf

drwxr-xr-x 4 song song 136 2007-05-01 21:37 etc

-rwxr-xr-x 1 song song 3233 2007-05-02 15:16 init

drwxr-xr-x 4 song song 416 2007-05-01 21:37 lib

drwxr-xr-x 2 song song 48 2007-04-14 15:59 modules

drwxr-xr-x 2 song song 208 2007-05-01 21:37 sbin

drwxr-xr-x 11 song song 400 2007-05-01 21:37 scripts

然后,将这些内容打成 gzip 压缩过的 cpio 包:

song@ubuntu:/home/linux_src/initrd/debian_etch/initrd$ find . | cpio -o -H newc | gzip -9 > ../initrd.img.gz

20500 blocks

song@ubuntu:/home/linux_src/initrd/debian_etch/initrd$ ls -l ../initrd.img.gz

-rw-r--r-- 1 song song 4493175 2007-05-02 17:17 ../initrd.img.gz

解包:

首先建立一个空目录,然后进入那个目录,并运行相应的命令。例如,在 /home/linux_src/initrd/debian_etch 目录下存在 initrd.img-2.6.18-4-686 文件,我们现在要把它解开,过程如下:

song@ubuntu:/home/linux_src/initrd/debian_etch$ mkdir tmp

song@ubuntu:/home/linux_src/initrd/debian_etch$ cd tmp

song@ubuntu:/home/linux_src/initrd/debian_etch/tmp$ gzip -dc ../initrd.img-2.6.18-4-686 | cpio -idm

20500 blocks

song@ubuntu:/home/linux_src/initrd/debian_etch/tmp$ ls -l

总用量 5

drwxr-xr-x 2 song song 864 2007-05-02 17:23 bin

drwxr-xr-x 3 song song 160 2007-05-02 17:23 conf

drwxr-xr-x 4 song song 136 2007-05-02 17:23 etc

-rwxr-xr-x 1 song song 3213 2007-03-08 06:30 init

drwxr-xr-x 4 song song 416 2007-05-02 17:23 lib

drwxr-xr-x 2 song song 48 2007-04-14 15:59 modules

drwxr-xr-x 2 song song 208 2007-05-02 17:23 sbin

drwxr-xr-x 11 song song 400 2007-05-02 17:23 scripts

我的实验环境

我用的系统是 ubuntu 6.10。系统中安装了 qemu 虚拟机,虚拟机里网络安装了 Debian etch 基本系统(在科大[中国科学技术大学的简称]安装 Debian 是一件理所当然的事情,因为科大的 Debian 源真的很好)。

顺便说一下,在用 qemu 的时候,如果从命令行里指定使用的 kernel 和 initrd,一定也要同时指定硬盘。另外,如果想要运行得更快,要下载并安装 kqemu。

initrd 中 init 脚本的分析

由前面 cpio-initrd 的处理流程可以看到,内核在将其解开并放入 rootfs 后,将要执行 /init 文件,所以我们分析的重点就是这个文件。其它的文件请结合具体的源码与本文的内容进行理解。

[1] !/bin/sh

可能有的人在想,这个 init 文件虽然具有可执行权限,但是并不是二进制代码,而是一个 shell script ,必须要由 shell 进行解释,那么为什么直接调用就可以运行呢?确实,第一行代码表明了这是一个脚本文件,这与教导我们 shell 编程的书写的一样,在文件的第一行要写上这么一段特殊的文字。除了我们可以通过这段文字了解脚本的类型之外,还有谁会关心这段文字呢?内核。内核通过文件头来确定应该怎样执行,就像 a.out 格式和 elf 格式一样。我们只需要在 shell 里输入具有可执行权限的文件名,并不关心文件的格式,内核会做好一切的。

那么对于脚本,内核是怎样处理的呢?

不知大家注意到了没有,不管我们写什么样的脚本,第一行都是这样的形式:

!/path/to/parser

开头的 "!" 会告诉内核要通过调用 /path/to/parser 来解释当前要运行的文件。这就是我们为什么可以直接调用 init 脚本就可以执行的原因。

[3] echo "Loading, please wait..."

这一行代码不懂吗?如果是真的,我劝你不要读下去了。因为我不是魔鬼,我的文章不是用来折磨人的。

[5] [ -d /dev ] || mkdir -m 0755 /dev

[ -d /root ] || mkdir --mode=0700 /root

[ -d /sys ] || mkdir /sys

[ -d /proc ] || mkdir /proc

[ -d /tmp ] || mkdir /tmp

[10] mkdir -p /var/lock

mount -t sysfs none /sys

[12] mount -t proc none /proc

这一部分的代码很简单,就是创建相应的目录,并且挂载相应的文件系统。其中, /root , /tmp 和 /var/lock 文件夹是作为一般的目录使用,其它的均是作为挂载点。我们系统的根目录所在分区将先被挂载到 /root 目录。从第11和12行的代码中可以看到 sysfs 和 proc 分别被挂载到了 /sys 和 /proc 目录下。从下面可以看到,这 /sys 目录主要是给 udev 使用的。udev 在收到 uevent 处理相应的规则的时候需要查找 /sys 目录。在后面 kill 掉 udevd 的时候,需要通过 /proc 目录得到 udevd 的 pid,而且内核启动的参数也需要从 /proc/cmdline 得到。

Note that this only becomes /dev on the real filesystem if udev's scripts

[15] are used; which they will be, but it's worth pointing out

tmpfs_size="10M"

if [ -e /etc/udev/udev.conf ]; then

. /etc/udev/udev.conf

fi

[20] mount -t tmpfs -o size=$tmpfs_size,mode=0755 udev /dev

这里首先设定了tmpfs的大小为10M,然后判断文件 /etc/udev/udev.conf 是否存在,若存在则读取该文件,并执行其中的命令。 /etc/udev/udev.conf 文件设定了 udev_log 的级别(具体参见 man udev),并设定了tmpfs_size为 10M,也就是说,在init里面设置的tmpfs_size的大小会被 /etc/udev/udev.conf 中的设置覆盖掉。当然,在此版本中,对tmpfs_size的大小设置均是10M。在这些设置好之后,将 tmpfs 挂载到 /dev 目录。其中,参数 udev 可以随便取,因为我们挂载的是 tmpfs。此处取成 udev 应该是为了表明系统是通过 udev 对设备进行管理。

[21] [ -e /dev/console ] || mknod /dev/console c 5 1

[ -e /dev/null ] || mknod /dev/null c 1 3

> /dev/.initramfs-tools

[24] mkdir /dev/.initramfs

接下来创建 /dev/console 节点,/dev/console 总是代表当前终端,在最后通过 exec 命令用指定程序替换当前 shell 时使用。/dev/null 也是很常用的,凡是重定向到它的数据都将消失得无影无踪。FIXME: /dev/.initramfs-tools 的作用? usplash 会使用 /dev/.initramfs 目录。usplash 会在机器启动的时候提供类似 windows 的启动画面,ubuntu linux 的启动画面就是通过 usplash 实现的。由于在 /sbin 目录当中没有任何 usplash 相关的文件,所以我们可以忽略这个目录的存在。

[26] Export the dpkg architecture

export DPKG_ARCH=

. /conf/arch.conf

Export it for root hardcoding

[31] export ROOT=

DPKG_ARCH 表明了当前运行linux的计算机的类型,对一般的pc是大多 i386,也可能是别的比如 powerpc 一类的。在下文中我们将会看到这个变量决定了在 /scripts/init-premount/thermal 中加载的关于cpu温度传感器及cpu风扇的内核模块。第27行用了export,是为了让这个变量不仅在此shell环境中有效,而且在它的子 shell环境中仍然有效。而且在第27行export DPKG_ARCH 变量的时候,让 DPKG_ARCH 变量等于空。这样,当前运行的计算机的类型就完全由 /conf/arch.conf 决定了。而且,第28行没有判断是否存在 /conf/arch.conf 文件就直接引用了,也明确了这一点。那么,我们在将这个 initrd.img port到其它类型的计算机时,只需要更改 /conf/arch.conf 文件并不需要对 init 作改动(当然,二进制代码肯定要是新的)。

ROOT 是你计算机启动之后的根目录所在的分区(如: /dev/hda3, UUID=xxx-...-xxx 等),此处将其export为空,其值将在下面解析跟在内核后面的参数时被赋予。

[33] Bring in the main config

. /conf/initramfs.conf

[35] for i in conf/conf.d/*; do

[ -f ${i} ] && . ${i}

done

[38] . /scripts/functions

第34行引入主配置文件 /conf/initramfs.conf。这个配置文件实际上是 mkinitramfs(8) 的配置文件,其中定义了一些变量,并赋予了适当的值,如 BOOT=local 则默认从本地磁盘启动(可以是可移动磁盘)。BOOT 变量的值实际上是 /scripts 目录下的一个文件,可以是 local 或是 nfs。在此 init 脚本挂载将要进入的系统的根目录所在分区的时候,会先读取并运行 /scripts/${BOOT} 文件(见此脚本的第150行)。在这个文件中定义了 mountroot 函数,对于 local 启动和 nfs 启动此函数的实现不同。这样通过对不同情况引入不同的文件,来达到同样名称的函数行为不同的目的。这就导致了第152行具体挂载的行为和启动方式相关。

第35到37行,引入 /conf/conf.d 下的所有文件,注意在引入的时候用了 -f 参数判断,这样只有普通的文件才会被引入,链接(硬链接除外)、目录之类的非普通文件不会被引入,所以,如果要在这个 initrd.img 的基础上添加自己的配置,不要妄图通过软链接来引入,除非你把这个init脚本改了。

在 /conf/conf.d 目录下只存在一个文件 resume, 其中定义了 RESUME 变量,在我这里 RESUME=/dev/hda5,因为我是在 qemu 里面装的 Debian,这个 /dev/hda5 是我的 swap 分区。如果安装了 uswsusp 包,在计算机上(不是在 initrd.img 里) 就会有 s2disk,s2both,s2ram 这样的程序,还会有一个 /dev/snapshot 设备节点,其中前面两个会把计算机的当前状态通过 /dev/snapshot 保存起来,默认是在 swap 分区。这样我们就可以实现计算机的休眠了。在唤醒的时候,会通过 initrd.img 里多出的 /sbin/resume 程序访问 RESUME 变量所指的分区,恢复调用 s2disk 或 s2both 前的状态。此版本的 initrd.img 中存在 /bin/resume,但是没有 /sbin/resume 文件。

FIXME: 关于以上 resume 的解释可能不准确。

第38行引入了要用到的函数,这些函数都在 /scripts/functions 文件中定义。在下文中将对遇到的函数进行解释。

[40] Export relevant variables

export break=

export init=/sbin/init

export quiet=n

export readonly=y

[45] export rootmnt=/root

export debug=

export cryptopts=${CRYPTOPTS}

export ROOTDELAY=

[49] export panic=

export 一些变量。

break 由 maybe_break 函数使用。若 break 的值同 maybe_break 的第一个参数相同,则 maybe_break 函数调用 panic 函数(注意 panic 函数和 panic 变量是不同的)。 若 panic 变量为"0"(此处是字符串,其内容是"0",不是整数),则 panic 函数将重新启动机器。其他情况下(包括 panic 变量为空的情况)都将以交互的方式调出shell,此shell的输入输出使用已经创建好的节点 /dev/console。

init 此变量指定在这个脚本最后要执行的进程。 此处 /sbin/init 是系统上所有进程的父进程,负责开启其它进程。当然,你也可以把它换成其他的程序,甚至是 ls,不一定非要是 /sbin/init,虽然这样你的系统启动之后什么都不能做。

quiet 指定为非"y",会显示一些启动的状态信息;若指定为"y"则不显示这些信息。

rootmnt 最终进入的系统的根目录所在分区挂载到的目录。在最终进入系统的时候,这个目录下的东西将要转变为你的根目录(可通过 pivot_root 或 chroot 命令,或系统调用。本脚本最后调用的 run-init 是通过 chroot 系统调用的方式实现的。

readonly 如果 readonly 等于字符串"y",则以只读方式挂载最终要进入的系统的根目录所在分区到 ${rootmnt} 目录,其他情况(包括 readonly 为空)以读写方式挂载。

debug debug mode,具体见下文对第110行和第115行的分析。

cryptopts 加密选项? FIXME:这个的用途?

ROOTDELAY 在 mountroot 函数中使用,root设备在ROOTDELAY时间内必须准备好,否则 mountroot 调用 panic 函数导致进入shell或机器重启(具体见 break 的说明)。若不指定 ROOTDELAY 的值,其值在 mountroot 函数中默认是180妙。

panic 描述见 break参数的说明。

[51] Parse command line options

[52] for x in $(cat /proc/cmdline); do

case $x in

init=*)

[55] init=${xinit=}

;;

root=*)

ROOT=${xroot=}

case $ROOT in

[60] LABEL=*)

ROOT="/dev/disk/by-label/${ROOTLABEL=}"

;;

UUID=*)

ROOT="/dev/disk/by-uuid/${ROOTUU

[65] ;;

/dev/nfs)

BOOT=nfs

;;

esac

[70] ;;

rootflags=*)

ROOTFLAGS="-o ${xrootflags=}"

;;

rootfstype=*)

[75] ROOTFSTYPE="${xrootfstype=}"

;;

rootdelay=*)

ROOTDELAY="${xrootdelay=}"

;;

[80] cryptopts=*)

cryptopts="${xcryptopts=}"

;;

nfsroot=*)

NFSROOT="${xnfsroot=}"

[85] ;;

ip=*)

IPOPTS="${xip=}"

;;

boot=*)

[90] BOOT=${xboot=}

;;

resume=*)

RESUME="${xresume=}"

;;

[95] noresume)

NORESUME=y

;;

panic=*)

panic="${xpanic=}"

[100] ;;

quiet)

quiet=y

;;

ro)

[105] readonly=y

;;

rw)

readonly=n

;;

[110] debug)

debug=y

exec >/tmp/initramfs.debug 2>&1

set -x

;;

[115] debug=*)

debug=y

set -x

;;

break=*)

[120] break=${xbreak=}

;;

break)

break=premount

;;

[125] esac

done

这一段代码很简单,就是解析加在kernel后面的参数。

第52行是从 /proc/cmdline 里面得到每一个内核参数,并在接下来的case当中进行处理。cat一下你现在的linux系统的 /proc/cmdline 文件,你就会看到当前系统启动时候的参数。

init=* 决定 init 变量的值。在第42行中已经赋予了 "/sbin/init" 的值,可以用 init=your_program 来代替,your_program 理论上可以随便是什么可执行的文件,因为没人限制你做你想做的事情。

root=* 显然是指定即将要进入的系统的根目录。在其下还有一个case判断,根据参数形式的不同,对 ROOT 变量赋予不同的字符串。注意此时 udev 并没有启动,目前对 ROOT 赋予的仅仅是字符串而已。待 udev 启动之后,就可以通过 /dev/disk/* 下面的连接得到 ROOT 适当的具有 /dev/hdxx 等形式的值。现在常用的是 root=UUID=x...x-...-xxx 的形式,如 root=UUID=fa96108d-afa0-45a8-ba28- c80d1673d958。不建议使用 root=LABEL=* 的形式,自己看一下 /dev/disk/by-label 和 /dev/disk /by-uuid 下面的软链接就知道了,并不是所有的分区都有 label,但是所有的分区都有 uuid。如果参数是 root=/dev /hdxx 形式的值,其值将保持不变,但是不推荐这样做,推荐使用 root=UUID=* 的形式。因为,如果分区数目变化或增减机器的硬盘,经常会造成设备节点的变化,/dev/hdxx 就不是原来的 /dev/hdxx 了。而使用 UUID 的形式,通过 /dev/disk/by- uuid 下面的软链接就可以得到对应的设备节点,无论设备节点怎样变化。因为 udev 会照顾好这些软链接和设备节点的关系的。分区的 UUID 可以通过 vol_id (/sbin/vol_id 或 /lib/udev/vol_id) 命令得到。用 UUID 来挂载分区和用 /dev /hdxx 的方式是一样的。比如说,将 /dev/hdyy 挂载到 /media/hdyy 目录下用 mount /dev /hdyy /media/hdyy,现在只是将 /dev/hdyy 换成它的 UUID: mount UUID=y...y-...-yyy /media/hdyy。 自己对nfs不熟悉,所以略过 root=/dev /nfs。

rootflags=* 指定将要进入的系统的根目录所在的分区挂载到 ${rootmnt} 目录时的参数,并将参数转化成 mount 命令可识别的形式(就是在前面加了"-o ",具体见 man mount)赋予 ROOTFLAGS 变量。

rootfstype=* 指定将要进入的系统的根目录所在的分区的文件系统的格式(如 vfat, ext3 等),赋予 ROOTFSTYPE 变量。

rootdelay=* 指定将要进入的系统的根目录所在的分区必须在多少秒之内准备好,其值赋予 ROOTDELAY 变量。

以上 ROOT, ROOTFLAGS, ROOTFSTYPE, ROOTDELAY 以及上面提到的 rootmnt 变量将在 mountroot 函数(/scripts/local 中定义)中使用,在下文介绍到挂载系统根目录所在分区的时候将详细介绍。

cryptopts=* 加密选项?FIXME: 具体用途?

nfsroot=* nfs 相关。略过。

ip=* nfs 相关。略过。

boot=* 赋予 BOOT 变量相应的值,即在挂载将要进入的系统的根目录所在的分区之前要读取并执行的文件,具体见上文对第34行代码的说明。

resume=* 指明存放 system snapshot image 的分区。具体见对第35行代码的说明。

noresume 赋予 NORESUME 变量字符串"y"。若 NORESUME 变量不为空,则禁止休眠后的唤醒。若在休眠之后的一次启动使用了 noresume 参数,则在再一次启动的时候,不会有恢复状态的过程,而是像普通的启动一样进入系统。这是因为正常启动系统 swap 分区被重新 activate 的缘故。FIXME: 上一句话对吗?

panic=* 赋予 panic 变量相应的值,具体见对此脚本第41行 break 变量的解释。

quiet 赋予 quiet 变量相应的值,具体见对此脚本第43行 quiet 变量的解释。

ro, rw 不同选项赋予 readoly 变量相应的值,readonly 变量在 mountroot 函数中控制以只读或读写方式挂载分区。

debug debug 模式。赋予 debug 变量值"y",并通过 exec 把当前 shell 的 stdout 和 stderr 都重定向到了文件 /tmp/initramfs.debug,这样平常输出到终端的文字都被输出到了那个文件。最后,set -x 相当于在 shell 被调用时添加了 -x 参数,每一条命令在执行之前都被输出到了 stderr,并且在前面添加了一个'+',便于debug。

debug=* 除了没有将 stdout 和 stderr 重定向之外,其余同上。

break=* 赋予 break 变量相应的值,具体见对此脚本第41行 break 变量的解释。

break 赋予 break 变量值"premount",其余同上。

[128] if [ -z "${NORESUME}" ]; then

export resume=${RESUME}

[130] fi

若 NORESUME 变量不为空,则将 RESUME 变量的值赋给 resume 变量,并把 resume 变量 export 出去,使得在子 shell 环境中也可以使用 resume 变量。resume 变量将在 /scripts/local-premount/resume 脚本中使用。具体见下文第148~153行代码中对 /scripts/local 中定义的 mountroot 函数的分析。

[132] depmod -a

在下面装入内核模块之前生成 initrd.img 里面的各内核模块之间的依赖关系。

[133] maybe_break top

具体件对本脚本第41行 break 的说明。

[135] Don't do log messages here to avoid confusing usplash

[136] run_scripts /scripts/init-top

从第136行的代码中很容易看出来,这里是在为下面各个过程做准备。

run_scripts 函数(/scripts/functions 中定义)的唯一一个参数是一个目录,这个目录中所有的具有可执行权限的文件都将被执行。并且,在这些文件被执行之前,它们所要求的必须在它们之前执行的文件会被执行。这就要求这些可执行文件必须按照某一个规则编写,以便我们可以得到它们的先决条件,这就是:这些文件必须能够处理 prereqs 参数。有了这个条件,我们就可以带这个参数调用相应的文件,然后从 stdout 里得到必须在它们之前执行的命令。

在 /scripts/init-top 目录当中只存在 framebuffer 这样一个脚本文件,它负责解析 splash*, vga=* 和 video=* 这样的参数,并依照处理结果加载适当的内核模块,设置适当的显示模式。另外,/dev/tty{0..8} 这8个tty也在这里被创建。其中,/dev/tty0 代表当前终端(它代表的终端和X下的终端是不同的)。

[138] maybe_break modules

log_begin_msg "Loading essential drivers..."

[140] load_modules

[141] log_end_msg

第138行具体件对本脚本第41行 break 的说明。

log_begin_msg 函数(/scripts/functions 中定义)功能很简单,就是将其所有的参数输出。

load_modules 函数(/scripts/functions 中定义)从 /conf/modules 中读取要加载的内核模块,在模块不存在时并不显示错误信息。在 /conf/modules 中只有 unix 模块,它提供对 Unix domain sockets 的支持。许多程序 (如 X Window, syslog 等)即使没有联网也需要这些 sockets。但是,在 /lib/modules/2.6.18-4-686 中并不存在 unix.ko 模块,所以我们可以推断,其已经被编译到了内核中。

log_end_msg 函数(/scripts/functions 中定义)仅仅输出"Done."字符串,表示这一阶段已完成。

[143] maybe_break premount

[ "$quiet" != "y" ] && log_begin_msg "Running /scripts/init-premount"

[145] run_scripts /scripts/init-premount

[146] [ "$quiet" != "y" ] && log_end_msg

这段代码从字面上理解是为接下来挂载将要使用的系统的根目录所在的分区作准备。在 /scripts/init-premount 目录下存在两个脚本:thermal 和 udev。

thermal 根据上面介绍过的 DPKG_ARCH 变量决定需要加载的控制 cpu 温度传感器和风扇的内核模块。

udev 以 daemon 的方式启动 udevd,接着执行 udevtrigger 触发在机器启动前已经接入系统的设备的 uevent,然后调用 udevsettle 等待,直到当前 events 都被处理完毕。之后,如果 ROOTDELAY 变量不为空,就sleep ROOTDELAY 秒以等待 usb/firewire disks 准备好。

[148] maybe_break mount

log_begin_msg "Mounting root file system..."

[150] . /scripts/${BOOT}

parse_numeric ${ROOT}

mountroot

[153] log_end_msg

第150行读取并执行 /scripts/${BOOT} 中的命令。由于我们前面并没有讲 nfs 作为将要使用的系统的根目录,所以我们这里假定本地启动 BOOT=local。其它信息请参照对第34行代码的说明。在这个假定的前提下,第150行的代码就引入了 /scripts/local 文件,这个文件定义了具有本地启动行为的 mountroot 函数,这样在第152行调用 mountroot 就会把将要使用的系统的根目录所在的分区挂载到 ${rootmnt}。

下面我们来看一下 /scripts/local 中定义的 mountroot 函数是如何工作的。

首先,它通过 run_scripts 函数(见第136行代码的说明)执行 /scripts/local-top 目录下所有具有可执行权限的文件。在这个目录下有3个文件:lvm,mdrun 和 udev_helper。

lvm 是逻辑卷管理方面的脚本,我没有过(估计一般pc很少有人会用),而且其中调用的具有可执行权限的文件在此 initrd.img 中也不存在。因为这个脚本在运行的时候会先检查需要的文件是否存在,若不存在则退出,所以这个脚本相当于什么也没做。略过。

mdrun 是 raid 方面的脚本。它要求 udev_helper 先被执行(见第136行代码的说明)其中用到的具有可执行权限的文件在此 initrd.img 中不存在。这等效于这个脚本不起作用。

udev_helper 脚本 mdrun 的先决条件,根据实际情况 ide-generic 模块可能会被加载。

在这三个脚本执行过之后,mountroot 函数会查看 ROOT 设备节点是否已经存在,如果不存在将等待 ${ROOTDELAY} 秒。若在这段时间内 ROOT 设备节点没有出现则调用 panic 函数(见第41行的说明)重启机器或是生一个交互 shell。

若 ROOT 设备节点已经存在,则查看 ROOTFSTYPE 变量是否为空。若不空,则 FSTYPE 变量的值就是 ${ROOTFSTYPE};否则通过 eval 调用 fstype 命令得到 ROOT 的分区格式。其中,fstype 命令会输出 FSTYPE=blabla 类型的字符串,它跟在 eval 后面就相当于作了 FSTYPE=blabla 这样的赋值操作。如果经过这一步之后 ROOTFSTYPE 的值是 "unknown"(包括通过在 kernel 后添加 rootfstype=unknown 参数和 fstype 输出的 FSTYPE=unknown),则 mountroot 函数调用 /lib/udev/vol_id 得到分区的格式。此时,FSTYPE 的值仍有可能是 "unknown"。如果是这样的话,在最后的 mount 操作就会失败。或许你会觉得这里要判断分区格式是不是很麻烦。是的,确实如此。但是要知道这里的 mount 不会自己判断分区格式,所以要在参数中指定。

在得到了 FSTYPE 之后,mountroot 函数调用 run_scripts 函数运行 /scripts/local-premount 下面具有可执行权限的文件。

在 /scripts /local-premount 目录中只有一个具有可执行权限的脚本 resume。此脚本负责在计算机休眠后恢复休眠前的状态。若 resume 变量为空或者这个变量所指的设备不存在,则直接退出;否则,运行 /bin/resume 恢复状态。FIXME: 如果安装了 uswsusp 包,在 /scripts/local-premount 目录下会多一个 uswsusp 脚本,它会调用 /sbin/resume 关于这两个脚本的关系目前不是很清楚。

在这之后,mountroot 函数根据变量 readonly 确定是以只读还是读写的方式挂载,根据 FSTYPE 变量加载适当得内核模块。在得到了所有必要的参数之后,通过 mount 命令将将要进入的系统的根目录所在的分区挂载到 ${rootmnt} 目录下。

最后,mountroot 函数通过 run_scripts 函数执行 /scripts/local-bottom 下具有可执行权限的文件。由于在此目录下没有文件,所以这一步什么都没有做。

第151行的 parse_numeric 函数( /scripts/functions 中定义)从它的注释中可以看出,这个是为了和 lilo 兼容而存在的。由于现在一般用 grub 作为 bootloader,我们平常写的 root=/dev /hdxx,root=LABEL=xx...xx 或 root=UUID=x...x-...-xxx 的形式都会造成此函数的直接返回,相当于什么都没有做。由于我没有用过 lilo,所以对于下面 lilo 的处理,我也不好说什么。

第152行就是调用 mountroot 函数挂载分区了,具体的细节上面已经说过了,这里就不再重复。

[155] maybe_break bottom

[ "$quiet" != "y" ] && log_begin_msg "Running /scripts/init-bottom"

run_scripts /scripts/init-bottom

[158] [ "$quiet" != "y" ] && log_end_msg

在 /scripts /init-bottom 目录下只有一个具有可执行权限的脚本文件 udev。在这个脚本当中,首先停止 udevd 进程,然后删除 /dev /.udev/queue/ 目录。接下来读取并执行 /etc/udev/udev.conf 文件。在这之后,判断 no_static_dev 变量是否为空。若是,则建立 /dev/.static/ 及 /dev/.static/dev/ 目录,并把 ${rootmnt}/dev 目录通过 mount 命令 bind 到 /dev/.static/dev 目录。从一上行为很容易理解 .static/dev 目录目录的含义,它就是用来放硬盘上的 ${rootmnt}/dev 当中东西的地方。因为不是动态建立的,所以放在 /dev/.static 目录下。之后,把 /dev 目录 move 到 ${rootmnt}/dev 目录。通过以上操作就把磁盘上 /dev 目录中的内容和在此脚本动态运行过程中建立的 /dev 目录中的内容整合了起来,一起放到了 ${rootmnt}/dev 目录下。

因为此时 /dev 目录中已经没有东西了,所以现在删除这个目录,然后做一个叫 /dev 的软链接指向 ${rootmnt}/dev 目录。因为现在的根目录在 tmpfs 文件系统中,而 ${rootmnt}/dev 目录在磁盘上的文件系统中(如 ext2, reiserfs 等),不是同一个文件系统,所以做硬链接是不可能的,我们只能做一个软链接。

[160] Move virtual filesystems over to the real filesystem

mount -n -o move /sys ${rootmnt}/sys

[162] mount -n -o move /proc ${rootmnt}/proc

这段代码把当前的 /sys 和 /proc 移动到 ${rootmnt}/sys 和 ${rootmnt}/proc 下面。不要忘了,${rootmnt} 才是我们最终要使用的系统的根目录所在的地方。

[164] while [ ! -x ${rootmnt}${init} ]; do

[165] panic "Target filesystem doesn't have ${init}"

[166] done

这段代码检查 ${rootmnt}${init} 是否存在,也就是下面我们把根目录切换到 ${rootmnt} 下时要执行的 ${init},在上面 init 变量已经被赋值 "/sbin/init"。如果不存在,则通过 panic 函数生一个交互的 shell,或重启机器。这取决于 panic 变量。具体见第41行中对 break 变量的说明。

[168] Confuses /etc/init.d/rc

if [ -n ${debug} ]; then

[170] unset debug

[171] fi

因为在最终要使用的系统的 /etc/init.d/rc 中通过 debug 变量来显示要执行的一些命令,其中 debug=echo 那一行是注释掉的。所以这里要 unset debug 变量,否则 /etc/init.d/rc 的执行会出问题。

[173] Chain to real filesystem

maybe_break init

[175] exec run-init ${rootmnt} ${init} "$@" <${rootmnt}/dev/console >${rootmnt}/dev/console

这一段代码是这个 init 脚本的最会部分,第175行把系统的启动交给了将要进入的系统的 ${init} (上面初始化为 "/sbin/init"),并用 /dev/console 作为输入与输出的设备。

那么这个 run-init (/bin/run-init) 究竟作了些什么。我们得到 klibc-utils 源码包并解开之后,run-init 的源码在 klibc-1.4.34/usr/kinit/run-init 目录下。这个程序要完成的功能的核心在 run-init.c 的第88行,run_init(realroot, console, init, initargs) (runinitlib.c 中定义)函数的调用。坐在这个函数中首先通过 chdir 调用将目录切换到了 realroot。因为此时还没有改变根目录,所以 / 和 . 应该不是同一个目录。然后确认 / 和 . 不在同一个文件系统上(注意,同样的分区格式,不同的分区,也是不同的文件系统)。接下来确定存在 /init 文件,并且当前的根目录所在的文件系统类型是 ramfs 或 tmpfs。在这一切都确定之后,通过 nuke_dir("/") (runinitlib.c 中定义)调用删除当前根目录下除挂载点以外的内容,以释放它们所占用的内存。紧接着把当前目录,也就是 realroot 通过 mount 调用移动到根目录,并通过 chroot 函数将根目录设为当前目录,再通过一个 chdir("/") 调用改变当前工作目录为根目录。现在,我们剩下的只是让 /sbin/init 跑起来。但在开始之前要得到 0, 1, 2 三个文件描述符,用来做我们的 stdin, stdout 和 stderr。在得到这些之后就通过 execv(init, initargs) 调用让我们的 /sbin/init 跑起来了。

FIXME: 原以为第175行的代码也可以用下面的脚本来代替,但是在 qemu(装的 Debian etch) 以及物理机器(装的 ubuntu 7.04) 中试验 pivot_root . initrd 那一行失败,错误信息:pivot_root: Invalid argument. Google 了一下,貌似 2.6.14 及其以后就不行了,好像是和 root_fs 有关。所以不要试图使用下面形式的代码了:

cd /${rootmnt}

mkdir -p initrd

pivot_root . initrd

chroot . ${init} $@ <dev/console >dev/console 2>&1

小结

好了,上面我已经说了这么多。那么,init 脚本究竟都作了什么呢?

首先,建立一些必要的文件夹作为程序工作的时候需要的目录或者必要的挂载点,以及必需的设备节点。

然后,根据提供的参数建立适当的设备节点并加载适当的内核模块,启动适当的进程(udevd)帮助我们完成这一步骤。

最后,在做完了这些乱七八糟的为挂载根目录及运行 /sbin/init 进程作准备的事情之后,调用 run-init 来运行 /sbin/init 从而启动我们的系统。

精简的 init 脚本

既然我们已经知道了 initrd.img 到底要做什么,我们现在就来一个精简的 init 脚本。

把几乎所有的过程都放到一个脚本当中,仍掉了 nfs 启动的内容,仍掉了从休眠中唤醒的功能,根据需要舍弃了一些文件和文件夹的创建,以及一些变量。这样我们的脚本只有本地启动的内容,结构更加紧凑,操作过程可能会更加清楚。这个也难说,具体和个人有关。不要 udev, 虽然很实用。因为我们下边的这个脚本是个原理性的演示。由于没有了 udev, /sys 目录就没有必要了,同时我们还得自己照顾设备节点。对于我这里的情况,要手动建立 /dev /hda1, /dev/hda2 和 /dev/hda5 这三个设备节点,其中 hda1 是主分区,它挂载到根目录,hda2 是扩展分区,hda5 是 swap。如果设备节点创建少了,启动的时候就会失败。现在我这里的情况是比较简单的,但是如果通过改变启动参数使用移动存储设备启动呢?所以 udev 是一个很有用的东西,同时对于移动的设备你不知道确切的 /dev/sdaxx 这样的形式,UUID 就变得很重要了。

一些表示启动阶段的语句被保留以便和原始的 init 脚本相对照。内容如下:

!/bin/sh

echo "Loading, please wait..."

[ -d /dev ] || mkdir -m 0755 /dev

[ -d /root ] || mkdir --mode=0700 /root

[ -d /proc ] || mkdir /proc

[ -d /tmp ] || mkdir /tmp

mkdir -p /var/lock

mount -t proc none /proc

This should be here, or a fatal error may occur

said no modules.dep found

depmod -a

Note that this only becomes /dev on the real filesystem if udev's scripts

are used; which they will be, but it's worth pointing out

tmpfs_size="10M"

mount -t tmpfs -o size=$tmpfs_size,mode=0755 udev /dev

mknod /dev/console c 5 1

mknod /dev/null c 1 3

mknod /dev/hda b 3 0

mknod /dev/hda1 b 3 1 root

mknod /dev/hda5 b 3 5 swap

Export it for root hardcoding

export ROOT=

Only maybe_break, log_begin_msg and log_end_msg are needed

. /scripts/functions

Export relevant variables

export break=

export init=/sbin/init

export quiet=n

export readonly=y

export rootmnt=/root

export ROOTDELAY=

export panic=

Parse command line options

for x in $(cat /proc/cmdline); do

case $x in

init=*)

init=${xinit=}

;;

root=*)

ROOT=${xroot=}

case $ROOT in

LABEL=*)

ROOT="/dev/disk/by-label/${ROOTLABEL=}"

;;

UUID=*)

ROOT="/dev/disk/by-uuid/${ROOTUU

;;

esac

;;

panic=*)

panic="${xpanic=}"

;;

ro)

readonly=y

;;

rw)

readonly=n

;;

break=*)

break=${xbreak=}

;;

break)

break=premount

;;

esac

done

maybe_break top

nothing to be done for top

maybe_break modules

log_begin_msg "Loading essential drivers..."

modprobe -q unix

log_end_msg

maybe_break premount

log_begin_msg "Running /scripts/init-premount"

thermal modules, FOR x86 ONLY

If commented here, they will still be loaded when running /sbin/init.

I think it should be safe here to comment them off.

Think about the situation: no system installed,

when powered on, nothing bad should happen.

FIXME: Am I right?

modprobe -q fan

modprobe -q thermal

no udev invoked here

log_end_msg

maybe_break mount

log_begin_msg "Mounting root file system..."

Get the root filesystem type

fstype should be enough for detecting filesystem type

eval $(fstype < ${ROOT})

if [ "$FSTYPE" = "unknown" ] && [ -x /lib/udev/vol_id ]; then

FSTYPE=$(/lib/udev/vol_id -t ${ROOT})

[ -z "$FSTYPE" ] && FSTYPE="unknown"

fi

if [ ${readonly} = y ]; then

roflag=-r

else

roflag=-w

fi

FIXME This has no error checking

modprobe -q ${FSTYPE}

FIXME This has no error checking

Mount root

mount ${roflag} -t ${FSTYPE} ${ROOT} ${rootmnt}

log_end_msg

maybe_break bottom

log_begin_msg "Running /scripts/init-bottom"

if [ -z "$no_static_dev" ]; then

mkdir -m 0700 -p /dev/.static/

mkdir /dev/.static/dev/

mount -n -o bind $rootmnt/dev /dev/.static/dev

fi

Now move it all to the real filesystem

mount -n -o move /dev $rootmnt/dev

create a temporary symlink to the final /dev for other initramfs scripts

nuke /dev

ln -s $rootmnt/dev /dev

log_end_msg

Move virtual filesystems over to the real filesystem

mount -n -o move /sys ${rootmnt}/sys

mount -n -o move /proc ${rootmnt}/proc

while [ ! -x ${rootmnt}${init} ]; do

panic "Target filesystem doesn't have ${init}"

done

Confuses /etc/init.d/rc

if [ -n ${debug} ]; then

unset debug

fi

Chain to real filesystem

maybe_break init

exec run-init ${rootmnt} ${init} "$@" <${rootmnt}/dev/console >${rootmnt}/dev/console

现在让我们用上面这个 init 脚本替换原有的脚本(不要忘了作一个备份),打包之后在 qemu 中运行,命令如下:

song@ubuntu:/home/linux_src/initrd/debian_etch$ qemu -hda /media/hda5/os/Debian_etch/disk0 -kernel vmlinuz-2.6.18-4-686 -initrd initrd.img.gz -append "root=/dev/hda1 ro" -m 96 -boot c

太好了,一切正常! Cheers!!

好了,现在这篇文章已经结束了,希望对你有所帮助 :)

参考文献:

[1] /linux/source/directory/Documentation/initrd.txt

内容有点老了,但还是有助于理解 initrd

[2] Linux2.6 内核的 Initrd 机制解析, http://www.ibm.com/developerworks/cn/linux/l-k26initrd/index.html

从内核源代码的层面阐述了 linux 2.6 内核的 initrd 机制

[3] Almesberger, Werner; "Booting Linux: The History and the Future", http://www.almesberger.net/cv/papers/ols2k-9.ps.gz

从技术层面上阐述了 linux 启动的历史及发展趋势

[4] 各种各样的 man 手册

[5] /usr/share/doc 下的各种文档

[6] 网上五花八门的文章