从无盘启动看Linux启动原理

0. 故事的开始

0.1 为什么和做什么

最近家里买了对音响,我需要一个数字播放器。一凡研究后我看上了volumio 这是一个基于Debian二次开发的HIFI播放器系统,可以运行下x86和树莓派上。

我打算让volumio运行在我2009年购买的老爷机笔记本上,也让它发挥一点余温热。正常操作是将volumio的系统镜像刷到U盘上,连接电脑后使用U盘启动系统即可。但是家里没有找到合适的U盘(穷~~),加上前段时间听了同事关于linux内核的分享,感慨自己对系统的理解不够。因此我决定使用无盘启动volumio顺便研究一下linux启动原理。

目标:无盘启动volumio系统

0.2 方案

正常Linux启动流程大体如下:

  1. BIOS启动,完成自检,选择启动硬件
  2. 如果是磁盘系统读取MBR
  3. 从MBR指示,找到GRUB所在分区,加载GRUB显示菜单
  4. 加载Linux内核到内存中
  5. 执行INIT程序
  6. 进入用户界面

由于我需要从网络启动,过程会变得复杂一些,主要变化如下

  • 在MBR引导前,需要执行一系列的PXE流程,目的是挂载iscsi磁盘。
  • 在加载linux内核后,由于之前iPXE固件已经退出,还需要再次挂载iscsi磁盘。

0.3 准备工作

无盘启动并不是说完全没有磁盘,只是客户端本身没有磁盘,我们需要在远端给机器提供一种文件存储和磁盘共享的方案。 我这里选择的是iscsi共享,相比于NFS和samba共享,它更底层,对系统的兼容性更好。

iSCSI利用了TCP/IP作为沟通的渠道。透过两部计算机之间利用iSCSI的协议来交换SCSI命令,让计算机可以透过高速的局域网集线来把SAN模拟成为本地的储存设备。

关于iscsi的配置不是本文重点,这里就不详细描述了,要完成iscsi磁盘的挂载需要接信息。

iscsi服务器地址:我这里是nas服务的地址192.168.3.5

target名称:这个是服务端用来区分目标的,通常一个target服务一个客户端,并关联一块共享存储,例如:iqn.2005-10.org.freenas.ctl:yong-pc.volumio

initiator名称:这个是客户端名称,用来告诉服务端谁来请求了。

1 BIOS和UEFI

准备工作做完,我们先来了解一下计算机的启动原理,这里就要说到BIOS和UEFI,他们是计算机按下电源后最先被执行的程序。

1.1 BIOS (Basic Input/Output System)

上个世纪70年代初,"只读内存"(read-only memory,缩写为ROM)发明,开机程序被刷入ROM芯片,计算机通电后,第一件事就是读取它。这块芯片里的程序叫做"基本输入输出系统"(Basic Input/Output System),简称为BIOS。

BIOS程序首先检查,计算机硬件能否满足运行的基本条件,这叫做"硬件自检"(Power-On Self-Test),缩写为POST。 硬件自检完成后,BIOS把控制权转交给下一阶段的启动程序。

这时,BIOS需要知道,"下一阶段的启动程序"具体存放在哪一个设备。也就是说,BIOS需要有一个外部储存设备的排序,排在前面的设备就是优先转交控制权的设备。这种排序叫做"启动顺序"(Boot Sequence)。

1.2 UEFI (Unified Extensible Firmware Interface)

不知道大家是否发现,这些年已经很难看到BIOS的身影了。

ROM的存储能力有限,BIOS能驱动的硬件类型和数量大大受限。导致大量新硬件无法在PC启动时被加载。最明显就是你无法在BIOS时使用鼠标。此外BIOS的代码历史悠久难以维护。

在2005年年中时候,包括BIOS供应商、OS供应商、系统制造商以及芯片生产公司在内的行业参与者统一建立了统一的EFI联盟(UEFI,Unified Extensible Firmware Interface)并在2006年一月发行了UEFI规范2.0。

