2022年8月

需求是在gem5里跑SPEC程序。作为一个指令级模拟器,允许在不单独运行操作系统的情况下运行指定架构的程序本身就已经是一个巨大的工程了,而它所付出的代价则是运行效率低下和不支持动态链接(作者似乎说了在x86模式下支持加载动态库,但我没测试)。

于是对应的解决方案是,静态链接。

然而,在如今这个内外存充足、处理器足够快的时代,静态链接的用途可以说是已经被大幅削减了,如果要依赖外部程序的话,没有什么是附加一个动态库解决不了的。一定要说离不开静态链接的,可能也就只有在仍处于系统底层的内核了。对于只能识别汇编指令的处理器来说,它不知道该去哪里找对应的依赖实现,因而程序必须一字一句指导处理器执行。

好在仍然有部分C语言程序离不开静态编译,因而现有的操作系统还是提供了gcc的静态包。而fortran就不一样了,天生作为计算工具的语言往往在操作系统中执行,没必要搞静态链接,但凡一个库函数被调用了两次都是一种空间的浪费——或许是基于这种思想,几乎所有的Linux发行版都从安装源里删去了gfortran的静态依赖库,仿佛多的这几百KB的空间对服务器也是种累赘。

然而还是有人关注这个问题:在CentOS论坛中,有人解决方案。作为开源操作系统,保留源码可以说是一个非常好的文明了,而红帽系的系统甚至还提供了rpmbuild工具用来实现快捷的,从源码包到二进制包的编译功能,大致分为两步:

首先找到源码rpm包(这里推荐pkgs.org和rpmfind.net,格式工整,资料齐全)并解压:

rpm -i gcc-8.3.1-5.1.el8.src.rpm

然后用rpmbuild按照配置文件编译即可:

rpmbuild -bb ./rpmbuild/SPECS/gcc.spec

然后编译过程中可能会提示缺包缺文件,通过yum/dnf安装即可,缺失的文件也都是可以通过包管理器补上的

以经典的/lib/libc.so.6/usr/lib/libc.so为例,这两个文件是x86架构下的glibc库文件,在amd64系统上是默认不装的,但是因为rpmbuild需要,可以通过分别glibc.i686glibc-devel.i686补上。

补完缺失的依赖之后就可以快乐泡茶等编译啦~

鸽了五年来填坑了

随着人年龄越来越大,逐渐对U盘的数量不再那么感兴趣,反而觉得一串串的U盘变成了随身携带的累赘,生怕一不小心就弄丢了。

几年前就试图给自己整个活,弄一个支持Windows+Linux+MacOS的三合一安装启动盘,无奈学识尚浅,花了几周都没搞定。

最近正好手头有装服务器的需求,心中的整活之魂又燃起来了,花了一下午时间总算把坑填好。

一 明确需求

由于通常情况下不论哪个系统都要求使用一个完整的U盘(尤其是MBR时期,刻盘是最方便的MBR更新方式),所以常规情况下换个系统就要重刻一张盘。

因而本项目的基本需求就是一个U盘能够在不修改引导的情况下完成三类系统的安装,且最好同时支持兼容(后文简称为MBR)+UEFI模式。

二 设计方案

三种不同的系统安装方法有所不同,虽然W/L/M三家都支持原装启动盘启动,但是Windows的官方启动盘功能实在是少得可怜,远比不上*nix两家的shell好用,所以大部分情况下会使用PE+WinNTSetup的方法,附加工具多,可定制化程度高。因而在整体方案上,使用PE工具建立Windows安装环境,Linux、MacOS则原盘启动安装。

好在MBR/UEFI的启动方式不同,一个是读扇区,一个是读文件系统,因而两者具有共存的余地——实际上现在的绝大部分启动盘都是同时支持两种模式的。

由于MacOS很早就抛弃了MBR启动,所以MBR启动不做MacOS支持(其实也不难,加个Chameleon iso引导就行);其他方式能支持则尽可能支持:

Windows Linux MacOS
MBR ×
UEFI

三 大致步骤

由于MBR模式和UEFI模式在引导方式上有区别,因而要采用不同的方式对待。由于需要支持MBR模式,因而整体上U盘还是需要保持MBR格式的。

