本文发自 http://www.binss.me/blog/boot-process-of-linux/,转载请注明出处。
最近在折腾kernel的过程中频频卡在启动过程的某个阶段,发现自己对Linux的启动过程缺乏了解,因此查阅了一些资料,先以本文作为概述,后续再进行详述。
启动过程
-
通电后,寄存器reset,CPU跳转到BIOS(Basic Input/Output System)代码处开始执行
读取并校验CMOS(可读写RAM芯片,保存计算机基本启动信息,如时间、设备信息、启动设置等。靠钮扣电池供电,断电信息不丢失)中的内容是否正确、有哪些硬件、检查某些硬件的状态。如果出错,由于VGA未激活,无法从屏幕上查看相关信息,只能通过主板的蜂鸣声进行判断。
-
BIOS进行POST(Power-On Self-Test, 硬件自检),初始化硬件
检查硬件设备能否正常工作,并执行硬件的ROM代码进行硬件初始化。
自检通过后,BIOS根据设置的启动顺序(Boot Sequence)把控制权转交给排在第一位的存储设备。可以通过狂按F12打开启动顺序菜单手动选择要加载的存储设备。
-
BIOS引导程序读取位于该设备第一个扇区(512bytes)的主引导记录(MBR,Master boot record)到内存中,加载并将控制权转交给bootloader
MBR组成如下(下文单位都为byte)
0-445 bootloader
446-509 磁盘分区表(Disk Partition table)
510-511 MBR结束标志。如果值为0x55和0xAA,表明这个设备可以用于启动,否则将控制权转交给"启动顺序"中的下一个设备。
由于分区表的长度只有64个字节,每个表项16个字节。所以最多只能定义四个分区,称为主分区。
分区表表项组成如下:
0 flag,如果为0x80,就表示该主分区是激活分区,可引导
1-3 分区第一个扇区的物理位置(磁头号+扇区号+柱面号)
4 分区类型。0B为FAT32,07为NTFS,83为ext
5-7 分区最后一个扇区的物理位置(磁头号+扇区号+柱面号)
8-11 分区第一个扇区的相对于磁盘开始的位置。
12-15 分区的扇区总数(4byte可以表示2^32个扇区)
为了解决主分区不够的问题,引入了扩展分区,即通过表示为扩展分区。通过对扩展分区进一步进行划分,可以得到多个逻辑分区。逻辑分区通过维护下一个逻辑分区的起始位置,像链表式地串了起来。但需要注意的是扩展分区只能有一个。
每个逻辑分区的第一个扇区为扩展引导记录(EBR, Extended boot record),组成如下:
0-445 未用。可能用来存放其他boot loader
446-461 第一个分区表项
462-477 第二个(下一个)分区表项
478-493 第三个分区表项,未用
494-509 第四个分区表项,未用
510-511 结束标志:0x55 0xAA
分区表项结构和MBR的分区表项相同。
BIOS引导程序检查分区表寻找一个激活的分区。一旦找到,就开始在/boot寻找bootloader,并把它加载到RAM。
-
执行bootloader
目前Liunx下主流的bootloader为GRUB2。它是GRUB的升级重构版,相比GRUB有以下变化:
-
配置文件由/boot/grub/menu.lst改为/boot/grub/grub.cfg 不应该直接修改配置文件,而是修改/etc/default/grub后通过update-grub来更新之。
-
已经模块化,不再需要'stage1.5'
-
磁盘分区编号变更。现在第一个磁盘分区是 1 而不是 0。然而第一个设备仍然是 0
-
引入菜单项生成脚本
脚本存放在/etc/grub.d下,负责生成菜单项。它们按文件名前端的数字按从小到大的顺序依次执行。
00_header负责解析/etc/default/grub配置文件。
10_linux为/boot下的每个内核创建一个正规菜单项和一个恢复菜单项。
20_memtest86+为memtest86+(内存测试工具)创建菜单项。
20_linux_xen为xen创建菜单项。
30_os-prober为其它分区里的操作系统创建菜单项。能够识别安装 Linux、 Windows、 BSD 以及 Mac OS X 。
40_custom,供用户手动添加菜单项。
GRUB能够将控制权转交给其它bootloader,如转交给Windows的bootloader,这时只需在40_custom加入以下内容即可:
menuentry "Windows" { insmod chain insmod ntfs set root=(hd0,1) chainloader +1 }
(hd0,1)表示第一个磁盘设备的第一个分区(设备号从0开始数,而分区号从1开始数)。该配置会增加Windows菜单项,选中后控制权将转交给hd0的第一个分区(ntfs格式),启动其位于boot sector的bootloader。
还可以跳回MBR的bootloader:
menuentry 'Go to MBR' --id 'mbr' { insmod chain set root=(hd0) chainloader +1 }
由于MBR中提供给bootloader的空间有限(只有446 byte),所以需要进行二阶段加载(stage1.5是可选的,一般没有)。
stage1:加载存放在MBR前446个byte中的boot.img,其中硬编码了stage2要用到文件的磁盘位置。它将core.img读入内存,然后将控制权交给它
stage2:动态加载链接的mod(/boot/grub2/i386-pc/*.mod),执行配置文件(/boot/grub/grub.cfg)生成菜单项供用户选择(按ESE/Shift可避免菜单界面被跳过),将控制权转交给用户选择或默认的启动项,比如说某个Linux kernel。
我们选择的菜单项的配置如下:
menuentry 'Ubuntu, with Linux 4.4.0-57-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.4.0-57-generic-advanced-de5274e8-7fd2-47f0-b567-eed623d05ada' { recordfail load_video gfxmode $linux_gfx_mode insmod gzio if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi insmod part_msdos insmod ext2 set root='hd0,msdos1' if [ x$feature_platform_search_hint = xy ]; then search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 de5274e8-7fd2-47f0-b567-eed623d05ada else search --no-floppy --fs-uuid --set=root de5274e8-7fd2-47f0-b567-eed623d05ada fi echo 'Loading Linux 4.4.0-57-generic ...' linux /boot/vmlinuz-4.4.0-57-generic root=UUID=de5274e8-7fd2-47f0-b567-eed623d05ada ro find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US quiet echo 'Loading initial ramdisk ...' initrd /boot/initrd.img-4.4.0-57-generic }
该配置先载入了gzio驱动(用来解压initrd)、msdos(用来支持硬盘的MS-DOS分区)和ext2驱动(为了挂载/boot目录),然后根据initrd和linux的路径进行加载,从而进入下一阶段。
-
-
加载kernel
kernel其实就是/boot/vmlinuz-xxx。它是可引导的、用gzip压缩过Linux kernel。该文件在开头部分内嵌有gzip解压缩代码,能够进行自解压。
$ sudo file vmlinuz-4.4.0-57-generic vmlinuz-4.4.0-57-generic: Linux kernel x86 boot executable bzImage, version 4.4.0-57-generic (buildd@lgw01-54) #78-Ubuntu SMP Fri Dec 9 23:, RO-rootFS, swap_dev 0x6, Normal VGA
可以通过以下方式解压:
// 寻找gzip第一个签名的位置 $ od -t x1 -A d vmlinuz-4.4.0-57-generic | grep "1f 8b 08" 0018864 ac fe ff ff 1f 8b 08 00 00 00 00 00 02 03 ec dd // 18864 + 偏移量4(ac fe ff ff) = 18868 $ dd if=vmlinuz-4.4.0-57-generic bs=1 skip=18868 | zcat > vmlinuz-4.4.0-57-generic-decompress $ file vmlinuz-4.4.0-57-generic-decompress vmlinuz-4.4.0-57-generic-decompress: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=d3b00331a23bbf9d77720a19923606381623b3cb, stripped $ strings vmlinuz-4.4.0-57-generic-decompress | grep 'Linux version' Linux version 4.4.0-57-generic (buildd@lgw01-54) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) ) #78-Ubuntu SMP Fri Dec 9 23:50:32 UTC 2016 (Ubuntu 4.4.0-57.78-generic 4.4.35)
kernel加载后,开始进行一些初始化工作,比如加载驱动等。在以往驱动类型少的时候,直接把驱动嵌入到kernel里就完事了。但随着设备的发展,文件系统的类型越来越多,可能是各种各样的存储设备(RAID/USB/MTD),网络的文件系统,甚至是加了密的文件系统,这些文件系统都需要专有的驱动。为了能够加载这些文件系统,将一大堆驱动都打包进kernel显然不现实。
于是我们将驱动从kernel中分离开来,放到磁盘上,kernel将磁盘挂载后再进行动态加载驱动。但这时问题来了:kernel需要加载驱动来挂载磁盘,而驱动又存于磁盘上,这不就是鸡生蛋蛋生鸡的问题吗。为了解决这个问题,Linux引入了initrd(Initial RAM Disk),来为kernel提供初始化时所需要的文件系统环境。
initrd是ramdisk类型的文件,本质上是块设备,只提供了块设备的读写接口。kernel将其解压后挂到
/
目录上,从而得到一系列驱动和启动脚本。然而initrd有以下缺点:
- 作为块设备,kernel在使用时依然需要文件系统(ext2)的支持,意味着要把ext2驱动编译进kernel
- 需要挂载,挂载后还要用被称为"dirty hack"的pivot_root切换根目录
- 即使ramdisk本身就在内存中,但依然要先复制到page cache中来使用,相当于在内存中存在了两份。
- 没有弹性,分配容量后,未用区域被浪费,而扩展需要重新格式化
- 基于root device是块设备的假设
- 不以pid为1运行初始化程序
因此自Linux kernel 2.6以来,initrd被initramfs(Initial Ram File System)取代。initramfs是rootfs(ramfs的一种)类型的文件,无需挂载,由cpio解压后能够直接被加载到内存。但为了保持一致性,initramfs依然按传统以initrd为名。
挂载initramfs后,执行/init脚本,挂载根设备到$rootmnt(默认为/root)下,然后切换根目录,运行真正的init程序。
关于该阶段在此后将有更详细的文章进行分析。
-
启动userspace程序
执行对应平台汇编代码(arch/i386/boot/head.S),进行一些硬件的初始化和环境初始化(如栈设置),然后执行通用内核代码(init/main.c),调用start_kernel。最后,调用kernel_thread(arch/i386/kernel/process.c)执行init。
init是kernel初始化后第一个运行的进程
/sbin/init
(如Upstart、systemd,详情可以看下一篇文章[init之争]),pid为1。它根据配置文件启动其他进程。就绪后,提供用户登陆方式,如命令行(getty)登录,ssh(sshd)登陆,图形界面等。
至此,系统的启动过程就结束了,总结一下,启动流程如下:
其他
正文塞不下都塞到这里了。
UEFI(Unified Extensible Firmware Interface)
Legacy BIOS由于历史原因,存在以下问题:
-
以Real mode(16bit)启动,而目前主流CPU已是64bit
-
寻址空间只有1MB(2^20)
-
以中断向量的方式与硬件进行交互,需要将中断处理程序放到内存中,但空间受限
-
界面丑陋,操作死板,实现困难,易用性低
为了避免BIOS拖后腿,英特尔牵头搞出UEFI。相比BIOS,有以下优点:
-
取消Real mode。突破传统16位代码的寻址能力,能够在32位或64位乃至未来更多位的模式下运行。
-
事先储存硬件信息,跳过POST阶段(出问题时还是要重新POST的),加快启动速度
-
取消中断服务,利用加载EFI驱动的形式来和硬件进行交互,一般驱动被存放EFI分区中。
-
几乎全由C编写,降低了实现难度
吐槽:一切都挺好,然而微软搞了个大新闻,让市面上贴着Windows认证的带UEFI固件的机器都附带并开启了Secure Boot功能。该功能号称为了保证操作系统的安全性,程序需要有合法的签名才会被boot,从而保证恶意代码无法被执行在boot阶段不会被执行。然而真实目的你懂的,没签名的bootloader无法加载,自然系统也无法加载,系统启动的生杀大权掌握在微软手上。在饱受抨击后,微软为部分系统签了名(如Ubuntu),并声明用户可以手动关闭Secure Boot功能。对此Liunx阵营是拒绝的,依然在玩BIOS+MBR那一套。。
UEFI一般采用了GPT作为分区表格式,其不包含bootloader。在启动时,它会读取EFI分区,如果EFI分区存在且有bootloader,则将控制权转交给它。否则退化到BIOS的启动方式(说的就是GRUB),读取位于第一个扇区的Protective MBR到内存中,将控制权交给里面的Bootloader。
GPT(GUID Partition Table)
相比传统分区表MBR,GPT最多支持128个分区,并且每个分区最大容量能够达到EB级。
GPT将分区信息存储在分区表项区中,但出于兼容性考虑,磁盘的第一个扇区仍然用作MBR(Protective MBR),用来防止不支持GPT的硬盘管理工具错误识别并破坏硬盘中的数据。该MBR只有一个标识为0xEE的分区,以此来表示这块硬盘使用GPT分区表。不能识别GPT硬盘的系统通常会识别出一个未知类型的分区,然后拒绝对硬盘进行操作。而能够识别GPT的系统会加载下一个扇区,即GPT分区表头,其结构如下:
0-7 签名(EFI PART)
8-11 版本
12-15 分区表头的大小
16-19 分区表头(0-91)的CRC32校验,在计算时把这个字段作为0处理
20-23 保留
24-31 当前LBA(location of this header copy)
32-39 备份LBA
40-47 第一个可用于分区的LBA
48-55 最后一个可用于分区的LBA
56-71 硬盘GUID/UUID
72-79 分区表项的起始LBA(默认为2)
80-83 分区表项的数量
84-87 分区表项的大小(默认为128)
88-91 分区数组的CRC32校验
92- 保留
GPT分区表头扇区后跟着的是GPT分区表项,每个表项大小为128byte,结构如下:
0-15 分区类型的GUID
16-31 分区的GUID
32-39 分区的起始LBA
40-47 分区的末尾LBA
48-55 属性
56-127 分区名
可以通过parted查看当前分区表类型:
sudo parted -l | grep "Partition Table"
Partition Table: msdos
msdos其实就是MBR。如果显示gpt,则分区表类型为GPT。
配置GRUB2
可供用户修改的配置文件:/etc/default/grub
。常用配置如下:
-
GRUB_DEFAULT
默认选择的启动菜单项。grub默认启动最新安装的kernel,但用户可以通过该选项自定义。
查找
/boot/grub/grub.cfg
文件中的submenu,得到一级标题,如 Advanced options for Ubuntu查找menuentry,得到二级标题,如:Ubuntu, with Linux 4.4.0-59-generic
然后拼起来:
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 4.4.0-59-generic"
-
GRUB_SAVEDEFAULT
设置为true后,如果GRUB_DEFAULT为saved,则默认启动上一次选择的启动项。
-
GRUB_TIMEOUT
菜单显示时间。
-
GRUB_DEVICE
指定根设备,会覆盖自动检测得到的设备,会在配置文件menuentry的linux项中体现(root=...)。
-
GRUB_DEVICE_UUID
指定根设备的UUID,会覆盖自动检测得到的设备,会在配置文件menuentry的linux项中体现(root=...)。
-
GRUB_DISABLE_RECOVERY
不生成recovery菜单项。recovery菜单项实际上就是在linux项中加了recovery参数。
-
GRUB_CMDLINE_LINUX
传递给kernel的参数。会在配置文件menuentry的linux项中体现。
-
GRUB_CMDLINE_LINUX_DEFAULT
传递给kernel的参数。只会在non-recovery配置文件menuentry的linux项中体现。
-
GRUB_TERMINAL
启动模式。
默认为GUI模式,可以设置为以text mode启动:
GRUB_TERMINAL=console
尽管提供了很多选项,然而有时候我们希望能够完全自定义menuentry,可以在/etc/grub.d/40_custom自己写menuentry。
配置完成后记得通过以下命令更新最终配置文件:
sudo update-grub
可以在/boot/grub/grub.cfg
查看最终生成配置文件,该文件不推荐修改,由update-grub自动生成。
/boot
目录包含以下文件:
-
vmlinuz
压缩后的kernel。
-
initrd.img
压缩后的initramfs。
-
System.map
kernel自身的符号映射表。一些应用程序利用此来将地址转换为名称。
-
config kernel的配置文件。该kernel编译时用的就是该配置文件,只是让你看看kernel包含了什么特性而已,修改它并不能改变什么,除非重新编译。
-
grub
grub目录。包括grub的配置文件、启动时需要加载的一些mod等。
-
abi
应用程序二进制接口,同样是符号表,向应用程序提供驱动和内核接口。 Linux内核通过地址来识别变量或函数,为了用户编程方便,维护符号(变量名/函数名)到它们地址的映射。
这个目录的文件系统为ext2,和/
是分离开,独立被挂载。
参考
《鸟哥的Linux私房菜》
http://stackoverflow.com/questions/10603104/the-difference-between-initrd-and-initramfs
https://en.wikipedia.org/wiki/Master_boot_record
https://www.ibm.com/developerworks/cn/linux/l-linuxboot/
https://www.ibm.com/developerworks/library/l-grub2/
https://wiki.gentoo.org/wiki/GRUB2
https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt