作者:bobyzhang,腾讯 IEG 运营开发工程师
0. 故事的开始
0.1 为什么和做什么
最近家里买了对音响,我需要一个数字播放器。一凡研究后我看上了volumio(
我打算让volumio运行在我2009年购买的老爷机笔记本上,也让它发挥一点余温热。正常操作是将volumio的系统镜像刷到U盘上,连接电脑后使用U盘启动系统即可。但是家里没有找到合适的U盘(穷~~),加上前段时间听了同事关于linux内核的分享,感慨自己对系统的理解不够。因此我决定使用无盘启动volumio顺便研究一下linux启动原理。
目标:无盘启动volumio系统
0.2 方案
正常Linux启动流程大体如下:BIOS启动,完成自检,选择启动硬件
如果是磁盘系统读取MBR
从MBR指示,找到GRUB所在分区,加载GRUB显示菜单
加载Linux内核到内存中
执行INIT程序
进入用户界面
由于我需要从网络启动,过程会变得复杂一些,主要变化如下在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原理Client向DHCP发送IP地址请求消息,DHCP返回Client的IP地址,同时将启动文件(如:pxelinux.0)的位置信息(通常是TFTP路径)一并传送给Client
Client向TFTP发送获取启动文件请求消息,TFTP接收到消息之后再向Client发送启动文件大小信息,试探Client是否满意,当TFTP收到Client发回的同意大小信息之后,正式向Client发送启动文件Client执行接收文件
Client向TFTP发送针对本机的配置信息文件请求,TFTP将配置文件发回Client,继而Client根据配置文件执行后续操作。
Client会加载启动文件,之后根据配置执行动作。这里有多重方案进行下一步操作。可以直接通过Http协议获取Linux kernel和ramdisk然后启动
或者加载一块iscsi磁盘,将linux kernel和ramdisk等信息放在iscsi磁盘中,走正常磁盘引导。我用的是这种方案
2.2 iPXE
上面说到了启动文件,普通的pxe启动文件功能有限,通常只能从tftp服务器上获取文件,不支持HTTP协议和其他共享协议,更别说我们要支持的iscsi磁盘挂载了。这里推荐一个高端开源pxe启动文件:iPXE(
iPXE需要根据自己硬件对应的平台进行编译,编译前需要搞清楚几个要点:启动方式:BIOS或者EFI前面已经说了。
平台:X86或ARM,如果用树莓派等产品就是ARM,PC是x86
CPU位:32或64,32位机器只支持32位固件,64位机器可以兼容32位和64位固件。注意:如果使用64位固件需要保证后续所有环节使用兼容64位的软件,我就遇到了SysLinux不支持64位,导致卡死的问题。
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发现它至少做了几件事情:读取syslinux传递来的环境变量
根据变量决定是否在屏幕打印日志。 USE_KMSG参数决定
加载各种内核驱动模块
挂载boot分区
使用fdisk处理磁盘,img文件写入磁盘后大小不一致,首次启动需要使用fdisk命令调整分区大小
挂载一个imgpart分区,这个不是真正的root分区,这里面的volumio_current.sqsh文件才是,这样做的目的是方便系统升级,在系统内替换imgpart分区的volumio_current.sqsh文件即可完成系统升级。volumio_current.sqsh文件名也是通过 imgfile 参数决定的。
处理volumio_current.sqsh升级问题,发现有新的 volumio.sqsh文件会重命名旧的,然后将新的重命名volumio_current.sqsh
使用overlay方式结合volumio_current.sqsh文件挂载真正的root分区。
执行switch_root命令,重定向新的根分区并执行/sbin/init命令。
5.2 initrd编辑
由于linux内核启动后,之前ipxe对应的环境已经退出,因此之前挂载的iscsi磁盘也无法访问,需要在initrd的init shell中重新挂载iscsi磁盘。因此我需要在上文的4步骤之前挂载iscsi磁盘,修改如下:加载网卡内核驱动
启动网络
启动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.shx86config.sh 这是编译生成x86版本volumio镜像的脚本,在这个文件中,我们需要添加命令,使得生成的initrd文件中包含iscsi客户端
init-x86 这个文件是initrd文件在系统启动后,需要执行的init shell脚本。这里我们需要添加 网卡驱动、初始化iscsi客户端。
首先处理x86config.sh脚本,我们需要在initrd中添加iscsi客户端下图中: 193-195行安装iscsi客户端 231-232行向initrd中添加iscsi模块
之后处理init-x86,在118行左右的位置,脚本读取了配置在/proc/cmdline中的根目录uuid并在之后挂载磁盘。这里的cmdline就是之前说到的在syslinux阶段向内核传递的参数。所以我们要在挂载磁盘前加载网卡驱动、启动网络、启动iscsi客户端、挂载iscsi磁盘。
修改如下图:103行 加载网卡驱动
104行 加载iscsi内核模块
105行 加载iscsi ibft模块
107-108行 通过ibft配置网络
114-116行 使用ibft配置连接iscsi服务器并挂载磁盘
这里要说一下ibft这是一种将iscsi配置信息传递到系统的方式,我们在iPxe阶段已经配置网络信息、iscsi服务器地址、iscsi target等信息了,这里可以使用ibft直接读取并使用。当然你也可以在这里再次手动启用DHCP,手动初始化iscsi客户端。
修改完成后,iscsi磁盘就可以像正常本地磁盘一样被挂载,之后的操作就和正常硬盘安装一样了,正常启动进入volumio系统。
6. init进程
内核引导阶段完成以后,系统会挂载真实的root分区,执行/sbin/init程序初始化系统环境。这一阶段已经和是否网络启动没有关系了,不过启动原理都研究到现在了就顺便一起看一下吧。
/sbin/init会首先确定运行级别,这个配置在/etc/inittab中,一般Linux有7种运行级别(0-6)。一般来说,0是关机,1是单用户模式(也就是维护模式),6是重启。运行级别2-5,各个发行版不太一样,对于Debian来说,都是同样的多用户模式(也就是正常模式)。 确定运行级别后会访问/etc/rcN.d(这里的N就是运行级别)。
这里的文件都采用“字母S或K+两位数字+程序名”的命名方式。其中S开头的表示在这个级别需要执行start命令,K开头需要执行Stop命令,数字越小越优先执行。系统会依次执行相应的软件和服务,负责用户界面的程序也被启动你就有了X11界面,然后是SSH服务你就可以使用ssh登录。这样系统就完成了启动。
当然啦现在这种方式已经过时了,目前基本使用systemd方式用systemctl命令管理。篇幅已经很长了,这块有兴趣的同学自己搜索一下。
7. 尾巴
7.1 其他遇到的问题
syslinux卡死 这个问题前面说到了,挂载iscsi磁盘后ipxe交棒磁盘引导,但是就卡死了。
经过很多的google和尝试之后最终发现,我使用了64位的iPxe引导固件,但是syslinux只有32位版本导致卡死,更换了32位的iPxe固件后解决。
可以启动无法关闭 这个问题困扰了我很久,系统可以正常启动,但是在关机或者重启时会死机,按键没有任何反应但是系统应该还是活的(大小写灯正常切换)只能强制关机退出。经过排查原因可能是:关机时网络服务会关闭导致网卡关闭,进而导致iscsi网盘断开。但是此时系统根分区还没有umount导致系统无响应。
我禁用了网络服务的关机关闭,把K06networking从rc0.d目录中去掉就好了。
Airplay服务无法找到 Volumio自带shairport-sync服务,手机可以通过airplay链接volumio系统播放音乐,但是在我折腾完以后发现怎么也搜不到。经过排查shairport-sync使用mDns发布组播告诉局域网内的所有设备自己的地址,使用的是avahi-daemon程序。排查日志发现它启动时没有识别到网卡。我猜原因应该是我们的网卡是在内核引导阶段自己拉起的,并不是进入系统后由networking服务拉起的,所以avahi-daemon无法查找到它对应的ip。
我没有找到很好的解决方案,还好老爷机还有一块无线网卡,最后使用了无线网卡绑定shairport-sync服务。
7.2 最终效果
7.3 总结
总结:为了省掉一块U盘,我开始折腾iscsi无盘启动没想到这一折腾就是好久,前后研究了好多资料好好的学习了一下linux的启动原理。
实际过程并没有文中展现的那么顺利,很多研究的弯路没有在文中一一展现出来。在不同的节点也有很多方案可以选择,比如:iPxe本可以直接http下载vmlinuz和initrd引导,这样就可以省去MBR和syslinux引导。但是后来想想都研究了还是整理给大家。再比如initrd中iscsi客户端的启动和初始化有很多种方式,一开始我都手动初始化网卡,设置dhcp和ip路由。最后还是觉得太麻烦发现ibft的方案最简单,果断选择了它。
水平有限如果发现那里总结的不对欢迎指正。
你都看到这了点个赞再走吧~ 对了前几天99公益日同事10块钱买了块U盘好像挺香的~
参考文献
更多干货尽在腾讯技术,官方微信交流群已建立,交流讨论可加:Journeylife1900(备注腾讯技术) 。