对于UEFI启动比较简单,首先到网上找一份efi shell,放到efi分区(也就是U盘物理地址上第一个分区)。然后启动进入efi shell之后,直接用命令行执行要uefi启动的系统引导即可。

由于只有MacOS本身可以正常操作其文件系统,通常要为MacOS的安装盘单独准备一个分区,然后在MacOS里用磁盘工具恢复分区。(对,手头需要先有一个能用的MacOS,不论是白的还是黑的)

MBR模式的稍微麻烦一点,我这里是基于大白菜魔改的。大白菜的多级菜单引导模式是基于grldr做的,也就是大名鼎鼎的grub4dos,因而需要通过修改菜单,让grub4dos来引导Linux系统的启动流程。

需要准备一个工具fbinsttool,用来读写大白菜的隐藏文件系统。大白菜专门预留了一个文件用来给我们制作自定义的启动菜单,在(ud)/IDBC/GRUB/DIY.LIST位置,在里面照抄现有启动项即可实现自定义启动项的添加。

以我配置的Rocky Linux 9为例:

title 【01】 Install Rocky Linux 9 Minimal 
# find只支持定位文件,不支持文件夹!
find --set-root --ignore-cd --ignore-floppies /RPM_GPG_KEY_ROCKY_9
kernel /ISOLINUX/VMLINUZ inst.stage2=hd:LABEL=Rocky_9_0-x86_64-dvd quiet
initrd /ISOLINUX/INITRD.IMG
boot

需要注意的是,这里最好用dd直接把光盘镜像的分区写入到U盘,不然后面的Anaconda安装可能找不到本地安装源。然后偷懒使用光盘内特有的文件来定位该分区(因为grub4dos不支持用分区名定位)。然后照常加载kernel和initrd即可完成启动。需要注意的是inst.stage2参数,该参数用于指导安装器Anaconda的加载,要是填错了或者没填会导致启动失败。(在光盘里这俩文件在不同位置出现了几次,估计是为了支持不同的启动方式吧)

最近的工作想基于Intel的AEP非易失内存的物理特性做个读写优化,哪想到这方案还没具体定下来,Intel就宣布要放弃生产所有Optane的产品了,犹如做安倍政治学的成果还没整出来,人就敞开胸怀了。

为了试图让自己做的前期工作不至于白费,必须得想个办法把这个方案从具体硬件上剥离下来,即抽出硬件上的特性作为研究点,而不要将整个方案完全依托在硬件展示出的读写特性上。

因为读写特性大多与底层结构有关,因而有必要了解硬件的底层结构。这里先从数据的底层组织入手,去看了一下之前一直忽略的DRAM的底层结构。

通常来说,DRAM可以分为五个层次:(Row/Column)/Array/Bank/Rank/Channel,分别对应其中物理结构的特性。

RowColumn对应的是DRAM最底层的硬件结构,对应的都是一组一维的线性存储结构。首先要知道的是,现有的DRAM都是呈二维方形排布的,因而可以像经典笛卡尔坐标系一样使用横纵坐标对每个存储单元寻址,而RowColumn恰好对应这两个坐标量,它们的功能完全不同。从组成DRAM的基本结构上来看,Row连接的是一组存储单元的源极,而Column连接的是一组存储单元的栅极,这也就决定了他们各自的作用:Row线用于存取DRAM电容上的0/1数据,而Column线则负责从Row线上选出需要导通用于传输数据的存储单元。

这里简要补充一个题外话,DRAM电容存储的电量有限,那么怎么有效地读取数据呢?

DRAM采用了一种非常骚的方法:反正万物皆可电容,能带电就有电压,那我让Row线充电到工作电压的一半(介于0/1之间),然后将Row线悬空,这样由于接通后存储单元的电压非高即低,那么Row线上的电压在短暂的接通后肯定会发生变化,利用电压差即可判断电容内的数据原本是高电平还是低电平了。那么与哪来的参考电压比较呢?DRAM将Row线一分为二,将电平读取电路放在中间,反正你一次只读取一位数据,总有一头是保持了原滋原味没动的,可以作为参考电压。

