最近在自学 Linux kernel 方面的东西,这两天了粗浅的研究了下 kernel boot 过程,在此记录。这里所指 Linux 引导加载未涉及虚拟化环境,即系统未运行在 hypervisor 之上。
Linux 通过执行不同阶段的引导加载程序(boot loader)程序来引导操作系统,在完成内核等引导之后,最终会由调度器接管 CPU,其通过启用中断来周期性的抢占控制权,处理多个用户进程/客户进程(kvm 虚拟化)。Top level 的引导过程如下图。
整个 Linux 系统引导共分 5 步执行操作:
- BIOS/BootMonitor 引导程序;BIOS 包括 POST 和 Runtime 服务。
- 被称为第一阶段的 MBR (Master boot record)引导程序;位于 BIOS 配置的启动磁盘 0 柱面 1 扇区的主引导记录,用于启动第二阶段的 linux boot loader。
- 被称为第二阶段的 linux boot loader;主要有 LILO (Linux loader)和 GNU GRUB (Grand unified boot loader)两种 boot loader 程序,现主流为 GRUB。包括了通过 initrd 来创建 RAM 盘,执行 init 脚本,通过 LKM (linux kernel module)加载本地磁盘等驱动程序来挂载磁盘中的 root 文件系统。RAM 盘中是个完整的小型 linux 环境,在没有磁盘的嵌入式环境中,initrd 可以是最终的根文件系统,也可以通过 NFS 来挂载最终的文件系统。
- linux kernel (及 initrd 函数)引导;负责加载并解压 zImage/bzImage kernel 及 initrd 映像,并开始执行 kernel 初使化和引导程序/过程。
- init 进程。用于启动 linux 配置的各项用户空间服务(demon)进程。
加电后首先被执行的是 BIOS (Base input/output system)程序。 嵌入式环境使用 boot monitor,它负责在一个位于 rom/flash 中预定地址开始执行引导程序,而在 PC 环境中这个启动地址是 0xFFFF0,相对来讲 BIOS 提供了更多的配置功能。它主要由两部分组成:
- POST (Power On Self Test)程序;其负责接通电源时对硬件检测,包括创建中断向量、设置寄存器、对一些外部设备进行初始化和检测等。
- BIOS Runtime 服务;负责为操作系统提供一些基础服务,主要与IO外设有关。
当 BIOS POST 执行完后,其将会从内存中清理,而 Runtime 服务会常驻内存,为操作系统提供一些底层的支持。最后 BIOS 将控制权交给称为第一阶段引导程序的 MBR (Master boot record)程序。
接下来执行的 MBR 是一个512 byte 固定大小的映像。 包括 446 byte 长的被称为初始程序加裁程序 (Initial program loader, IPL)的可执行代码和 64 byte 分区表(16 byte * 4 个),最后以 0xaa55 特殊字节结束。如下图所示。
MBR 引导程序会将扫描分区表,获得唯一活动分区后,将其中的引导程序读入 RAM 并开始执行。
MBR 启动的引导程序被称为第二阶段引导程序,它是引导的主体,是引导加载的真正部分。 Linux 中该阶段有两个流行的程序,LILO (较老)和 GRUB。如果安装了 lilo 程序,可以通过 root 用户执行如下命令来通过 lilo 生成默认配置的 MBR ,并写入到启动磁盘 0 柱面 1扇区位置上。
- # /sbin/lilo -v -v
# /sbin/lilo -v -v
一般需要修改 lilo 的配置文件,使生成的 MBR 有效。位于 /etc/lilo.conf 。lilo 配置示例。
- boot=/dev/hda
- map=/boot/map
- install=/boot/boot.b
- prompt
- timeout=100
- compact
- default=Linux
- image=/boot/vmlinuz-2.4.18-14
- label=Linux
- root=/dev/hdb3
- read-only
- password=linux
- other=/dev/hda
- label=WindowsXP
boot=/dev/hda map=/boot/map install=/boot/boot.b prompt timeout=100 compact default=Linux image=/boot/vmlinuz-2.4.18-14 label=Linux root=/dev/hdb3 read-only password=linux other=/dev/hda label=WindowsXP
boot 键指定了 lilo 在哪里安装 MBR。可以通过替换 boot=/dev/fd0 配置来指定 lilo 创建有引导记录的软盘。
LILO 天生存在一些缺点和不足,因此 linux 在新版本中引入了 GRUB 程序。 它为了从磁盘来加裁配置和 kernel 映像,不像 LILO 只能从裸扇区中执行引导程序,而具有了访问磁盘文件系统(ext2/3、reiserfs、
jfs、fat 等)的能力。GRUB 是通过引入所谓 1.5 阶段的引导加载程序来实现这项功能的,在该阶段中,GRUB 主要来加载特殊的文件系统驱动。此后,阶段 2 的引导加载程序就可以进行加载了。
一般 GRUB 有一个不错的 GUI 界面,其中通过分析配置文件来显示了一此引导选项。在我的 ubuntu 8.10 系统中,该配置文件位于 /boot/grub/menu.lst。我们可以选择内核甚至修改附加内核参数,甚至可以使用 GRUB shell 对引导过程进行高级手工控制。我的 menu.lst 文件内容如下。
- default 0
- timeout 3
- hiddenmenu
- title Ubuntu 8.10, kernel 2.6.27-11-generic
- uuid e2cf53c5-11de-4d57-a532-878901afd9b4
- kernel /boot/vmlinuz-2.6.27-11-generic root=UUID=e2cf53c5-11de-4d57-a532-878901afd9b4 ro locale=zh_CN quiet splash
- initrd /boot/initrd.img-2.6.27-11-generic
- quiet
- title Ubuntu 8.10, kernel 2.6.27-11-generic (recovery mode)
- uuid e2cf53c5-11de-4d57-a532-878901afd9b4
- kernel /boot/vmlinuz-2.6.27-11-generic root=UUID=e2cf53c5-11de-4d57-a532-878901afd9b4 ro locale=zh_CN single
- initrd /boot/initrd.img-2.6.27-11-generic
- title Ubuntu 8.10, kernel 2.6.27-7-generic
- uuid e2cf53c5-11de-4d57-a532-878901afd9b4
- kernel /boot/vmlinuz-2.6.27-7-generic root=UUID=e2cf53c5-11de-4d57-a532-878901afd9b4 ro locale=zh_CN quiet splash
- initrd /boot/initrd.img-2.6.27-7-generic
- quiet
- title Ubuntu 8.10, kernel 2.6.27-7-generic (recovery mode)
- uuid e2cf53c5-11de-4d57-a532-878901afd9b4
- kernel /boot/vmlinuz-2.6.27-7-generic root=UUID=e2cf53c5-11de-4d57-a532-878901afd9b4 ro locale=zh_CN single
- initrd /boot/initrd.img-2.6.27-7-generic
- title Ubuntu 8.10, memtest86+
- uuid e2cf53c5-11de-4d57-a532-878901afd9b4
- kernel /boot/memtest86+.bin
- quiet
default 0 timeout 3 hiddenmenu title Ubuntu 8.10, kernel 2.6.27-11-generic uuid e2cf53c5-11de-4d57-a532-878901afd9b4 kernel /boot/vmlinuz-2.6.27-11-generic root=UUID=e2cf53c5-11de-4d57-a532-878901afd9b4 ro locale=zh_CN quiet splash initrd /boot/initrd.img-2.6.27-11-generic quiet title Ubuntu 8.10, kernel 2.6.27-11-generic (recovery mode) uuid e2cf53c5-11de-4d57-a532-878901afd9b4 kernel /boot/vmlinuz-2.6.27-11-generic root=UUID=e2cf53c5-11de-4d57-a532-878901afd9b4 ro locale=zh_CN single initrd /boot/initrd.img-2.6.27-11-generic title Ubuntu 8.10, kernel 2.6.27-7-generic uuid e2cf53c5-11de-4d57-a532-878901afd9b4 kernel /boot/vmlinuz-2.6.27-7-generic root=UUID=e2cf53c5-11de-4d57-a532-878901afd9b4 ro locale=zh_CN quiet splash initrd /boot/initrd.img-2.6.27-7-generic quiet title Ubuntu 8.10, kernel 2.6.27-7-generic (recovery mode) uuid e2cf53c5-11de-4d57-a532-878901afd9b4 kernel /boot/vmlinuz-2.6.27-7-generic root=UUID=e2cf53c5-11de-4d57-a532-878901afd9b4 ro locale=zh_CN single initrd /boot/initrd.img-2.6.27-7-generic title Ubuntu 8.10, memtest86+ uuid e2cf53c5-11de-4d57-a532-878901afd9b4 kernel /boot/memtest86+.bin quiet
将第二阶段的引导加载程序加载到内存中之后,就可以对文件系统进行查询了,并将默认的内核映像和 initrd 映像加载到内存中。当这些映像文件准备好之后,阶段 2 的引导加载程序就可以调用内核映像。 正如上面配置文件描述的那样,我的 ubuntu 启动会将加载 /boot/vmlinuz-2.6.27-11-generic (zImage/bzImage 格式的 kernel 映像)和 /boot/initrd.img-2.6.27-11-generic (cpio 格式的 initrd 映像)。
接下来就是 kernel 引导加载过程,这个过程包括如下 6 步。
- 执行一个对硬件做些基本设置的例程;
(,/arch/i386/boot/head.S 中的 start 例程) - 设置一个基本的环境(堆栈等),并清除 Block Started by Symbol(BSS);
(./arch/i386/boot/compressed/head.S 中的 startup_32 例程) - 通过连接在映像中的函数来解压内核;
(./arch/i386/boot/compressed/misc.c 中的 decompress_kernel C 函数) - 启动 swapper (0 进程)进程,初始化页表,启用 CPU 内存分页。然后会为任何可选的浮点单元(FPU)检测 CPU 的类型,并将其存储起来供以后使用;
(./arch/i386/kernel/head.S 中的 startup_32 函数) - 调用 linux kernl main 函数,进入与体系结构无关的 Linux 内核部分。
(init/main.c 中的 start_kernel 函数 )
这会调用一系列初始化函数来设置中断,执行进一步的内存配置,并加载已初始化的 RAM 盘。最后启动 init 进程,这是第一个用户空间进程(user-space process);
(./arch/i386/kernel/process.c 中的 kernel_thread) - 最后,启动空任务。现在调度器就可以接管控制权了(在调用 cpu_idle 之后)。通过启用中断,抢占式的调度器就可以周期性地接管控制权,从而提供多任务处理能力。
上面第 5 步加载的 RAM 盘(initrd)是由阶段 2 引导加载程序加载到内存中的,它用来加载必要的磁盘驱动内核模块,来挂载真正磁盘的 root 文件系统。
引导加载的最后的一步就是执行 init (1 进程),该进程会根据配置来启动服务。 一般的配置都会写在 inittab 里,不过我这里用的 ubuntu 使用的是 upstart,它是基于事件驱动的,发生什么 event 怎么处理,在这里 init 进程会产生 startup event, upstart 据此来启动 rc.* 配置的进程。不过无论如何,此时引导加载程序已经放权了。
这里抄录一段 LILO 与 GURB 的优缺点对比。
- LILO 没有交互式命令界面,而 GRUB 拥有。
- LILO 不支持网络引导,而 GRUB 支持。
- LILO 将关于可以引导的操作系统位置的信息物理上存储在 MBR 中。如果修改了 LILO 配置文件,必须将 LILO 第一阶段引导加载程序重写到 MBR。相对于 GRUB,这是一个更为危险的选择,因为错误配置的 MBR 可能会让系统无法引导。使用 GRUB,如果配置文件配置错误,则只是默认转到 GRUB 命令行界面。
关于 kernel 和 initrd 两个映像。 技术含量很高的,嵌入式开发中 bootloader 可是很大一块。值得深入,只可惜现在的技术水平,哎~
- kernel /boot/vmlinuz-2.6.27-7-generic
- initrd /boot/initrd.img-2.6.27-7-generic
initrd 映像是打包的 RAM 盘根文件系统。 一般 initrd.img-2.6.27-7-generic 是一个 cpio 包文件,老版本也有 gzip 压缩格式的。通过 cpio 命令将其解包到当前目录中,如下。cpio 使用方法可参见 cpio 命令详解 。
- zcat initrd.img-2.6.27-11-generic | cpio -i -d --no-absolute-filenames
zcat initrd.img-2.6.27-11-generic | cpio -i -d --no-absolute-filenames
在我这里解包后的根文件系统包括如下内容。
从上面的 directory tree 可以看到 initrd 中主要包括的就是磁盘、网络、文件系统的驱动 lkm 文件。其中还有最主要是的 init shell 脚本,它包括了初使化的全过程。
- #!/bin/sh
- echo "Loading, please wait..."
- [ -d /dev ] || mkdir -m 0755 /dev
- [ -d /root ] || mkdir -m 0700 /root
- [ -d /sys ] || mkdir /sys
- [ -d /proc ] || mkdir /proc
- [ -d /tmp ] || mkdir /tmp
- mkdir -p /var/lock
- mount -t sysfs -o nodev,noexec,nosuid none /sys
- mount -t proc -o nodev,noexec,nosuid none /proc
- # 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
- mount -t tmpfs -o mode=0755 udev /dev
- [ -e /dev/console ] || mknod -m 0600 /dev/console c 5 1
- [ -e /dev/null ] || mknod /dev/null c 1 3
- > /dev/.initramfs-tools
- mkdir /dev/.initramfs
- # Export the dpkg architecture
- export DPKG_ARCH=
- . /conf/arch.conf
- # Set modprobe env
- export MODPROBE_OPTIONS="-Qb"
- # Export relevant variables
- export ROOT=
- export ROOTDELAY=
- export ROOTFLAGS=
- export ROOTFSTYPE=
- export break=
- export init=/sbin/init
- export quiet=n
- export readonly=y
- export rootmnt=/root
- export debug=
- export panic=
- export blacklist=
- export resume_offset=
- # Bring in the main config
- . /conf/initramfs.conf
- for conf in conf/conf.d/*; do
- [ -f ${conf} ] && . ${conf}
- done
- . /scripts/functions
- # Parse command line options
- for x in $(cat /proc/cmdline); do
- case $x in
- init=*)
- init=${x#init=}
- ;;
- root=*)
- ROOT=${x#root=}
- case $ROOT in
- LABEL=*)
- ROOT="/dev/disk/by-label/${ROOT#LABEL=}"
- ;;
- UUID=*)
- ROOT="/dev/disk/by-uuid/${ROOT#UUID=}"
- ;;
- /dev/nfs)
- [ -z "${BOOT}" ] && BOOT=nfs
- ;;
- esac
- ;;
- rootflags=*)
- ROOTFLAGS="-o ${x#rootflags=}"
- ;;
- rootfstype=*)
- ROOTFSTYPE="${x#rootfstype=}"
- ;;
- rootdelay=*)
- ROOTDELAY="${x#rootdelay=}"
- case ${ROOTDELAY} in
- *[![:digit:].]*)
- ROOTDELAY=
- ;;
- esac
- ;;
- resumedelay=*)
- RESUMEDELAY="${x#resumedelay=}"
- ;;
- loop=*)
- LOOP="${x#loop=}"
- ;;
- loopflags=*)
- LOOPFLAGS="-o ${x#loopflags=}"
- ;;
- loopfstype=*)
- LOOPFSTYPE="${x#loopfstype=}"
- ;;
- cryptopts=*)
- cryptopts="${x#cryptopts=}"
- ;;
- nfsroot=*)
- NFSROOT="${x#nfsroot=}"
- ;;
- netboot=*)
- NETBOOT="${x#netboot=}"
- ;;
- ip=*)
- IPOPTS="${x#ip=}"
- ;;
- boot=*)
- BOOT=${x#boot=}
- ;;
- resume=*)
- RESUME="${x#resume=}"
- ;;
- resume_offset=*)
- resume_offset="${x#resume_offset=}"
- ;;
- noresume)
- noresume=y
- ;;
- panic=*)
- panic="${x#panic=}"
- case ${panic} in
- *[![:digit:].]*)
- panic=
- ;;
- esac
- ;;
- quiet)
- quiet=y
- ;;
- ro)
- readonly=y
- ;;
- rw)
- readonly=n
- ;;
- debug)
- debug=y
- quiet=n
- exec >/tmp/initramfs.debug 2>&1
- set -x
- ;;
- debug=*)
- debug=y
- quiet=n
- set -x
- ;;
- break=*)
- break=${x#break=}
- ;;
- break)
- break=premount
- ;;
- blacklist=*)
- blacklist=${x#blacklist=}
- ;;
- esac
- done
- if [ -z "${noresume}" ]; then
- export resume=${RESUME}
- else
- export noresume
- fi
- depmod -a
- maybe_break top
- # export BOOT variable value for compcache,
- # so we know if we run from casper
- export BOOT
- # Don't do log messages here to avoid confusing usplash
- run_scripts /scripts/init-top
- maybe_break modules
- log_begin_msg "Loading essential drivers..."
- load_modules
- log_end_msg
- maybe_break premount
- [ "$quiet" != "y" ] && log_begin_msg "Running /scripts/init-premount"
- run_scripts /scripts/init-premount
- [ "$quiet" != "y" ] && log_end_msg
- maybe_break mount
- log_begin_msg "Mounting root file system..."
- . /scripts/${BOOT}
- parse_numeric ${ROOT}
- mountroot
- log_end_msg
- maybe_break bottom
- [ "$quiet" != "y" ] && log_begin_msg "Running /scripts/init-bottom"
- run_scripts /scripts/init-bottom
- [ "$quiet" != "y" ] && 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
- # Check init bootarg
- if [ -n "${init}" ] && [ ! -x "${rootmnt}${init}" ]; then
- echo "Target filesystem doesn't have ${init}."
- init=
- fi
- # Search for valid init
- if [ -z "${init}" ] ; then
- for init in /sbin/init /etc/init /bin/init /bin/sh; do
- if [ ! -x "${rootmnt}${init}" ]; then
- continue
- fi
- break
- done
- fi
- # No init on rootmount
- if [ ! -x "${rootmnt}${init}" ]; then
- panic "No init found. Try passing init= bootarg."
- fi
- # 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 2>&1
- panic "Could not execute run-init."
#!/bin/sh echo "Loading, please wait..." [ -d /dev ] || mkdir -m 0755 /dev [ -d /root ] || mkdir -m 0700 /root [ -d /sys ] || mkdir /sys [ -d /proc ] || mkdir /proc [ -d /tmp ] || mkdir /tmp mkdir -p /var/lock mount -t sysfs -o nodev,noexec,nosuid none /sys mount -t proc -o nodev,noexec,nosuid none /proc # 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 mount -t tmpfs -o mode=0755 udev /dev [ -e /dev/console ] || mknod -m 0600 /dev/console c 5 1 [ -e /dev/null ] || mknod /dev/null c 1 3 > /dev/.initramfs-tools mkdir /dev/.initramfs # Export the dpkg architecture export DPKG_ARCH= . /conf/arch.conf # Set modprobe env export MODPROBE_OPTIONS="-Qb" # Export relevant variables export ROOT= export ROOTDELAY= export ROOTFLAGS= export ROOTFSTYPE= export break= export init=/sbin/init export quiet=n export readonly=y export rootmnt=/root export debug= export panic= export blacklist= export resume_offset= # Bring in the main config . /conf/initramfs.conf for conf in conf/conf.d/*; do [ -f ${conf} ] && . ${conf} done . /scripts/functions # Parse command line options for x in $(cat /proc/cmdline); do case $x in init=*) init=${x#init=} ;; root=*) ROOT=${x#root=} case $ROOT in LABEL=*) ROOT="/dev/disk/by-label/${ROOT#LABEL=}" ;; UUID=*) ROOT="/dev/disk/by-uuid/${ROOT#UUID=}" ;; /dev/nfs) [ -z "${BOOT}" ] && BOOT=nfs ;; esac ;; rootflags=*) ROOTFLAGS="-o ${x#rootflags=}" ;; rootfstype=*) ROOTFSTYPE="${x#rootfstype=}" ;; rootdelay=*) ROOTDELAY="${x#rootdelay=}" case ${ROOTDELAY} in *[![:digit:].]*) ROOTDELAY= ;; esac ;; resumedelay=*) RESUMEDELAY="${x#resumedelay=}" ;; loop=*) LOOP="${x#loop=}" ;; loopflags=*) LOOPFLAGS="-o ${x#loopflags=}" ;; loopfstype=*) LOOPFSTYPE="${x#loopfstype=}" ;; cryptopts=*) cryptopts="${x#cryptopts=}" ;; nfsroot=*) NFSROOT="${x#nfsroot=}" ;; netboot=*) NETBOOT="${x#netboot=}" ;; ip=*) IPOPTS="${x#ip=}" ;; boot=*) BOOT=${x#boot=} ;; resume=*) RESUME="${x#resume=}" ;; resume_offset=*) resume_offset="${x#resume_offset=}" ;; noresume) noresume=y ;; panic=*) panic="${x#panic=}" case ${panic} in *[![:digit:].]*) panic= ;; esac ;; quiet) quiet=y ;; ro) readonly=y ;; rw) readonly=n ;; debug) debug=y quiet=n exec >/tmp/initramfs.debug 2>&1 set -x ;; debug=*) debug=y quiet=n set -x ;; break=*) break=${x#break=} ;; break) break=premount ;; blacklist=*) blacklist=${x#blacklist=} ;; esac done if [ -z "${noresume}" ]; then export resume=${RESUME} else export noresume fi depmod -a maybe_break top # export BOOT variable value for compcache, # so we know if we run from casper export BOOT # Don't do log messages here to avoid confusing usplash run_scripts /scripts/init-top maybe_break modules log_begin_msg "Loading essential drivers..." load_modules log_end_msg maybe_break premount [ "$quiet" != "y" ] && log_begin_msg "Running /scripts/init-premount" run_scripts /scripts/init-premount [ "$quiet" != "y" ] && log_end_msg maybe_break mount log_begin_msg "Mounting root file system..." . /scripts/${BOOT} parse_numeric ${ROOT} mountroot log_end_msg maybe_break bottom [ "$quiet" != "y" ] && log_begin_msg "Running /scripts/init-bottom" run_scripts /scripts/init-bottom [ "$quiet" != "y" ] && 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 # Check init bootarg if [ -n "${init}" ] && [ ! -x "${rootmnt}${init}" ]; then echo "Target filesystem doesn't have ${init}." init= fi # Search for valid init if [ -z "${init}" ] ; then for init in /sbin/init /etc/init /bin/init /bin/sh; do if [ ! -x "${rootmnt}${init}" ]; then continue fi break done fi # No init on rootmount if [ ! -x "${rootmnt}${init}" ]; then panic "No init found. Try passing init= bootarg." fi # 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 2>&1 panic "Could not execute run-init."
Kernel 映像与 initrd 不同,它是个 zImage/bzImage 文件。 通过 linux 编译脚本可以确认 zImage 实际上就是是由一个压缩后的内核(piggy.o),连接上一段初始化及解压功能的代码(head.o、misc.o)组成的。前面 kernel 引导加载过程中的硬件基本设置、设置基本环境(堆栈等)并清除BSS,直至解压内核都是 kernel 映像中压缩内核所连接的代码完成的。关于 kernel 映像这块还在研究、学习中。
学习的资料有如下文档,但不限于此。
- Linux 引导过程内幕 。全面的讲解了 linux 系统引导过程。
- 引导加载程序之争:了解 LILO 和 GRUB 。其给出了 LILO 详细介绍和配置方法。
- zImage内核镜像解压过程详解 。从内核开发角度深入介绍了 zImage 内核映像。
- Linux 内核映象文件解密 。简要介绍了 initrd 映像。
- Ubuntu upstart 简单说明 。
整个的 Linux kernel 引导过程还在研究、学习中欢迎大家分享、指正。
最后再次对技术前辈的进取和谦虚致敬!