
0. 故事的开始4 `4 Y( j" U# v! q 8 w% D7 q$ P; ] 0.1 为什么和做什么 最近家里买了对音响,我需要一个数字播放器。一凡研究后我看上了 volumio(https://volumio.org/) 这是一个基于 Debian 二次开发的 HIFI 播放器系统,可以运行下 x86 和树莓派上。 我打算让 volumio 运行在我 2009 年购买的老爷机笔记本上,也让它发挥一点余温热。正常操作是将 volumio 的系统镜像刷到 U 盘上,连接电脑后使用 U 盘启动系统即可。但是家里没有找到合适的 U 盘(穷~~),加上前段时间听了同事关于 linux 内核的分享,感慨自己对系统的理解不够。因此我决定使用无盘启动 volumio 顺便研究一下 linux 启动原理。 目标:无盘启动 volumio 系统 0.2 方案正常 Linux 启动流程大体如下:
![]() 由于我需要从网络启动,过程会变得复杂一些,主要变化如下
![]() 1 y; C2 _- ~& c$ k# Z: E 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 结构,这里不详细展开,简单提一下。
回到我的 BIOS 老爷机,上电自检完成后 BIOS 按照设置的启动顺序应该交棒磁盘,但是 但是 但是 这个机器没有硬盘,也没有插入 U 盘,找不到任何启动设备的 BIOS 将控制权交给了网卡,BIOS 光荣退场进入了 PXE 阶段。 预启动执行环境(Preboot eXecution Environment,PXE,也被称为预执行环境)提供了一种使用网络接口启动计算机的机制。这种机制让计算机的启动可以不依赖本地数据存储设备(如硬盘)或本地已安装的操作系统。
![]() ) }& j# _! D, V6 h" \5 f# i/ M 2.1 PXE 原理
![]() 6 r7 @% A' a* w: y( q& Q - ]( x, v8 B( e1 Q% M 2.2 iPXE 上面说到了启动文件,普通的 pxe 启动文件功能有限,通常只能从 tftp 服务器上获取文件,不支持 HTTP 协议和其他共享协议,更别说我们要支持的 iscsi 磁盘挂载了。这里推荐一个高端开源 pxe 启动文件:iPXE(https://ipxe.org/)。它支持从 HTTP、iscsi SAN、 Fibre Channel SAN、AoE SAN 等多种方式启动,甚至还支持无线网卡。此外它还可以定制一个启动脚本和菜单。 iPXE 需要根据自己硬件对应的平台进行编译,编译前需要搞清楚几个要点:
使用如下命令编译(更多细节见:https://ipxe.org/appnote/buildtargets): git clone git://git.ipxe.org/ipxe.gitmake [platform]/[driver].[extension]: C+ ~( S! l8 j- v7 Y8 e Platform 支持如下:按照上面说的启动方式、平台、CPU 情况选择。
Driver:主要选择支持的网卡驱动类型,一般选 ipxe(表示所有支持的网卡,但可能导致生成的启动文件过大,如果过大可以酌情选其它) Boot type:和启动方式、启动介质有关,参考下表: ![]() 编译时添加 EMBED={脚本名称} 可以关联一个启动脚本。推荐一个大佬做好的脚本 http://boot.netboot.xyz/menu.ipxe 可以直接使用。 我最终命令如下: git clone git://git.ipxe.org/ipxe.git, w* b, f. E7 M, K$ j4 d8 kcd ./ipxe/src5 U8 h1 R8 `; j; U# p1 r& T4 p 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 路由器配置界面如下。
拷贝之前编译好的 ipxe.pxe 和 menu.ipxe 文件到/www/pxe/目录下,并设置网络启动镜像为:ipxe.pxe 配置正确,启动后就可以看到如下选择界面了:
![]() W( M) l$ A4 l- \ f) o0 g9 g' j x 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:这部分记录了一段较小引导代码,用于去启动硬盘其他分区位置上更大的引导文件,例如 linux 操作系统的 grub 目录。 ![]() 我们知道一个硬盘的每个分区的第一个扇区叫做 boot sector,这个扇区存放的就是操作系统的 loader。如上图,第一个分区的 boot sector 存放着 windows 的 loader,第二个分区放着 Linux 的 loader,第三个第四个由于没有安装操作系统所以空着。至于 MBR 的 bootloader 是干嘛呢, bootloader 有三个功能:
Disk Partition table:这一部分 64 字节大小被均分为 4 份,每份大小 16 字节,每当我们在硬盘上创建出一个新的主分区或者扩展分区时,便会占用 1 个 16 字节的大小用于记录这个分区的相关信息(例如起始和截止柱面位置、分区文件系统类型等等)。这就是为什么 mbr 分区模式最多只能有 4 个主分区的原因。 MBR 的局限:
如今我家的硬盘都 4T 了,MBR 早就不能满足需求了。你也不能怪 MBR,毕竟人家 1983 年就提出了,比我的年纪还大。 3.2 GPT为了解决 MBR 的问题,GPT 分区诞生,GPT 全称 Globally Unique Identifier Partition Table,也叫 GUID 分区表,它是 UEFI 规范的一部分(但这并不是说它只支持 UEFI,它也支持 BIOS 方式的引导)。 ![]() GPT 分区结构如下:
使用 dd 命令结合 hexdump 可以输出 MBR 信息 dd if=~/Desktop/volumio-2.799-2020-07-16-x86.img ibs=512 count=1 | hexdump -C' Y: j" [: `! V! @3 q![]() 同样的使用 dd 命令可以拷贝 MBR 信息从 img 文件到物理磁盘。(之前我是分分区写入到磁盘的,导致 MBR 信息丢失无法引导)* k6 Q$ W: P' ]0 f% n' v dd if=~/Desktop/volumio-2.799-2020-07-16-x86.img ibs=512 count=1 of=/dev/sda. M: n0 o& ?( C. r& n2 U也可以使用下载的 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:
对于普通用户来说他们有什么用呢?它可以提供选单选择 Linux 内核版本,此外加载程序使得我们可以向 Linux 内核传递参数。这点很重要,在我的案例中 volumio 就是通过 Syslinux 向内核传递启动参数的。 Syslinux 已经不支持 bios64 位系统了,目前使用 GRUB2 的比较多。由于 volumio 使用的是 Syslinux 我没有对 GRUB 展开研究。 下图是 volumio 的默认 syslinux 配置。
这里指定了 imgpart,bootpart 的 uuid 用于挂载分区,imgfile 名字用于确定当前真实 root 分区的文件名,还有 loglvevel、USE_KMSG 等参数。 5. 内核:vmlinuz 和 initrd引导加载程序交棒之后系统进入内核引导阶段。这一步会在内存中运行系统内核和根文件系统。之后根目录下的 init shell 会被调用执行,完成进一步的初始化操作。 5.1 vmlinuz 和 initrdvmlinuz 是可引导的、压缩的内核。“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 发现它至少做了几件事情:
由于 linux 内核启动后,之前 ipxe 对应的环境已经退出,因此之前挂载的 iscsi 磁盘也无法访问,需要在 initrd 的 init shell 中重新挂载 iscsi 磁盘。因此我需要在上文的 4 步骤之前挂载 iscsi 磁盘,修改如下:
可以使用如下方式编辑已经生成好的 initrd 文件。 mount -o loop,offset=1048576 ./wrt/Build/Volumio2.799-2020-09-29-x86.img ./vboot/ //挂载img镜像的boot分区到目录% ^. y, p t" K, Q3 Icp ../vboot/volumio.initrd volumio.initrd.gz //拷贝initrd文件,重命名一下 gunzip ./volumio.initrd.gz //解压gz文件 cpio -ivmd < volumio.initrd //展开initrd文件,在当前目录就可以看到整个rom disk的内容了 vim init //编辑init shell" S/ Z! e& D8 B7 Y! P find . | cpio -c -o > ../volumio.initrd.img //重新打包成新的initrd+ h# d+ y1 e' q gzip volumio.initrd.img mv volumio.initrd.img.gz volumio.initrd 还有另外一种方案,由于 volumio 是开源项目,编译 volumio 的脚本在 github 开源。我可以编辑编译脚本,直接修改 init 之后编译成新的 initrd 文件。 git clone https://github.com/volumio/Build.git/ P, c+ V1 y, K) tls -la scripts/initramfs/init-x86 ls -la scripts/x86config.sh: i2 @) p) s1 V" e3 V# t
首先处理 x86config.sh 脚本,我们需要在 initrd 中添加 iscsi 客户端下图中:193-195 行安装 iscsi 客户端 231-232 行向 initrd 中添加 iscsi 模块 ![]() 之后处理 init-x86,在 118 行左右的位置,脚本读取了配置在/proc/cmdline 中的根目录 uuid 并在之后挂载磁盘。这里的 cmdline 就是之前说到的在 syslinux 阶段向内核传递的参数。所以我们要在挂载磁盘前加载网卡驱动、启动网络、启动 iscsi 客户端、挂载 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. 尾巴$ N# R# J, }* A; k9 o" M- e* d1 z; f6 I |3 V$ ^0 _ 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 最终效果![]() 3 `8 P6 V+ J6 S H8 W6 z3 Z+ d4 Z 7.3 总结总结:为了省掉一块 U 盘,我开始折腾 iscsi 无盘启动没想到这一折腾就是好久,前后研究了好多资料好好的学习了一下 linux 的启动原理。 实际过程并没有文中展现的那么顺利,很多研究的弯路没有在文中一一展现出来。在不同的节点也有很多方案可以选择,比如:iPxe 本可以直接 http 下载 vmlinuz 和 initrd 引导,这样就可以省去 MBR 和 syslinux 引导。但是后来想想都研究了还是整理给大家。再比如 initrd 中 iscsi 客户端的启动和初始化有很多种方式,一开始我都手动初始化网卡,设置 dhcp 和 ip 路由。最后还是觉得太麻烦发现 ibft 的方案最简单,果断选择了它。 水平有限如果发现那里总结的不对欢迎指正。 |