接着,RowColumn组成的二维阵列就称为Array,一次可以从一个Array中读取的数据量为1bit。由于一个Column要占用的资源远大于一个Row(电压比较电路、buffer等),因而在大多数现代DRAM芯片设计中,一个Column连接的单元数是远大于一个Row的。(PS. 一个DRAM读写的完整流程可分为6个阶段:解码要访问的Row地址-Column线预充电(precharge,1/2工作电压)-电压比较-数据写入buffer-根据Column地址返回要读取的bit-根据buffer数据写回数据单元)

那么,我要字节寻址(一次读写1Byte)怎么办?直接整8个Array并行放置不就好了。这就是Bank的定义:一次可从一个Bank中读取的数据大小位1Byte。而由于一个Bank通常较小,我们平常见到内存条上的一个芯片内部都是封装了多个(4/8/16)Bank的。

然后通常一个内存条上有8个芯片,可以分别同时读取,这个单位叫做Rank;而内存条级别的并行就是Channel,也就是平常常说的单通道双通道什么的,读写带宽翻倍带来的是实打实的性能提升。

对于内存条而言,通常有DIMM这个说法,即Dual-Inline Memory Module。那么这个“双”体现在哪里呢,双面都贴芯片吗?答案是针脚。仔细观察,可以发现现有内存条两面都有金手指,并且是不互通的,相当于针脚数是我们一次可以看到的数量的两倍。而早期内存条通常是SIMM,也就是说内存条两面的金手指电气功能是一致的,这便是DIMM中这个“Dual”的含义。(PS. 与体系结构相对应,SIMM单次读取宽度位32bit,DIMM则为64bit)

但是,光这么看的话,似乎还是与现代CPU 64Byte的缓存行相差甚远(即便是4-Channel也只是每周期32Byte数据)。原本内存频率就要低于CPU,结果数据速率还跟不上,听起来似乎还是有点问题。

这个时候就可以将视野重新拉回Array层次了:我们可以发现,每次为了取1bit数据,都要经过完整的从预充电到写回的流程。然而,实际上在整个流程中,读取数据占用的周期数反而很小,频繁对Row线做激活读数据操作浪费了大量的时钟周期

那么怎么办呢?考虑到程序执行正好有空间局部性,实际上连续读取内存区域(尤其是在没有分支或跳转的代码段的情况下)的概率是很高的,那么在空间上做预取是可能具有性能提升的可能的。而恰好我们可以发现,实际上在Row内部,一次读取的数据是物理地址上连续的,恰好满足连续地址预取的要求——那么我在每个内存读写周期内,多传输几个数据,不就能提升数据传输效率了吗?这个想法就是DRAM中的Burst Mode,通过一次预充电一口气读写多个DIMM宽度的方式实现了读写带宽的提升。

与想法完全相同的是,我们可以直接在芯片内添加缓冲区来将我们一次读取的多个数据打包带走。考虑到在Array层面以bit为单位做缓存对设计复杂度影响大,在实际的实现中,是把这个缓冲区放在了Bank的层面,将8个Array读出来的8个bit缓存为一个byte(写则相反,在写回之前多改几个bit)。根据规范,DDR2一次可连续从一个Array中读取4bit,DDR3/DDR4是8bit。可以看到,从DDR3开始,我们就实现了一次内存读取可以填满一个CPU缓存行的功能了。

这里顺便提一下Bank Interleaving这个东西,类似于RAID3/4/5,把地址连续的数据拆分放在了不同的物理介质上(如Chip0存地址0/4,Chip1存1/5,Chip2存2/6,Chip4存3/7),两者都含有充分利用多份物理设备的带宽的含义在内。只不过在Bank这里,由于读写完数据后还要加电刷回存储单元,要占据缓冲区并消耗多个时钟周期,导致在两次内存访问之间是有一定的绝对不应期的(雾)。使用了Bank Interleaving的话,相当于就给这些Bank建立了一个流水线,在读完上一个地址的数据后,下一个地址的预充电操作可能就已经完成,可以直接读取数据了,不需要再像单Bank场景一样,在两次内存访问之间白白等一个内存访问周期的时间。如果Bank数量和Burst长度都是2的幂指数倍的时候,还能再二进制地址上进一步做小动作来优化实现,这个就不展开说了。