从此你可以愉快的在PC启动初期使用鼠标,甚至像苹果一样加载网络,实现联网下载并安装操作系统。

UEFI的启动流程和BIOS的启动流程不同,由于我2009年购买的老爷机还是BIOS结构,这里不详细展开,简单提一下。

  • 系统开机 - 上电自检(Power On Self Test 或 POST)。
  • UEFI 固件被加载,并由它初始化启动要用的硬件。
  • 固件读取其引导管理器以确定从何处(比如,从哪个硬盘及分区)加载哪个 UEFI 应用。
  • 固件按照引导管理器中的启动项目,加载UEFI应用。
  • 已启动的 UEFI 应用还可以启动其他应用(对应于 UEFI shell 或 rEFInd之类的引导管理器的情况)或者启动内核及initramfs(对应于GRUB之类引导器的情况),这取决于 UEFI 应用的配置

2. PXE

回到我的BIOS老爷机,上电自检完成后BIOS按照设置的启动顺序应该交棒磁盘,但是 但是 但是 这个机器没有硬盘,也没有插入U盘,找不到任何启动设备的BIOS将控制权交给了网卡,BIOS光荣退场进入了PXE阶段。

**预启动执行环境(Preboot eXecution Environment,PXE,也被称为预执行环境)**提供了一种使用网络接口启动计算机的机制。这种机制让计算机的启动可以不依赖本地数据存储设备(如硬盘)或本地已安装的操作系统。

2.1 PXE原理

  1. Client向DHCP发送IP地址请求消息,DHCP返回Client的IP地址,同时将启动文件(如:pxelinux.0)的位置信息(通常是TFTP路径)一并传送给Client
  2. Client向TFTP发送获取启动文件请求消息,TFTP接收到消息之后再向Client发送启动文件大小信息,试探Client是否满意,当TFTP收到Client发回的同意大小信息之后,正式向Client发送启动文件Client执行接收文件
  3. Client向TFTP发送针对本机的配置信息文件请求,TFTP将配置文件发回Client,继而Client根据配置文件执行后续操作。
  4. Client会加载启动文件,之后根据配置执行动作。这里有多重方案进行下一步操作。
  • 可以直接通过Http协议获取Linux kernel和ramdisk然后启动
  • 或者加载一块iscsi磁盘,将linux kernel和ramdisk等信息放在iscsi磁盘中,走正常磁盘引导。我用的是这种方案

2.2 iPXE

