使用isolinux制作Linux启动光盘-
任何一个操作系统在任何一个硬件平台上的运行都需要一个引导的过程,即,初始化软件环境、把内核从存储介质放到内存当中去,并开始运行。当然对于某些简单软硬件系统,这个过程可能及其简单,而对于 PC 就要略微复杂一些了。
PC 的引导程序上承 BIOS,下接内核的初始化代码,虽然开一次机只运行一次后就不留痕迹了,不过还是相当重要的。所有的引导程序都在做类似的事情:
驻留在存贮介质的特殊位置可以被 BIOS 启动,或是自己是某一系统的可执行文件,可以被用户显式或隐式在该系统(宿主系统)内启动;
了解要被启动的必要启动文件的位置,包括系统内核、ramdisk 等,并把它们读取出来、装载到内存之中;
构造环境、运行操作系统的内核,自己则就此退出历史舞台。
历史上,用于 Linux 的最著名的引导程序莫过于 LILO 和 Grub 了,作为通用的引导程序,二者用途广泛,但对于一些特殊的场合,譬如引导程序可利用的空间比较有限的可移动存储介质 (通俗地说,包括光盘、软盘、u 盘等),它们有些过于厚重了,这就引出了我们今天的主角SYSLINUX/ISOLINUX。
SYSLINUX/ISOLINUX 是专门用来引导可移动介质的轻量级引导程序,因为这样的介质通常不会固定只针对一种硬件。我们主要介绍以下ISOLINUX 引导安装程序。ISOLINUX其实是一个简单的Linux系统。其构造很简单。主要包括以下几个方面的内容:
引导程序isolinux.bin
这个文件是ISOLINUX的引导文件。相当于Linux系统中的grub程序一样,在系统启动时,先加载isolinux.bin来启动系统,当isolinux.bin启动以后,会根据下面的配置文件isolinux.cfg来选择不同的启动选项来启动系统。
这个文件是一个二进制文件,在编译isolinux时可以得到,在这里不做过多讲述。
配置引导项文件isolinux.cfg
这个文件是ISOLINUX启动的配置文件,有了这个文件,引导程序isolinux.bin在引导时才会根据该配置文件的配置内容的不同,而选择不同的引导项来启动系统。
isolinux.cfg中的配置项有很多,用户可以根据自己的需求来选择性的加入跟自己相关的配置项即可。但是下面的这些配置项是必须要有的:
default linux指定 label 是 linux 的启动选项为缺省,当然也可以是别的。
label linux
kernel vmlinuz
append initrd=initrd26.gz ramdisk_size=1000000 vga=791
这就是一个启动描述项,前面的 label 是指Linux系统启动时的引导选项。相当于grub中的title。kernel制定了启动时的内核。initrd= 指定 initrd 的文件和 ramdisk_size= 指定 initrd 的尺寸上限。其余的内核参数还可能有很多。其实Linux内核中启动的所有参数,在这里都是可以加入的。
prompt=1这是说,向用户提示输入选择,直接回车就是缺省选项了。当然,如果使其等于0则,不向用户提示输入选择。
timeout=0没有时间限制,当然也可以指定一定时间之后自动进入缺省选项。这个时间是秒数的10倍。例如,如果要等待30秒进入,则应该在这里输入timeout=300。
这些是系统引导时的必有选项,当然,有些选项是可以没有的。下面的这些选项可以没有。
display xxx.txt这指定了一个文件名,会在启动的时候显示的内容,该文件甚至可以包含一个 RLE 编码的图形文件,也就是大家在安装光盘启动时看到的那个;不过这个字段不甚重要,我们就略过了。
gfxboot bootlogo 这指定了启动时的图形界面。一般的Linux系统安装盘中都会加入此项,但是在一些特殊需求下,是不需要用图形界面的,而需要字符模式。具体如何制作图形启动模式,如何制作字符启动模式,需要根据选择的内核选项以及设置选项有关系。这将在下面进行详细介绍。
include ×××这是引入一个已经写好的配置选项文件到配置文件中。这在执行时,会将引入的文件中的全部内容给添加到此文件中,形成一个零时的配置文件来启动系统。
基本的配置项就这么多,当然还有很多的配置项,还是需要用户去参考相关的权威手册来一一了解。
系统启动内核程序
ISOLINUX系统在使用isolinux.bin文件引导完成以后,就会调用一个启动内核来启动一个简单的Linux系统。实际上无论是安装,还是修复Linux系统都需要一个简单的Linux系统来调用相应的程序来完成。在启动盘中使用的Linux内核程序跟普通的Linux系统内核是完全一致的,这里比较特殊的是其initrd镜像文件。该文件实际就是一个最小化的Linux系统。里面包含了shell,mount,fdisk之外,主要要包含Linux系统下各种常用的基本驱动。尤其是硬盘驱动,键盘鼠标驱动。如果没有这些驱动,那么系统将无法找到硬盘,导致系统无法正常启动。
initrd文件特殊,就特殊在该文件中不仅要包含上述的这些文件,还需要包含一些跟该光盘功能相关的文件。例如,如果要进行安装,那么简单的格式化命令也必须要有的。除了这些,为了让制作的iso文件被大部分PC 所使用,所以必须要包含各种驱动在里面。
initrd文件很好制作,可以将Linux系统启动时的initrd文件作为一个基本文件,在里面修改即可。如果有需要添加的内容,直接将linux系统中的相应文件拷贝进去就可以了。另外,initrd下面的启动脚本是init文件,建议根据自己的需求修改该文件,该文件是一个用shell写的脚本。在Linux系统启动时,加载完成内核以后,就开始调用该脚本了,所以有什么需要启动的,都可以在该脚本中添加。甚至可以将该脚本作为一个自己安装,修复等的基本脚本来做。但是建议不要如此,因为这样做会不易调试。建议将系统启动相关的内容放置在这里执行,而将自己的脚本放置在可执行目录下[bin/sbin等],在init脚本中调用该脚本再执行。
举例说明
有了上面的这几步,基本上就对ISOLinux了解了。接下来的工作就是要靠自己的本事和自己的需求来调整initrd,以及iso目录下的内容了。
我在这里主要介绍一下,几种启动界面的制作:
字符模式启动界面
字符模式的启动界面,使用的是menu.c32内核做为启动内核。menu.c32文件由ISOLINUX包提供。可以直接从ISOLINUX包中编译产生。
有了该文件,我们只需要对isolinux.cfg文件进行修改一下即可。具体的修改可以参照下面的配置项
default menu.c32
prompt 0
menu title My Distro Installer
timeout 600
f1 help.txt
f2 version.txt
label bls
menu label Normal install
menu default
kernel vmlinuz
append initrd=cpio.gz rdinit=/init
label bad
menu label Bad hardware install
kernel vmlinuz
append initrd=cpio2.gz badhardware rdinit=/init
可以看的出来,这里的主要调整是,调整default选项,修改其为menu.c32文件。因为如此是指定,默认使用menu.c32引导。接下来就是几个menu选项的加入。这几个选项的主要目的是设置启动的选择项。在命名时建议能够设置成容易识别的名称。
另外,注意menu default选项是指定,默认从那项启动。
当然,使用menu时,还可以加入下面的一些参数来设置选项窗口的宽高比:
MENU WIDTH 80 /*设置宽度*/
MENU MARGIN 10
MENU ROWS 12 /*设置行数*/
MENU TABMSGROW 18
MENU CMDLINEROW 12
MENU ENDROW 24
MENU TIMEOUTROW 20
这几个选项可以添加,也可以不添加,可以均添加,也可以一个都不添加。设置很方便。
此种启动,都是字符模式,而且是用ascii码绘制出来的。其优点是占用内存小,启动快。缺点是界面单调。
2. 使用vesamenu制作启动界面
使用vesamenu启动的方法与使用menu的使用方法基本是一致的。所不同的是default的引导项不一样,此种模式下,default的启动项要设置成vesamenu。
另外,vesamenu的默认背景色是灰色,如果想更换背景图片,可以加入MENU BACKGROUND os102.png来更换背景图片。但是值得注意的是背景图片不能够制作的过于绚丽,因为该图片如果比较绚丽,则无法被正常加载。
这种方法的有点在于启动快,而且可以制作一个图形启动界面。缺点是无法制作一个比较绚丽的启动界面。
3. 使用bootlogo文件制作启动界面
使用bootlogo制作启动界面的方法是,先制作一个比较绚丽的bootlogo文件。这个文件是一个加入图片的二进制文件,具体如何制作,还需要高手能够帮忙指点一下,小弟还不是很清楚。另外,只需要在isolinux.cfg文件中加入gfxboot bootlogo选项即可。
制作ISO镜像文件
配置文件写完了,现在进入实质阶段。
在准备制作ISO的目录里添加一个子目录,比如boot/isolinux/,然后放入 isolinux.cfg和一个对所有光盘都一样的isolinux提供的引导介质 isolinux.bin,当然还要放入相应的kernel,initrd等我们需要在引导时调用到的文件,然后制作iso的时候要使用-b参数,来指明要使用isolinux.bin文件启动:
mkisofs -o output.iso \
-b boot/isolinux/isolinux.bin -c boot/isolinux/boot.cat \
-no-emul-boot -boot-load-size 4 -boot-info-table \
for-iso-dir/
最后的参数就是指定的光盘的目录了,-c参数的那个文件是自动生成的,不用太担心,其余参数都是固定的。事实上,也常常有人用isolinux/而不是 boot/isolinux/,这都是约定俗成的,你完全可以用自己的。这里的路径都是相对于光盘的根的,而和制作光盘时的工作目录没有关系。
至此,整个系统的启动和制作过程就已经完成了。可以说有了这些知识,就可以设计一个简单的启动光盘,至于光盘的功能,需要用户根据自己的需求来修改和调整!
Linux 的 initrd 技术是一个非常普遍使用的机制,linux2.6 内核的 initrd 的文件格式由原来的文件系统镜像文件转变成了 cpio 格式,变化不仅反映在文件格式上, linux 内核对这两种格式的 initrd 的处理有着截然的不同。本文首先介绍了什么是 initrd 技术,然后分别介绍了 Linux2.4 内核和 2.6 内核的 initrd 的处理流程。最后通过对 Linux2.6 内核的 initrd 处理部分代码的分析,使读者可以对 initrd 技术有一个全面的认识。为了更好的阅读本文,要求读者对 Linux 的 VFS 以及 initrd 有一个初步的了解。
1.什么是 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。
2.Linux2.4内核对 Initrd 的处理流程
为了使读者清晰的了解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 处理流程的一个显著区别。
3.Linux2.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 的处理流程。
cpio-initrd 的处理流程
1. boot loader 把内核以及 initrd 文件加载到内存的特定位置。
2. 内核判断initrd的文件格式,如果是cpio格式。
3. 将initrd的内容释放到rootfs中。
4. 执行initrd中的/init文件,执行到这一点,内核的工作全部结束,完全交给/init文件处理。
image-initrd的处理流程
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的制作方法更加简单
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的处理,对上面命令不理解的读者可以参考相关文档。
cpio-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处理流程示意图
cpio-initrd的职责更加重要
如图1所示,cpio-initrd不再象image-initrd那样作为linux内核启动的一个中间步骤,而是作为内核启动的终点,内核将控制权交给cpio-initrd的/init文件后,内核的任务就结束了,所以在/init文件中,我们可以做更多的工作,而不比担心同内核后续处理的衔接问题。当然目前linux发行版的cpio-initrd的/init文件的内容还没有本质的改变,但是相信initrd职责的增加一定是一个趋势。
5.linux2.6内核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的处理相关部分函数调用层次如下图,笔者按照这个层次对每一个函数都给予了比较详细的分析,为了更好的说明,下面列出的代码中删除了同本文主题不相关的部分:
图2 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的处理
对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的内存盘释放。
到此代码分析完毕。
6.结束语
通过本文前半部分对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的参考资料:
[5] http://www.die.net/doc/linux/man/man4/initrd.4.html
引导 Linux® 系统的过程包括很多阶段。不管您是引导一个标准的 x86 桌面系统,还是引导一台嵌入式的 PowerPC® 机器,很多流程都惊人地相似。本文将探索 Linux 的引导过程,从最初的引导到启动第一个用户空间应用程序。在本文介绍的过程中,您将学习到各种与引导有关的主题,例如引导加载程序、内核解压、初始 RAM 磁盘以及 Linux 引导的其他一些元素。
早期时,启动一台计算机意味着要给计算机喂一条包含引导程序的纸带,或者手工使用前端面板地址/数据/控制开关来加载引导程序。尽管目前的计算机已经装备了很多工具来简化引导过程,但是这一切并没有对整个过程进行必要的简化。
让我们先从高级的视角来查看 Linux 引导过程,这样就可以看到整个过程的全貌了。然后将回顾一下在各个步骤到底发生了什么。在整个过程中,参考一下内核源代码可以帮助我们更好地了解内核源代码树,并在以后对其进行深入分析。
概述
图 1 是我们在 20,000 英尺的高度看到的视图。
图 1. Linux 引导过程在 20,000 英尺处的视图
当系统首次引导时,或系统被重置时,处理器会执行一个位于已知位置处的代码。在个人计算机(PC)中,这个位置在基本输入/输出系统(BIOS) 中,它保存在主板上的闪存中。嵌入式系统中的中央处理单元(CPU)会调用这个重置向量来启动一个位于闪存/ROM 中的已知地址处的程序。在这两种情况下,结果都是相同的。因为 PC 提供了很多灵活性,BIOS 必须确定要使用哪个设备来引导系统。稍后我们将详细介绍这个过程。
当找到一个引导设备之后,第一阶段的引导加载程序就被装入 RAM 并执行。这个引导加载程序在大小上小于 512 字节(一个扇区),其作用是加载第二阶段的引导加载程序。
当第二阶段的引导加载程序被装入 RAM 并执行时,通常会显示一个动画屏幕,并将 Linux 和一个可选的初始 RAM 磁盘(临时根文件系统)加载到内存中。在加载映像时,第二阶段的引导加载程序就会将控制权交给内核映像,然后内核就可以进行解压和初始化了。在这个阶段 中,第二阶段的引导加载程序会检测系统硬件、枚举系统链接的硬件设备、挂载根设备,然后加载必要的内核模块。完成这些操作之后启动第一个用户空间程序(init ),并执行高级系统初始化工作。
这就是 Linux 引导的整个过程。现在让我们深入挖掘一下这个过程,并深入研究一下 Linux 引导过程的一些详细信息。
系统启动
系统启动阶段依赖于引导 Linux 系统上的硬件。在嵌入式平台中,当系统加电或重置时,会使用一个启动环境。这方面的例子包括 U-Boot、RedBoot 和 Lucent 的 MicroMonitor。嵌入式平台通常都是与引导监视器搭配销售的。这些程序位于目标硬件上的闪存中的某一段特殊区域,它们提供了将 Linux 内核映像下载到闪存并继续执行的方法。除了可以存储并引导 Linux 映像之外,这些引导监视器还执行一定级别的系统测试和硬件初始化过程。在嵌入式平台中,这些引导监视器通常会涉及第一阶段和第二阶段的引导加载程序。
|
在 PC 中,引导 Linux 是从 BIOS 中的地址 0xFFFF0 处开始的。BIOS 的第一个步骤是加电自检(POST)。POST 的工作是对硬件进行检测。BIOS 的第二个步骤是进行本地设备的枚举和初始化。
给定 BIOS 功能的不同用法之后,BIOS 由两部分组成:POST 代码和运行时服务。当 POST 完成之后,它被从内存中清理了出来,但是 BIOS 运行时服务依然保留在内存中,目标操作系统可以使用这些服务。
要引导一个操作系统,BIOS 运行时会按照 CMOS 的设置定义的顺序来搜索处于活动状态并且可以引导的设备。引导设备可以是软盘、CD-ROM、硬盘上的某个分区、网络上的某个设备,甚至是 USB 闪存。
通常,Linux 都是从硬盘上引导的,其中主引导记录(MBR)中包含主引导加载程序。MBR 是一个 512 字节大小的扇区,位于磁盘上的第一个扇区中(0 道 0 柱面 1 扇区)。当 MBR 被加载到 RAM 中之后,BIOS 就会将控制权交给 MBR。
第一阶段引导加载程序
MBR 中的主引导加载程序是一个 512 字节大小的映像,其中包含程序代码和一个小分区表(参见图 2)。前 446 个字节是主引导加载程序,其中包含可执行代码和错误消息文本。接下来的 64 个字节是分区表,其中包含 4 个分区的记录(每个记录的大小是 16 个字节)。MBR 以两个特殊数字的字节(0xAA55)结束。这个数字会用来进行 MBR 的有效性检查。
图 2. MBR 剖析
主引导加载程序的工作是查找并加载次引导加载程序(第二阶段)。它是通过在分区表中查找一个活动分区来实现这种功能的。当找到一个活动分区时,它会 扫描分区表中的其他分区,以确保它们都不是活动的。当这个过程验证完成之后,就将活动分区的引导记录从这个设备中读入 RAM 中并执行它。
第二阶段引导加载程序
次引导加载程序(第二阶段引导加载程序)可以更形象地称为内核加载程序。这个阶段的任务是加载 Linux 内核和可选的初始 RAM 磁盘。
|
在 x86 PC 环境中,第一阶段和第二阶段的引导加载程序一起称为 Linux Loader(LILO)或 GRand Unified Bootloader(GRUB)。由于 LILO 有一些缺点,而 GRUB 克服了这些缺点,因此下面让我们就来看一下 GRUB。(有关 GRUB、LILO 和相关主题的更多内容,请参阅本文后面的 参考资料 部分的内容。)
关于 GRUB,很好的一件事情是它包含了有关 Linux 文件系统的知识。GRUB 不像 LILO 一样使用裸扇区,而是可以从 ext2 或 ext3 文件系统中加载 Linux 内核。它是通过将两阶段的引导加载程序转换成三阶段的引导加载程序来实现这项功能的。阶段 1 (MBR)引导了一个阶段 1.5 的引导加载程序,它可以理解包含 Linux 内核映像的特殊文件系统。这方面的例子包括 reiserfs_stage1_5 (要从 Reiser 日志文件系统上进行加载)或 e2fs_stage1_5 (要从 ext2 或 ext3 文件系统上进行加载)。当阶段 1.5 的引导加载程序被加载并运行时,阶段 2 的引导加载程序就可以进行加载了。
当阶段 2 加载之后,GRUB 就可以在请求时显示可用内核列表(在 /etc/grub.conf 中进行定义,同时还有几个软符号链接 /etc/grub/menu.lst 和 /etc/grub.conf )。我们可以选择内核甚至修改附加内核参数。另外,我们也可以使用一个命令行的 shell 对引导过程进行高级手工控制。
将第二阶段的引导加载程序加载到内存中之后,就可以对文件系统进行查询了,并将默认的内核映像和 initrd 映像加载到内存中。当这些映像文件准备好之后,阶段 2 的引导加载程序就可以调用内核映像了。
内核
|
当内核映像被加载到内存中,并且阶段 2 的引导加载程序释放控制权之后,内核阶段就开始了。内核映像并不是一个可执行的内核,而是一个压缩过的内核映像。通常它是一个 zImage(压缩映像,小于 512KB)或一个 bzImage(较大的压缩映像,大于 512KB),它是提前使用 zlib 进行压缩过的。在这个内核映像前面是一个例程,它实现少量硬件设置,并对内核映像中包含的内核进行解压,然后将其放入高端内存中,如果有初始 RAM 磁盘映像,就会将它移动到内存中,并标明以后使用。然后该例程会调用内核,并开始启动内核引导的过程。
当 bzImage(用于 i386 映像)被调用时,我们从 ./arch/i386/boot/head.S 的 start 汇编例程开始执行(主要流程图请参看图 3)。这个例程会执行一些基本的硬件设置,并调用 ./arch/i386/boot/compressed/head.S 中的 startup_32 例程。此例程会设置一个基本的环境(堆栈等),并清除 Block Started by Symbol(BSS)。然后调用一个叫做 decompress_kernel 的 C 函数(在 ./arch/i386/boot/compressed/misc.c 中)来解压内核。当内核被解压到内存中之后,就可以调用它了。这是另外一个 startup_32 函数,但是这个函数在 ./arch/i386/kernel/head.S 中。
在这个新的 startup_32 函数(也称为清除程序或进程 0)中,会对页表进行初始化,并启用内存分页功能。然后会为任何可选的浮点单元(FPU)检测 CPU 的类型,并将其存储起来供以后使用。然后调用 start_kernel 函数(在 init/main.c 中),它会将您带入与体系结构无关的 Linux 内核部分。实际上,这就是 Linux 内核的 main 函数。
图 3. Linux 内核 i386 引导的主要函数流程
通过调用 start_kernel ,会调用一系列初始化函数来设置中断,执行进一步的内存配置,并加载初始 RAM 磁盘。最后,要调用 kernel_thread (在 arch/i386/kernel/process.c 中)来启动 init 函数,这是第一个用户空间进程(user-space process)。最后,启动空任务,现在调度器就可以接管控制权了(在调用 cpu_idle 之后)。通过启用中断,抢占式的调度器就可以周期性地接管控制权,从而提供多任务处理能力。
在内核引导过程中,初始 RAM 磁盘(initrd )是由阶段 2 引导加载程序加载到内存中的,它会被复制到 RAM 中并挂载到系统上。这个 initrd 会作为 RAM 中的临时根文件系统使用,并允许内核在没有挂载任何物理磁盘的情况下完整地实现引导。由于与外围设备进行交互所需要的模块可能是 initrd 的一部分,因此内核可以非常小,但是仍然需要支持大量可能的硬件配置。在内核引导之后,就可以正式装备根文件系统了(通过 pivot_root ):此时会将 initrd 根文件系统卸载掉,并挂载真正的根文件系统。
|
initrd 函数让我们可以创建一个小型的 Linux 内核,其中包括作为可加载模块编译的驱动程序。这些可加载的模块为内核提供了访问磁盘和磁盘上的文件系统的方法,并为其他硬件提供了驱动程序。由于根文件系统是磁盘上的一个文件系统 ,因此 initrd 函数会提供一种启动方法来获得对磁盘的访问,并挂载真正的根文件系统。在一个没有硬盘的嵌入式环境中,initrd 可以是最终的根文件系统,或者也可以通过网络文件系统(NFS)来挂载最终的根文件系统。
Init
当内核被引导并进行初始化之后,内核就可以启动自己的第一个用户空间应用程序了。这是第一个调用的使用标准 C 库编译的程序。在此之前,还没有执行任何标准的 C 应用程序。
在桌面 Linux 系统上,第一个启动的程序通常是 /sbin/init 。但是这不是一定的。很少有嵌入式系统会需要使用 init所提供的丰富初始化功能(这是通过 /etc/inittab 进行配置的)。在很多情况下,我们可以调用一个简单的 shell 脚本来启动必需的嵌入式应用程序。
结束语
与 Linux 本身非常类似,Linux 的引导过程也非常灵活,可以支持众多的处理器和硬件平台。最初,加载引导加载程序提供了一种简单的方法,不用任何花架子就可以引导 Linux。LILO 引导加载程序对引导能力进行了扩充,但是它却缺少文件系统的感知能力。最新一代的引导加载程序,例如 GRUB,允许 Linux 从一些文件系统(从 Minix 到 Reise)上进行引导。
1. 制作一个linux系统内核,这个不是我想讨论的话题,制作linux内核的文章网络上随处可以找到,比如我们准备好了一个bzImage
2. 准备一个自己需要使用的文件系统,制作文件系统的过程也不是本文想要讨论的,假设我们准备好了一个文件系统COLOR.gz
找到我们需要做引导的工具isolinux,具体说就是isolinux.bin isolinux.cfg,俺是从RH10的iso映像中抠出来的。:)
准备工作结束。
为自己的iso映像建立一个目录,作为iso的根目录(也就是光盘的根了),比如myiso/
在建立好的目录下,建立一个isolinux目录(这个名称好像是固定的,而其必须有,俺有一次没有建立这个目录,结果引导的系统不能正常检测中断,不知道为什么,郁闷了一晚上)
随后把isolinux.cfg和isolinux.bin放到isolinux目录下。
在iso的根目录下,放置光盘所包含的内容,当然要包含bzImage和COLOR.gz了。
剩下的就是你想要放什么都可以了,呵呵
这时候我们的目录结构就建立完成了。如下:
myiso
|
|---isolinux
| |
| |---isolinux.bin
| |__isolinux.cfg
|---bzImage
|---COLOR.gz
|---readme
。
配置isolinux.cfg
isolinux.cfg的格式和lilo.conf grub.conf相似:
prompt 1
timeout 100
default myiso
label mysio
kernel /bzImage
append initrd=/COLOR.gz load_ramdisk=1 prompt_ramdisk=0 ramdisk_size=60000 rw root=/dev/ram
不想等待的话可以去掉prompt和timeout,要不然启动的时候还要点一下,麻烦。
一切都做好了,下边我们就可以生成自己的iso映像了。
在linux使用命令mkisofs命令:
mkisofs -o myiso.iso -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table myiso
具体的参数使用大家可以man一下。
以上步骤在linux2.4下验证成功。
如果我们的COLOR.gz能够做得很小,和bzImage一起可以放到软盘上,我们就可以使用比较常见的syslinux来做引导光盘了.syslinux的使用网上很多,大家有用的时候可以看看。
isolinux的使用可以看看:
http://syslinux.zytor.com/iso.php