上面说到了启动文件,普通的pxe启动文件功能有限,通常只能从tftp服务器上获取文件,不支持HTTP协议和其他共享协议,更别说我们要支持的iscsi磁盘挂载了。这里推荐一个高端开源pxe启动文件:iPXE(https://ipxe.org/)。 它支持从HTTP、iscsi SAN、 Fibre Channel SAN、AoE SAN等多种方式启动,甚至还支持无线网卡。此外它还可以定制一个启动脚本和菜单。

iPXE需要根据自己硬件对应的平台进行编译,编译前需要搞清楚几个要点:

  • 启动方式:BIOS或者EFI前面已经说了。
  • 平台:X86或ARM,如果用树莓派等产品就是ARM,PC是x86
  • CPU位:32或64,32位机器只支持32位固件,64位机器可以兼容32位和64位固件。注意:如果使用64位固件需要保证后续所有环节使用兼容64位的软件,我就遇到了SysLinux不支持64位,导致卡死的问题。

使用如下命令编译(更多细节见:https://ipxe.org/appnote/buildtargets):

git clone git://git.ipxe.org/ipxe.git
make [platform]/[driver].[extension]

Platform支持如下:按照上面说的启动方式、平台、CPU情况选择。

  • bin (alias for bin-i386-pcbios)
  • bin-i386-pcbios
  • bin-i386-efi
  • bin-i386-linux
  • bin-x86_64-efi
  • bin-x86_64-linux
  • bin-x86_64-pcbios
  • bin-arm32-efi
  • bin-arm64-efi

Driver:主要选择支持的网卡驱动类型,一般选ipxe(表示所有支持的网卡,但可能导致生成的启动文件过大,如果过大可以酌情选其它)

Boot type:和启动方式、启动介质有关,参考下表:

编译时添加 EMBED={脚本名称} 可以关联一个启动脚本。推荐一个大佬做好的脚本 http://boot.netboot.xyz/menu.ipxe 可以直接使用。

我最终命令如下:

git clone git://git.ipxe.org/ipxe.git
cd ./ipxe/src
wget http://boot.netboot.xyz/menu.ipxe
make bin-i386-pcbios/ipxe.pxe EMBED=menu.ipxe

完成之后在/data/ipxe/src/bin-i386-pcbios/ipxe.pxe可以拿到最终的启动文件。

2.3 DHCP、TFTP配置

如何配置DHCP和TFTP服务器不是本文重点,如果需要命令行方式配置可以参考这篇文章的前半部分https://blog.51cto.com/dyc2005/2068188

如今大部分高端路由器或开源路由器固件都内置了DHCP和TFTP配置功能。我家的LEDE路由器配置界面如下。

  • TFTP服务器根目录:这个是启动文件、配置文件存放的目录路径(是在路由器上的路径,可以放在u盘挂上去,也可以直接放在路由器存储的目录
  • 网络启动镜像:这是对客户端下发的启动文件名称。(不同CPU架构,不同平台的文件名不同)

拷贝之前编译好的ipxe.pxe和menu.ipxe文件到/www/pxe/目录下,并设置网络启动镜像为:ipxe.pxe

配置正确,启动后就可以看到如下选择界面了:

3. 分区:MBR和GPT

ipxe完成使命后,正式交棒给磁盘,如果你是硬盘启动,可以直接跳过第2部分,直接到这一步。这一阶段系统需要从磁盘上找到启动文件并加载。在说如何找到启动文件前,先要说说硬盘是如何划分区块的,主要有两大方式MBR和GPT。我们先来聊一下机械硬盘的工作原理。

机械硬盘由坚硬金属材料制成的涂以磁性介质的盘片,盘片两面称为盘面或扇面

假设磁头不动,硬盘旋转,那么磁头就会在磁盘表面画出一个圆形轨迹并将之磁化,数据就保存在这些磁化区中,称之为磁道,将每个磁道分段,一个弧段就是一个扇区。一个硬盘可以包含多个扇面,扇面同轴重叠放置,每个盘面磁道数相同,具有相同周长的磁道所形成的圆柱称之为柱面,柱面数与磁道数相等。如下图:

最初的寻址方式称为CHS,所谓CHS即柱面(cylinder)磁头(header)扇区(sector),通过这三个变量描述磁盘地址。

3.1 MBR

说了这么多还是没说明白到底计算机怎么从磁盘上找到引导程序。答案是:它被固定写死在了 0柱面,0磁头,1扇区的位置通常是512byte,这个位置被称为主扇区(Master Boot Record, MBR)。

MBR主要包含如下数据:

  • 主引导记录(bootloader),负责从活动分区加载并运行系统引导程序。446字节
  • 硬盘分区表项(DPT——disk partition table),由四个分区表项组成,负责记录磁盘的分区情况。64字节。
  • 硬盘有效标志(magic number),代表引导扇区结束,占用2字节。

Bootloader: 这部分记录了一段较小引导代码,用于去启动硬盘其他分区位置上更大的引导文件,例如linux操作系统的grub目录。

我们知道一个硬盘的每个分区的第一个扇区叫做boot sector,这个扇区存放的就是操作系统的loader。如上图,第一个分区的boot sector存放着windows的loader,第二个分区放着Linux的loader,第三个第四个由于没有安装操作系统所以空着。至于MBR的bootloader是干嘛呢, bootloader有三个功能:

  • 提供选单:让用户选择进入哪个系统。
  • 读取内核文件:默认启动的loader会被拷贝一份到MBR中,这样就可以直接读取内核了,图中1部分
  • 转交给其他loader:图中2,3部分

Disk Partition table: 这一部分64字节大小被均分为4份,每份大小16字节,每当我们在硬盘上创建出一个新的主分区或者扩展分区时,便会占用1个16字节的大小用于记录这个分区的相关信息(例如起始和截止柱面位置、分区文件系统类型等等)。这就是为什么mbr分区模式最多只能有4个主分区的原因。

MBR的局限:

  • 最多只支持4个主分区,超过4个就需要使用扩展分区。
  • 磁盘的最大容量只能到2.2TB

如今我家的硬盘都4T了,MBR早就不能满足需求了。你也不能怪MBR,毕竟人家1983年就提出了,比我的年纪还大。

3.2 GPT

为了解决MBR的问题,GPT分区诞生,GPT全称Globally Unique Identifier Partition Table,也叫GUID分区表,它是UEFI规范的一部分(但这并不是说它只支持UEFI,它也支持BIOS方式的引导)。

GPT分区结构如下:

  • Protective MBR:GPT分区表的最前面部分也保存了和MBR相同的格式和内容称为Protective MBR,这极大的提高了GPT分区表的兼容性。
  • 主GPT Header:这里记录了分区表项目数和每项目大小。
  • 主GPT分区表:包含分区的类型GUID,名称,起始终止位置,该分区的GUID以及分区属性
  • 实际分区
  • 备份GPT分区表: 用于提高安全性,防止主GPT分区表损坏
  • 备份GPT Header: 用于提高安全性,防止主GPT Header损坏

3.3 Bootloader写入

使用dd命令结合hexdump可以输出MBR信息

dd if=~/Desktop/volumio-2.799-2020-07-16-x86.img ibs=512 count=1 | hexdump -C

同样的使用dd命令可以拷贝MBR信息从img文件到物理磁盘。(之前我是分分区写入到磁盘的,导致MBR信息丢失无法引导)

dd if=~/Desktop/volumio-2.799-2020-07-16-x86.img ibs=512 count=1 of=/dev/sda

也可以使用下载的syslinux中的mbr.bin写入

dd bs=440 count=1 conv=notrunc if=/usr/lib/syslinux/bios/mbr.bin of=/dev/sda //MBR分区表
dd bs=440 count=1 conv=notrunc if=/usr/lib/syslinux/bios/gptmbr.bin of=/dev/sda //GPT分区表

4. 引导加载程序:Syslinux和GRUB

前文说到MBR的bootloader主要功能是交棒内核,但是bootloader不会直接拉起linux内核,400K太小,它没有能力将linux内核直接加载到内存。这时需要引导加载程序登场,它的主要目的就是将系统内核镜像和initrd镜像加载到内存并将控制权交给它们。目前常用的有两种Syslinux和GRUB:

  • Syslinux是一个启动加载器集合,可以从硬盘、光盘或通过 PXE 的网络引导启动系统。支持的文件系统包括 FAT,ext2,ext3,ext4 和非压缩单设备 Btrfs 文件系统。
  • GRUB ,即GRand Unified Bootloader(大一统启动加载器),是一个多重启动加载器,承自PUPA项目。今的GRUB也被称作GRUB 2,而GRUB Legacy 表示0.9x版本。

对于普通用户来说他们有什么用呢?它可以提供选单选择Linux内核版本,此外加载程序使得我们可以向Linux内核传递参数。这点很重要,在我的案例中volumio就是通过Syslinux向内核传递启动参数的。

Syslinux已经不支持bios64位系统了,目前使用GRUB2 的比较多。由于volumio使用的是Syslinux我没有对GRUB展开研究。

下图是volumio的默认syslinux配置。

  • LINUX命令:指定了当前内核文件为vmlinuz-3.18.5版本;
  • INITRD命令:指定了initrd文件为volumio.initrd(之后修改initrd也就是修改这个文件);
  • APPEND命令:是向内核传递的参数,在下文initrd的init shell中可以通过cat /proc/cmdline读取到。

这里指定了imgpart,bootpart的uuid用于挂载分区,imgfile名字用于确定当前真实root分区的文件名,还有loglvevel、USE_KMSG等参数。

5. 内核:vmlinuz和initrd

引导加载程序交棒之后系统进入内核引导阶段。这一步会在内存中运行系统内核和根文件系统。之后根目录下的init shell会被调用执行,完成进一步的初始化操作。

5.1 vmlinuz和initrd

vmlinuz是可引导的、压缩的内核。“vm”代表“Virtual Memory”。Linux能够使用硬盘空间作为虚拟内存,因此得名“vm”。vmlinuz是可执行的Linux内核。

initrd是“initial ramdisk”的简写。initrd一般被用来临时的引导硬件到实际内核vmlinuz能够接管并继续引导的状态。initrd 字面上的意思就是"boot loader initialized RAM disk",换言之,这是一块特殊的RAM disk,在载入Linux kernel前,由boot loader予以初始化,启动过程会优先执行initrd的init程序,initrd完成阶段性目标后,kernel 会挂载真正的root file system ,并执行/sbin/init程序。

采用这种分离的方式,使得我们有机会在内核引导阶段做一些我们自己的事情。简单读了volumio.initrd中的init shell发现它至少做了几件事情:

  1. 读取syslinux传递来的环境变量
  2. 根据变量决定是否在屏幕打印日志。 USE_KMSG参数决定
  3. 加载各种内核驱动模块
  4. 挂载boot分区
  5. 使用fdisk处理磁盘,img文件写入磁盘后大小不一致,首次启动需要使用fdisk命令调整分区大小
  6. 挂载一个imgpart分区,这个不是真正的root分区,这里面的volumio_current.sqsh文件才是,这样做的目的是方便系统升级,在系统内替换imgpart分区的volumio_current.sqsh文件即可完成系统升级。volumio_current.sqsh文件名也是通过 imgfile 参数决定的。
  7. 处理volumio_current.sqsh升级问题,发现有新的 volumio.sqsh文件会重命名旧的,然后将新的重命名volumio_current.sqsh
  8. 使用overlay方式结合volumio_current.sqsh文件挂载真正的root分区。
  9. 执行switch_root命令,重定向新的根分区并执行/sbin/init命令。

5.2 initrd编辑

由于linux内核启动后,之前ipxe对应的环境已经退出,因此之前挂载的iscsi磁盘也无法访问,需要在initrd的init shell中重新挂载iscsi磁盘。因此我需要在上文的4步骤之前挂载iscsi磁盘,修改如下:

  1. 加载网卡内核驱动
  2. 启动网络
  3. 启动iscsi客户端挂载网络磁盘。

可以使用如下方式编辑已经生成好的initrd文件。

mount -o loop,offset=1048576 ./wrt/Build/Volumio2.799-2020-09-29-x86.img ./vboot/ //挂载img镜像的boot分区到目录
cp ../vboot/volumio.initrd volumio.initrd.gz //拷贝initrd文件,重命名一下
gunzip ./volumio.initrd.gz //解压gz文件
cpio -ivmd < volumio.initrd //展开initrd文件,在当前目录就可以看到整个rom disk的内容了

vim init //编辑init shell

find . | cpio -c -o > ../volumio.initrd.img //重新打包成新的initrd
gzip volumio.initrd.img 
mv volumio.initrd.img.gz volumio.initrd

还有另外一种方案,由于volumio是开源项目,编译volumio的脚本在github开源。我可以编辑编译脚本,直接修改init之后编译成新的initrd文件。

git clone https://github.com/volumio/Build.git
ls -la scripts/initramfs/init-x86
ls -la scripts/x86config.sh
  • x86config.sh 这是编译生成x86版本volumio镜像的脚本,在这个文件中,我们需要添加命令,使得生成的initrd文件中包含iscsi客户端
  • init-x86 这个文件是initrd文件在系统启动后,需要执行的init shell脚本。这里我们需要添加 网卡驱动、初始化iscsi客户端。

首先处理x86config.sh脚本,我们需要在initrd中添加iscsi客户端下图中: 193-195行安装iscsi客户端 231-232行向initrd中添加iscsi模块

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java